/* ==========================================================================
   OnUse — дизайн-система тёмного UI поверх Tailwind CDN

   Порядок разделов:
     1. токены и слои фона      6. кнопки и поля
     2. типографика             7. выбор игры и слайдеры
     3. поверхности и карточки  8. статусы, бейджи, метрики
     4. навигация и подвал      9. таблицы и адаптив
     5. утилиты и анимации     10. тосты и модалки
   ========================================================================== */

/* ==========================================================================
   1. ТОКЕНЫ
   ========================================================================== */
:root {
  --brand: 99 102 241;
  /* Светлый вариант акцента — для подписей на светлой подложке. Как
     CSS-переменная он же используется иконкой активного пункта меню. */
  --brand-400: #818cf8;
  --aqua: 34 211 238;

  /* Акцент переключается при выборе игры (см. --accent в pricing.js) */
  --accent: #6366f1;
  --accent-2: #22d3ee;

  /* Многослойный тёмный фон: подложка, приприподнятый слой, карточка */
  --bg:          #06070c;
  --bg-raised:   #0b0d14;
  --bg-card:     #0e1119;

  --line:      rgb(var(--tint) / .10);   /* базовая граница (white/10) */
  --line-soft: rgb(var(--tint) / .055);  /* внутренние разделители */
  --line-firm: rgb(var(--tint) / .16);

  --ink:       #e9ecf2;
  --ink-dim:   #9aa4b6;
  --ink-faint: #78829a;   /* 4.7:1 на тёмном — проходит WCAG AA */

  --glow-accent: 0 0 20px rgba(99, 102, 241, .15);
  --glow-accent-strong: 0 0 34px -6px rgba(99, 102, 241, .45);

  /* Три компонента, из-за которых тёмная тема выглядит «стеклянной»: белые
     полупрозрачные накладки (границы, подсветка строк, тень карточек) и
     цвет наведённой строки списка файлов. Всё, что раньше было записано
     как rgba(255, 255, 255, …), переведено на rgb(var(--tint) / …), поэтому
     светлая тема переключает его одним значением, а не правкой сотни строк. */
  --tint: 255 255 255;
  --row-hover: #171b25;

  /* Закреплённые панели и текст на карточках. Раньше эти места были записаны
     литералами под тёмный фон, и в светлой теме подписи не просто бледнели,
     а исчезали: #fff на белой карточке — это белый по белому. Токены нужны,
     чтобы ниже не пришлось повторять светлую тему по двести правил. */
  --panel:       rgba(9, 11, 18, .92);    /* панели с размытием фона */
  --panel-solid: rgba(14, 17, 26, .97);   /* выпадающие списки, тосты */
  --panel-shadow: 0 24px 50px -24px #000;
  --fg-card:     #c3cbd9;   /* вторичный текст на карточке */
  --fg-chip:     #b7c0d0;   /* подпись бейджа-параметра */
  --fg-muted:    #7c8698;   /* единица измерения рядом с суммой */
  --fg-accent:   #a5b4fc;   /* фиолетовый акцент на светлом фоне карточки */

  /* Текст поверх светлой тонировки: активная вкладка, наведённая кнопка,
     выбранный период. Такие подложки собраны из --tint и полупрозрачного
     бренда, поэтому в светлой теме они становятся светлыми — и зашитый
     здесь #fff превращал подпись в белый по белому (контраст 1.4:1).
     Насыщенные подложки — градиентные кнопки, аватар — этот токен
     не используют: там белым надёжно. */
  --fg-on-tint:  #fff;

  /* Подложка-врезка внутри карточки (полоса метрик сервера). Раньше там стояла
     утилита Tailwind bg-ink-900/60: её цвет зашит в конфиге и не зависит от
     темы, поэтому в светлой метрики оставались тёмной полосой. */
  --inset:       rgb(5 6 11 / .6);

  /* Фон страницы: фото стойки + затемняющий скрим поверх него */
  --bg-photo: image-set(
    url("/static/img/bg-servers-v2.webp") type("image/webp"),
    url("/static/img/bg-servers-v2.jpg") type("image/jpeg"));
  --bg-scrim:
    radial-gradient(120% 65% at 50% -18%, rgba(99, 102, 241, .16), transparent 62%),
    radial-gradient(70% 45% at 88% 8%, rgba(34, 211, 238, .08), transparent 60%),
    linear-gradient(180deg,
      rgba(6, 7, 12, .62) 0%,
      rgba(6, 7, 12, .80) 34%,
      rgba(5, 6, 10, .90) 100%);

  --radius: .875rem;
  --ease: cubic-bezier(.2, .9, .3, 1);

  /* ---------- Единый «стеклянный» блок ----------
     Раньше каждый блок красил подложку сам: .glass брал непрозрачный
     --bg-raised (#0b0d14), шапка и полоса метрик — свои градиенты. Из-за
     непрозрачного цвета backdrop-filter в .glass был бесполезен: размывать
     было нечего, и блоки выглядели сплошными плитами разной плотности.
     Здесь один набор токенов на все блоки страницы: полупрозрачная
     подложка, одна граница, одно размытие. Менять вид блоков — здесь,
     а не в каждом компоненте отдельно. */
  --block-bg:        rgb(14 17 25 / .52);
  --block-bg-hover:  rgb(14 17 25 / .62);
  --block-line:      rgb(var(--tint) / .10);
  --block-sheen:     inset 0 1px 0 rgb(var(--tint) / .06);
  --block-blur:      blur(14px) saturate(150%);
  --block-shadow:    0 18px 44px -30px rgb(0 0 0 / .85);
  /* Мелкие плашки внутри блока: та же подложка, но тоньше и без тени,
     иначе факт под кольцами читался бы как ещё один блок. */
  --pill-bg:         rgb(var(--tint) / .06);
  --pill-line:       rgb(var(--tint) / .09);
}

/* Светлая тема. Фотография светлая, поэтому скрим здесь не затемняет, а
   высветляет: белый полупрозрачный слой, иначе тёмный текст на белой стене
   стоек нечитаем. Акценты и тени оставлены как есть — они работают на обоих
   фонах. */
html[data-theme="light"] {
  color-scheme: light;

  --brand-400: #4f46e5;   /* 5.9:1 на белом — #818cf8 давал бы 2.5:1 */

  --bg:          #eef1f7;
  --bg-raised:   #f5f7fb;
  --bg-card:     #ffffff;

  --line:      rgba(15, 23, 42, .13);
  --line-soft: rgba(15, 23, 42, .07);
  --line-firm: rgba(15, 23, 42, .22);

  --ink:       #0f172a;
  --ink-dim:   #475569;
  /* 4.44:1 на фоне карточки (#f5f7fb) — ниже AA, хотя на чистом белом
     проходил. Затемнён до #5b6679: 5.41:1 на карточке и 5.80:1 на белом,
     то есть запас есть на обоих фонах, а не только на идеальном. */
  --ink-faint: #5b6679;

  --tint: 15 23 42;
  --row-hover: #eef1f7;

  /* Светлая тема: подложка блока — белый с альфой, а не серый. На
     светлой фотографии полупрозрачный тёмный слой превращал бы блок в
     грязное пятно; здесь блок «высветляет» фон, как и скрин под ним.
     Альфа чуть выше тёмной: на белом меньший контраст подложки. */
  --block-bg:       rgb(255 255 255 / .62);
  --block-bg-hover: rgb(255 255 255 / .74);
  --block-line:     rgb(15 23 42 / .10);
  --block-sheen:    inset 0 1px 0 rgb(255 255 255 / .70);
  --block-shadow:   0 18px 44px -32px rgb(15 23 42 / .38);
  --pill-bg:        rgb(15 23 42 / .045);
  --pill-line:      rgb(15 23 42 / .10);

  --panel:       rgba(255, 255, 255, .84);
  --panel-solid: rgba(255, 255, 255, .97);
  --panel-shadow: 0 24px 50px -28px rgb(15 23 42 / .45);
  --fg-card:     #334155;
  --fg-chip:     #475569;
  --fg-muted:    #5b6679;   /* см. --ink-faint: тот же приглушённый тон */
  --fg-accent:   #4338ca;

  /* --fg-on-tint: см. тёмную тему. Там это белый на тёмной подложке, здесь
     тёмный на светлой. Один токен вместо отдельного списка правил. */
  --fg-on-tint:  #0f172a;

  --inset:       rgb(15 23 42 / .05);

  --glow-accent: 0 0 20px rgba(99, 102, 241, .16);
  --glow-accent-strong: 0 0 34px -6px rgba(99, 102, 241, .38);

  --bg-photo: image-set(
    url("/static/img/bg-servers-light.webp") type("image/webp"),
    url("/static/img/bg-servers-light.jpg") type("image/jpeg"));
  /* Здесь --tint уже тёмный, поэтому скрим пишем белым литералом: он должен
     высветлять снимок, а не затемнять его (как в тёмной теме). Плотнее, чем
     было: исходный снимок светлеет к центру, и на .58 под заголовком
     оставались светлые пятна стоек — тёмный текст на них терялся. */
  --bg-scrim:
    radial-gradient(120% 65% at 50% -18%, rgba(99, 102, 241, .12), transparent 62%),
    radial-gradient(70% 45% at 88% 8%, rgba(34, 211, 238, .12), transparent 60%),
    linear-gradient(180deg,
      rgba(255, 255, 255, .74) 0%,
      rgba(248, 250, 255, .86) 34%,
      rgba(241, 245, 249, .94) 100%);
}

html {
/* 0.75 — исходный масштаб платформы. Пробовали 0.825 (+10%): на узких
     экранах места под контент становилось на десять процентов меньше, а
     медиазапросы при этом не меняются — их границы остаются на 1024px,
     и на телефоне интерфейс начинает лишний раз переноситься. */
  zoom: .75;
  scroll-behavior: smooth;
  color-scheme: dark;
}
html[data-theme="light"] { color-scheme: light; }

/* Иконка переключателя темы. В разметке лежат обе (солнце и луна), а какая
   видна — решает CSS: перерисовывать узлы из скрипта нельзя, lucide уже
   заменил <i> на <svg> и второй проход createIcons сломал бы разметку.
   Подпись иконки всегда говорит, куда нажатие переведёт: солнце — из тёмной
   темы в светлую, луна — из светлой в тёмную. */
/* Переключатель темы: солнце и луна не подменяются мгновенно, а
   перекрёстно затухают с поворотом — в обе стороны одинаково. Раньше здесь
   стояло display:none / block, и знак просто исчезал, оставляя на его месте
   второй: перемена читалась как мигание, а как превращение.

   Обе иконки живут в DOM всегда и занимают одно и то же место, поэтому
   соседние элементы не прыгают. Поворот идёт от ±90° к нулю и обратно — так
   же в обе стороны, из-за симметрии переход выглядит одинаково. */
[data-theme-icon] {
  transition: opacity .3s var(--ease), transform .45s var(--ease);
}
[data-theme-icon="sun"] { opacity: 1; transform: none; }
[data-theme-icon="moon"] { opacity: 0; transform: rotate(-90deg) scale(.4); }
html[data-theme="light"] [data-theme-icon="sun"] { opacity: 0; transform: rotate(90deg) scale(.4); }
html[data-theme="light"] [data-theme-icon="moon"] { opacity: 1; transform: none; }
/* Знаки переключателя темы лежат друг на друге в одной квадратной рамке.
   Раньше они подменялись через display, и в потоке лежал только один; с
   анимацией оба остаются в разметке всегда (иначе нечего анимировать), и
   без рамки они растянули бы строку вдвое. Рамка задаёт место, знаки внутри
   сложены абсолютно — так рядом ничего не прыгает.

   Общая обёртка для всех трёх переключателей: в шапке, в боковой панели и
   в подвале. У них разная вёрстка вокруг — flex-строка, квадратная кнопка,
   голый знак, — и общее только это место под иконку. */
.theme-icons {
  /* inline-block обязателен: элемент по умолчанию строчный, а строчные
     боксы ширину и высоту игнорируют. С getComputedStyle всё выглядело
     верно (18.4px), но бокс был 0x0 — и кнопка получалась невидимой. */
  display: inline-block;
  position: relative;
  width: 1.15rem; height: 1.15rem;
  flex: none;
  transition: transform .35s var(--ease);
}
.theme-icons [data-theme-icon] {
  position: absolute; inset: 0;
  display: grid; place-items: center;
}
.theme-icons svg { width: 100%; height: 100%; }

/* Плавающий блок гостя внизу слева: смена темы и вход.

   У вошедшего в этом месте низ панели — тема, профиль, выход. Гость панели
   не видит (на десктопе она скрыта), и переключить тему было нечем.

   Один фиксированный блок, а не две кнопки по отдельности: им нужно стоять в
   одном углу и уезжать вместе при сдвиге страницы. 4.5rem, а не притык к
   краю: у вошедшего кнопка стоит в подвале панели над блоком пользователя и
   выходом, то есть примерно на 80px выше края — замерено на 1280x700, его
   нижняя граница 620 при высоте 700.

   Знаки голые, без рамки и подложки, как и просил: только иконка, которая на
   наведении светлеет и слегка поворачивается.

   Сдвиг — трансформацией по той же величине, что и .page-shift, а не через
   left: иначе каждый кадр анимации пересчитывал бы раскладку ради двух
   значков.

   На телефоне не показывается: там всё это есть в шапке (тема) и в меню под
   бургером (вход), и второй раз то же самое на том же экране было бы дублем. */
.guest-dock {
  display: none;
  position: fixed; left: 1.25rem; bottom: 4.5rem;
  z-index: 40;
  flex-direction: column; align-items: center; gap: .6rem;
  transform: translateX(0);
  transition: transform var(--sb-dur-out, .24s) var(--sb-ease, ease);
}
.guest-dock__item {
  display: grid; place-items: center;
  width: 2rem; height: 2rem;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color .2s var(--ease);
}
.guest-dock__item:hover { color: var(--ink); }
.guest-dock__item:hover .theme-icons,
.guest-dock__item:hover svg { transform: rotate(18deg) scale(1.08); }
.guest-dock__item svg { width: 1.05rem; height: 1.05rem; transition: transform .35s var(--ease); }

@media (min-width: 1024px) {
  .guest-dock { display: flex; }
  /* Панели у гостя нет, но правило остаётся верным на любой странице с
     панелью: блок уезжает вместе со сдвигом содержимого, а не остаётся
     висеть под ней. */
  html[data-sb-open] .guest-dock {
    transform: translateX(var(--sb-push, 0px));
    transition-duration: var(--sb-dur-in, .38s);
  }
}

body {
  overflow-x: hidden;
  background: var(--bg);
  color: var(--ink);
  font-feature-settings: "cv02", "cv03", "cv04", "ss01";

  /* Фоновая стойка серверов. Наложена на цвет --bg и закреплена, чтобы при
     скролле фото не двигалось вместе с контентом. image-set() отдаёт браузерам
     с webp лёгкую версию (178 КБ вместо 1047 КБ), остальным — jpg.
     В имени файла стоит версия (v2): картинка подключена изнутри CSS, а не из
     шаблона, поэтому ?v= к ней не приходит и смена картинки иначе не доехала
     бы до браузера (старый кеш живёт год). См. _Static в main.py.
     Само фото и скрим берутся из --bg-photo / --bg-scrim: так светлая тема
     подставляет свою освещённую стойку и высветляющий скрим. */
  background-image: var(--bg-photo);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  background-attachment: fixed;
  transition: background-color .25s var(--ease), color .25s var(--ease);
}
/* Многослойная подложка поверх фото: фирменные свечения + скрим, который в
   тёмной теме затемняет снимок, а в светлой высветляет (--bg-scrim). */
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--bg-scrim);
}
main, header, footer, section { max-width: 100%; }
img, svg, video, canvas { max-width: 100%; }

::selection { background: rgb(var(--brand) / .35); }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgb(var(--tint) / .12);
  border-radius: 999px; border: 3px solid transparent; background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: rgb(var(--tint) / .24); background-clip: content-box; }

/* ==========================================================================
   2. ТИПОГРАФИКА
   ========================================================================== */
.eyebrow {
  display: block; font-size: .7rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-faint);
}

/* Моноширинный для IP:PORT, путей, переменных .env и значений конфигурации.
   letter-spacing и чуть увеличенный x-height отделяют код от обычного текста. */
.mono, code, kbd, .font-mono {
  font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, Consolas, monospace;
  font-variant-ligatures: none;
  letter-spacing: -.01em;
}
code {
  padding: .12rem .35rem; border-radius: .3rem;
  background: rgb(var(--tint) / .05); border: 1px solid var(--line-soft);
}

/* ==========================================================================
   3. ПОВЕРХНОСТИ И КАРТОЧКИ
   ========================================================================== */
.glass {
  position: relative;
  /* Подложка блока — общий токен, а не свой непрозрачный --bg-raised:
     ради этого здесь и появился backdrop-filter. */
  background: var(--block-bg);
  border: 1px solid var(--block-line);
  border-radius: var(--radius);
  backdrop-filter: var(--block-blur);
  -webkit-backdrop-filter: var(--block-blur);
  box-shadow: var(--block-shadow), var(--block-sheen);
}
/* Вложенный слой: «стеклянная» полка внутри карточки */
.surface-2 {
  background: rgb(var(--tint) / .028);
  border: 1px solid var(--line-soft);
  border-radius: .625rem;
}
.glass-hover { transition: border-color .22s var(--ease), box-shadow .22s var(--ease), transform .22s var(--ease); }
.glass-hover:hover {
  border-color: var(--line-firm);
  background-color: var(--bg-card);
  box-shadow: var(--glow-accent-strong), inset 0 1px 0 rgb(var(--tint) / .07);
  transform: translateY(-1px);
}

/* ---------------------------------------------------------------- карточки игр
   Слои снизу вверх: арт сцены → затемняющая маска → содержимое.
   Арт всегда под контентом и уходит в темноту книзу, поэтому текст
   поверх него остаётся контрастным.

   Раньше каждая карточка красилась в цвет своей игры: полоска сверху,
   рамка на `--popular` и ореол на наведении — оранжевая, зелёная,
   синяя. Три одинаковые по смыслу карточки выглядели тремя разными
   компонентами, и взгляд цеплялся за цвет рамки вместо текста. Теперь
   все три — одно семейство: нейтральная рамка и фирменный фиолетовый
   акцент на наведении. Цвет игры остался только на знаке игры. */
.game-card {
  display: flex; flex-direction: column;
  min-height: 100%; padding: 1.15rem 1.25rem 1.25rem;
  overflow: hidden; isolation: isolate;
}

.game-card__art {
  position: absolute; inset: 0 0 auto; height: 9rem; z-index: 0;
  color: var(--gh);
  opacity: .22;
  transition: opacity .35s var(--ease), transform .45s var(--ease);
  pointer-events: none;
}
.game-art { display: block; width: 100%; height: 100%; }
/* Маска: верх арта виден, низ растворяется в поверхности карточки */
.game-card__art::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg,
              transparent 0%,
              rgba(11, 13, 20, .62) 48%,
              var(--bg-raised) 94%);
}
.game-card:hover .game-card__art { opacity: .38; transform: scale(1.04); }

/* Наведение — фирменное, как у карточек в шапке: одна страница не должна
   показывать два разных языка выделения. */
.game-card:hover {
  border-color: rgb(var(--brand) / .5);
  box-shadow: 0 14px 34px -18px rgb(var(--brand) / .7),
              inset 0 1px 0 rgb(var(--tint) / .08);
}
/* Карточка сборки больше ничего не содержит: ни ссылок, ни кнопок.
   Поэтому здесь нет ни :focus-within, ни :focus-visible — фокусировать
   нечего, и в порядок обхода она не попадает. Наведение остаётся. */

/* Содержимое над артом */
.game-card__body { position: relative; z-index: 2; display: flex; flex-direction: column; flex: 1; }

/* Знак игры — официальный файл (static/img/games), плитка обрезает его по
   своему скруглению: object-fit: cover, чтобы знак был без полей, а
   overflow: hidden — чтобы скругление плитки шло по контуру картинки. */
.game-mark {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.game-mark-tile {
  display: inline-grid; place-items: center; color: #fff; overflow: hidden;
  background: linear-gradient(140deg, var(--g1), var(--g2));
  vertical-align: -3px;
}

/* Самодостаточный знак игры (GTA SAMP и подобные).
   У samp.png своя тёмная подложка, скруглённый контур, портрет и надпись
   «San Andreas», и он не квадратный (123x128). Обычная плитка превращала
   его в квадрат внутри квадрата, а object-fit: cover срезал подпись. Такой
   знак показывается целиком: плитка снимается, размер задаёт место через
   --mark-h, ширина идёт по пропорциям.

   !important не от лени: в четырёх местах градиент плитки задан
   инлайн-стилем, и обычный класс его не перебьёт — исключение молча
   не сработало бы именно там, где оно нужно.

   Класс ставит шаблон по games.py::GameSpec.logo_tile, а не по имени
   игры: правило живёт в данных, новый самодостаточный знак не потребует
   правок CSS. */
.game-mark-raw {
  background: none !important;
  box-shadow: none !important;
  border-radius: 0 !important;
  overflow: visible !important;
  width: auto !important;
  height: auto !important;
  display: inline-flex; align-items: center; flex: none;
}
/* Высота задаётся картинке напрямую, а не как 100% от контейнера: у
   контейнера height:auto, и процент от него не разрешается — картинка
   откатывалась в натуральный размер 123x128 и вылезала за карточку.

   max-width здесь — не перестраховка, а вторая половина того же правила.
   Высота задаёт размер по вертикали, а ширина у знака идёт от пропорций
   файла, поэтому широкая надпись (ARK, Terraria, а у DayZ и Zomboid ещё и
   маска) оказывалась шире самой плитки и наезжала на название игры.
   --mark-h у всех обёрток game-mark-raw равен стороне плитки, поэтому
   ограничение по нему — это ровно «знак помещается в плитку». */
.game-mark-raw .game-mark {
  width: auto; height: var(--mark-h);
  max-width: var(--mark-h);
  object-fit: contain; object-position: center;
}
.game-card__icon {
  display: grid; place-items: center; width: 2.5rem; height: 2.5rem;
  border-radius: .625rem; overflow: hidden; color: #fff;
  background: linear-gradient(140deg, var(--g1), var(--g2));
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--g1) 90%, transparent),
              inset 0 0 0 1px rgb(var(--tint) / .08);
}

/* Чек-лист преимуществ.
   Галочки переведены с зелёных на фиолетовые: рядом с фирменной рамкой
   и кнопками зелёный был вторым, не относящимся к бренду цветом. Плюс
   чуть плотнее по вертикали — блок стал ниже, три карточки помещаются в
   один экран и перестают дублировать то, что уже показала шапка. */
/* Колонка текста ограничена: без неё длинный пункт («Мгновенная установка
   карт из Workshop») занимал всю ширину карточки и уходил под фотографию
   — на 96% ширины фон был (146,140,143), и строка держала 3.1 вместо 4.5.
   Подбирал перебором: 100% — 3.14, 86% — 4.88, 80% — 7.77. Взял 86%:
   это первый шаг, где контраст проходит, и на нём карточка ещё не
   растёт (238px — столько же, сколько при свободной ширине; перенос
   начинается от 72%). */
.game-card__list { display: grid; gap: .45rem; margin-top: .95rem; max-width: 86%; }
.game-card__list li {
  display: grid; grid-template-columns: 1.1rem 1fr; align-items: start; gap: .55rem;
  font-size: .8rem; line-height: 1.45; color: var(--fg-card);
}
.game-card__check {
  display: grid; place-items: center; width: 1.1rem; height: 1.1rem; margin-top: .12rem;
  border-radius: 999px; color: #fff;
  background: linear-gradient(140deg, #6366f1, #4f46e5);
  box-shadow: 0 4px 12px -6px rgba(99, 102, 241, .9);
}

/* Плашка «Популярное» с градиентной обводкой */
/* «Популярное» — единственное место в карточке, где допустим цвет.
   Обводка вокруг всей карточки убрана: из-за неё Garry's Mod выглядел
   оранжевым, а соседние — зелёным и синим, и три одинаковых блока
   читались как три разных компонента. Акцент остался на самой плашке. */
.game-card__badge {
  position: absolute; top: .85rem; right: .85rem; z-index: 4;
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .22rem .55rem; border-radius: 999px;
  font-size: .68rem; font-weight: 700; letter-spacing: .02em;
  color: #fff;
  background:
    linear-gradient(180deg, rgb(255 255 255 / .14), transparent),
    linear-gradient(120deg,
      color-mix(in srgb, var(--g1) 72%, #111827),
      color-mix(in srgb, var(--g2) 72%, #111827));
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--g1) 55%, transparent),
    0 6px 16px -6px color-mix(in srgb, var(--g1) 95%, transparent);
}

/* Карточка-сводка метрик хоста */
.stat {
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, rgb(var(--tint) / .035), rgb(var(--tint) / .012));
  background-color: var(--bg-raised);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: .7rem .9rem;
  transition: border-color .2s var(--ease);
}
.stat:hover { border-color: var(--line-firm); }

/* Сводка хоста одним блоком. Пять отдельных плиток в ряд читались как
   пять несвязанных карточек, хотя это метрики одного узла, поэтому здесь
   одна рамка на всю полосу, а ячейки разделяет тонкая линия: собственные
   рамки ячеек вернули бы ровно то, что было до этого. */
.stat-band {
  display: grid;
  grid-template-columns: 1fr;
  overflow: hidden;
  background: linear-gradient(180deg, rgb(var(--tint) / .035), rgb(var(--tint) / .012));
  background-color: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.stat-band > .stat {
  background: none;
  border: 0;
  border-radius: 0;
  padding: .8rem 1rem;
}
/* Наведение у ячейки не должно перекрашивать разделитель и рамку блока —
   подсветка идёт мягкой подложкой, иначе полоса «рассыпается» обратно. */
.stat-band > .stat:hover {
  background: rgb(var(--tint) / .02);
  border-color: var(--line);
}
/* Одна колонка (телефон): разделяем строки. */
.stat-band > .stat + .stat { border-top: 1px solid var(--line); }
@media (min-width: 640px) {
  .stat-band { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* В две колонки вертикальный разделитель нужен только правым ячейкам. */
  .stat-band > .stat:nth-child(even) { border-left: 1px solid var(--line); }
}
@media (min-width: 1024px) {
  .stat-band { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  /* В одну строку горизонтальные разделители не нужны: блок снова стал бы
     полосой из пяти отдельных плиток. */
  .stat-band > .stat + .stat { border-top: 0; }
  .stat-band > .stat:not(:first-child) { border-left: 1px solid var(--line); }
}
.stat__label {
  display: flex; align-items: center; gap: .45rem;
  font-size: .68rem; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-faint);
}
.stat__value {
  margin-top: .3rem; font-size: 1.15rem; font-weight: 700; line-height: 1.15;
  color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: -.015em;
}

/* Полоса метрик: шесть значений в одном блоке. Раньше это были шесть
   отдельных плиток, и каждый ряд читался как свой набор карточек; теперь
   рамка и фон общие, блок один, а значения разделяют волосяные черты. */
.stat-strip {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  border: 1px solid var(--line); border-radius: var(--radius);
  background: linear-gradient(180deg, rgb(var(--tint) / .035), rgb(var(--tint) / .012));
  background-color: var(--bg-raised);
  overflow: hidden;
}
.stat-strip__item {
  padding: .75rem .95rem; min-width: 0;
  transition: background .2s var(--ease);
}
.stat-strip__item:hover { background: rgb(var(--tint) / .03); }

/* Черты между ячейками повторяют сетку: пока метрики стоят в одну строку,
   это вертикальные разделители, а когда колонки переносятся — горизонтальные
   между рядами. Одновременно их не бывает: вертикальная черта у края
   перенесённого ряда читалась бы как обрыв блока. */
.stat-strip__item:nth-child(n + 3) { border-top: 1px solid var(--line-soft); }
@media (min-width: 640px) {
  .stat-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .stat-strip__item:nth-child(n + 3) { border-top: 0; }
  .stat-strip__item:nth-child(n + 4) { border-top: 1px solid var(--line-soft); }
}
@media (min-width: 1024px) {
  .stat-strip { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .stat-strip__item:nth-child(n + 4) { border-top: 0; }
  .stat-strip__item + .stat-strip__item { border-left: 1px solid var(--line-soft); }
}

/* Хост панели в админке: один блок значений с полосами заполнения.
   Раньше каждое значение стояло в своей рамке — на восьми ячейках это
   восемь рамок, и половина экрана уходила на пустоты вокруг коротких
   чисел. Теперь ячейки делит волосяная черта, а «наглядность» дают
   полосы: одно число вроде «57.2 ГБ» не говорит, много это или нет. */
.host-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  border: 1px solid var(--line-soft); border-radius: var(--radius);
  background: rgb(var(--tint) / .022);
  overflow: hidden;
}
.host-cell {
  display: flex; flex-direction: column; gap: .2rem;
  padding: .6rem .8rem; min-width: 0;
}
.host-cell:nth-child(2n) { border-left: 1px solid var(--line-soft); }
.host-cell:nth-child(n + 3) { border-top: 1px solid var(--line-soft); }
.host-cell__head {
  display: flex; align-items: center; gap: .35rem;
  font-size: .62rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-faint);
}
.host-cell__head svg { width: .75rem; height: .75rem; flex: none; }
.host-cell__value {
  display: flex; align-items: center; gap: .4rem;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .95rem; font-weight: 700; line-height: 1.25; color: var(--ink);
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.host-cell__note { font-size: .62rem; color: var(--ink-faint); }

@media (min-width: 900px) {
  .host-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* Три колонки — чередование снова другое: вторая ячейка больше не
     последняя в ряду, а каждая третья начинает следующий ряд. */
  .host-cell:nth-child(2n) { border-left: 0; }
  .host-cell:nth-child(n + 2) { border-left: 1px solid var(--line-soft); }
  .host-cell:nth-child(3n + 1) { border-left: 0; }
  .host-cell:nth-child(n + 4) { border-top: 1px solid var(--line-soft); }
}

/* Акцентные метрики админки.
   Верхний ряд — то, куда смотрят в первую очередь: сколько онлайн, сколько
   заработали, сколько ошибок. Пять одинаковых серых плиток не выделяли ни
   одну из них, поэтому важные получают цвет, а остальные остаются нейтральными.
   Цветом помечены и значок, и число. Рамки у ячеек нет — блок общий, и
   своя рамка у метрики снова разбила бы его на плитки. */
.stat--ok .stat__value   { color: #34d399; }
.stat--bad .stat__value  { color: #fb7185; }
.stat--brand .stat__value { color: var(--brand-400); }

html[data-theme="light"] .stat--ok .stat__value  { color: #047857; }
html[data-theme="light"] .stat--bad .stat__value { color: #be123c; }

/* Значение, раскрытое из маски: подсвечено, чтобы его не спутали с
   обычной ячейкой и чтобы было видно, что это разовый показ. */
.stat__value.is-revealed { color: var(--fg-accent); }

/* ------------------------------------------------------------ админка: фильтры */
.admin-filter {
  display: inline-flex; align-items: center; gap: .15rem; padding: .2rem;
  border-radius: .55rem; border: 1px solid var(--line);
  background: rgb(var(--tint) / .03);
}
.admin-filter__btn {
  padding: .3rem .65rem; border: 0; border-radius: .4rem; cursor: pointer;
  background: transparent; color: var(--ink-dim);
  font-size: .75rem; font-weight: 500; line-height: 1.2;
  transition: color .16s var(--ease), background .16s var(--ease);
}
.admin-filter__btn:hover { color: var(--ink); background: rgb(var(--tint) / .07); }
.admin-filter__btn.is-active {
  color: var(--fg-on-tint); background: rgb(var(--brand) / .28);
  box-shadow: inset 0 0 0 1px rgb(var(--brand) / .45);
}

.admin-search {
  display: flex; align-items: center; gap: .5rem;
  padding: .45rem .7rem; border-radius: .55rem;
  border: 1px solid var(--line); background: rgb(var(--tint) / .03);
  color: var(--ink-faint); min-width: 15rem;
  transition: border-color .16s var(--ease), background .16s var(--ease);
}
.admin-search:focus-within { border-color: rgb(var(--brand) / .55); background: rgb(var(--tint) / .06); }
.admin-search input {
  flex: 1; min-width: 0; border: 0; background: transparent; outline: none;
  color: var(--ink); font-size: .8rem;
}
.admin-search input::placeholder { color: var(--ink-faint); }
.admin-search input::-webkit-search-cancel-button { filter: grayscale(1) opacity(.6); }

/* Строка, которую сервер отклонил (последний администратор, себя): короткая
   подсветка вместо смены текста — состояние остаётся правдивым. */
.flash-warn { animation: adminWarn 1.2s var(--ease); }
@keyframes adminWarn {
  0%, 100% { background: transparent; }
  30% { background: rgb(239 68 68 / .18); }
}

/* =============================================================================
   ПУСТОЙ КАБИНЕТ
   Пять карточек с нулями новому пользователю ничего не говорят, поэтому
   при 0 серверах показываем одну строку статуса, а место отдаём онбордингу.
   ========================================================================== */
.stat-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .55rem 1rem;
  padding: .8rem 1.15rem;
  /* Полоса внутри карточки пустого состояния: подложка — общая
     полупрозрачная, чтобы блоки страницы читались одним стилем. */
  background: var(--block-bg);
  border: 1px solid var(--block-line); border-radius: var(--radius);
  backdrop-filter: var(--block-blur);
  -webkit-backdrop-filter: var(--block-blur);
  font-size: .82rem; color: var(--ink-dim);
}
.stat-bar__item { display: inline-flex; align-items: center; gap: .4rem; }
.stat-bar__icon { width: .9rem; height: .9rem; color: var(--ink-faint); flex: none; }
.stat-bar__value { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
/* Хватит на N суток: подпись под балансом в шапке кабинета. Мелкая и
   приглушённая, чтобы не спорить с самим балансом, но с цветом — если
   денег меньше суток, это читается без чтения цифр. */
.stat-bar__note {
  display: block; margin-top: .1rem; font-size: .68rem; line-height: 1.35;
  color: var(--ink-dim); font-variant-numeric: tabular-nums;
}
.stat-bar__note.is-short { color: #fca5a5; }
/* Полоса сводки внутри карточки пустого состояния: своих рамки и фона у неё
   быть не должно — карточка уже нарисована вокруг, а вторая рамка внутри
   первой читалась как вложенная плитка. Остаётся разделительная линия снизу,
   которая и делит блок на сводку и приглашение. */
.stat-bar--inset {
  border: 0; border-bottom: 1px solid var(--line); border-radius: 0;
  background: none; padding: 1rem 1.5rem;
  /* Сводка прижата вправо: это деньги пользователя, а не заголовок
     раздела, и в пустом кабинете левее неё ничего нет — строка висела
     в левом углу карточки, откуда взгляд с неё и начинался, хотя
     смотреть нужно на баланс. */
  justify-content: flex-end;
}
/* Полоса, где слева приветствие, а справа баланс: две подписи одной
   строки разводятся по краям, и прижатие вправо больше не годится —
   приветствие оказалось бы вплотную к деньгам. */
.stat-bar--split { justify-content: space-between; }
/* На узком экране подписи перестают помещаться в одну строку, и разводка
   по краям превращается в рваные края: там снова влево, одна под первой. */
@media (max-width: 520px) {
  .stat-bar--inset,
  .stat-bar--split { justify-content: flex-start; }
}

/* Шапка пустого состояния */
.onboard-hero {
  padding: 3rem 1.5rem 2.5rem; text-align: center;
  background:
    radial-gradient(ellipse 60% 100% at 50% 0%, rgba(99, 102, 241, .1), transparent 70%);
}

/* Иконка: серверная стойка вместо абстрактного «сервер выключен» +
   медленное парение, чтобы блок не выглядел мёртвым.
   Знак берёт цвет из #c7d2fe — на тёмной подложке это светлая черничная,
   а на белом 1.3:1 и он просто исчезал; в светлой теме цвет переопределён
   в блоке темы (html[data-theme="light"]). */
.onboard-hero__icon {
  display: grid; place-items: center;
  width: 4rem; height: 4rem; margin: 0 auto;
  border-radius: 1.1rem;
  color: #c7d2fe;
  background: linear-gradient(140deg, rgba(99, 102, 241, .22), rgba(139, 92, 246, .12));
  border: 1px solid rgba(129, 140, 248, .3);
  box-shadow: 0 0 0 1px rgb(var(--tint) / .03), 0 12px 32px -14px rgba(99, 102, 241, .6);
  animation: float 5.5s var(--ease) infinite;
}
@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

/* Уважаем системную настройку «уменьшить движение» */
@media (prefers-reduced-motion: reduce) {
  .onboard-hero__icon { animation: none; }
}

/* Подпись над блоками онбординга */
.onboard-sub {
  font-size: .7rem; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint);
}

/* Мягкое фиолетовое свечение за главной кнопкой */
.onboard-cta { position: relative; display: inline-block; margin-top: 1.75rem; }
.onboard-cta::before {
  content: ""; position: absolute; inset: -14px -22px;
  z-index: -1; border-radius: 999px;
  background: radial-gradient(ellipse at center, rgba(99, 102, 241, .5), transparent 72%);
  filter: blur(14px);
  pointer-events: none;
  animation: cta-breathe 3.4s var(--ease) infinite;
}
@keyframes cta-breathe {
  0%, 100% { opacity: .65; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.05); }
}
@media (prefers-reduced-motion: reduce) {
  .onboard-cta::before { animation: none; }
}

/* ---- карточка быстрого пресета -------------------------------------------- */
.starter-card {
  display: flex; align-items: center; gap: .85rem;
  padding: .9rem 1rem; border-radius: .9rem;
  text-decoration: none; color: inherit;
  background: rgb(var(--tint) / .025);
  border: 1px solid var(--line);
  transition: background .2s var(--ease), border-color .2s var(--ease),
              transform .2s var(--ease), box-shadow .2s var(--ease);
}
.starter-card:hover {
  transform: translateY(-2px);
  background: rgb(var(--tint) / .05);
  border-color: rgb(var(--tint) / .18);
  box-shadow: 0 12px 28px -18px rgba(0, 0, 0, .9);
}
.starter-card__logo { flex: none; }
.starter-card__logo > span {
  display: grid; place-items: center;
  width: 2.4rem; height: 2.4rem; border-radius: .65rem; overflow: hidden;
}
.starter-card__body { display: flex; flex-direction: column; gap: .12rem; min-width: 0; flex: 1; }
.starter-card__title {
  font-size: .86rem; font-weight: 700; color: var(--ink); line-height: 1.2;
}
/* Подпись конфигурации («DarkRP», «The Island на 30 игроков») — фиолетовым
   фирменным цветом, а не цветом игры. Раньше здесь стоял var(--g1), и двенадцать
   карточек подряд пестрели оранжевым, зелёным, бирюзовым и жёлтым: блок
   переставал читаться как один список, хотя это один список. Тот же довод,
   по которому полоса слева и плашка игры у карточки сервера фирменные: цвет
   игры опознаёт знак, а не подпись. Токен --fg-accent сам переключается под
   тему (светло-лавандовый в тёмной, индиго в светлой). */
.starter-card__mode {
  font-size: .72rem; font-weight: 600;
  color: var(--fg-accent); line-height: 1.2;
}
.starter-card__specs {
  display: flex; flex-wrap: wrap; gap: .55rem; margin-top: .3rem;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .66rem; color: var(--ink-faint);
}
.starter-card__specs > span { display: inline-flex; align-items: center; gap: .22rem; }
.starter-card__specs svg { opacity: .8; }
.starter-card__price { flex: none; text-align: right; line-height: 1.15; }
.starter-card__price b {
  display: block; font-size: .92rem; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.starter-card__price small { font-size: .64rem; color: var(--ink-faint); }

/* ---- карточка справки ------------------------------------------------------ */
.help-card {
  display: flex; align-items: flex-start; gap: .75rem;
  padding: .85rem 1rem; border-radius: .8rem;
  text-decoration: none; color: inherit;
  background: rgb(var(--tint) / .02);
  border: 1px solid var(--line);
  transition: background .18s var(--ease), border-color .18s var(--ease),
              transform .18s var(--ease);
}
.help-card:hover {
  transform: translateY(-1px);
  background: rgb(var(--tint) / .045);
  border-color: rgb(var(--tint) / .16);
}
.help-card__icon {
  display: grid; place-items: center; flex: none;
  width: 2rem; height: 2rem; border-radius: .55rem;
  color: #c4b5fd; background: rgba(139, 92, 246, .14); border-color: rgba(139, 92, 246, .3);
}
.help-card__body { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.help-card__title {
  font-size: .8rem; font-weight: 600; color: var(--ink); line-height: 1.25;
}
.help-card__text {
  font-size: .7rem; color: var(--ink-faint); line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.help-card__chev {
  width: .9rem; height: .9rem; flex: none; margin-left: auto;
  color: var(--ink-faint); opacity: .5; align-self: center;
  transition: transform .18s var(--ease), opacity .18s var(--ease);
}
.help-card:hover .help-card__chev { opacity: 1; transform: translateX(2px); }

/* Иконка заголовка справки */
.guide-hub__icon {
  display: grid; place-items: center;
  width: 3rem; height: 3rem; border-radius: .85rem;
  color: #c4b5fd; background: rgba(139, 92, 246, .14); border-color: rgba(139, 92, 246, .3);
}

/* Блоки команд и конфигов в инструкциях */
.guide-code {
  overflow-x: auto; margin: 0; padding: .7rem .9rem;
  background: rgba(0, 0, 0, .32);
  border: 1px solid var(--line); border-radius: .6rem;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .74rem; line-height: 1.6; color: #a5f3fc;
  white-space: pre;
}

/* ---------------------------------------------------------------- BENTO GRID
   Асимметричная сетка: две крупные карточки на две колонки, остальные — на одну.
   На узких экранах всё складывается в одну колонку, порядок — по важности. */
.bento {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-auto-flow: dense;
}

/* Секция «Всё, что нужно для игры» не делает ничего: внутри нет ни одной
   ссылки, кнопки или обработчика — только текст, списки и иконки. Выделять
   её мышью незачем, а вот случайная подсветка через полэкрана мешает
   читать соседние карточки.

   pointer-events: none здесь безопасен именно потому, что интерактивного
   внутри нет: у карточек игр в карусели он бы сломал переход к расчёту,
   а тут — просто гасит наведение.

   Побочный эффект, о котором стоит знать: вместе с наведением умирают три
   эффекта — подъём карточки на 1px (.glass-hover:hover), фиолетовый цвет
   значков и подсветка тегов.

   user-select здесь раньше стоял рядом с pointer-events, теперь не нужен:
   выделение гасится общим правилом в конце файла. */
#features { pointer-events: none; }
/* Пятно за секцией шире экрана, поэтому секция его обрезает. clip, а не
   hidden: он не создаёт прокручиваемый контейнер и не меняет ничего, кроме
   видимости краёв. */
#features { overflow: clip; }
.bento__card {
  position: relative; isolation: isolate;
  grid-column: span 2;                  /* обычная карточка — 2 из 6 колонок */
  display: flex; flex-direction: column; gap: 1rem;
  padding: 1.5rem; border-radius: var(--radius);
}
.bento__card--wide { grid-column: span 3; }   /* крупная карточка — 3 из 6 */
/* Замыкающая карточка во всю ширину: иначе в сетке из 6 колонок
   после трёх карточек по 2 остаётся пустой ряд с четырьмя дырами. */
.bento__card--full { grid-column: span 6; }
/* В широкой карточке текст идёт слева, индикатор — справа, по центру строки */
.bento__card--full { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 1.25rem; }
.bento__card--full .bento__head { flex: 1 1 20rem; }
.bento__card--full .live { margin-top: 0; flex: 0 0 auto; }
.bento__card--full .feat-item__tags { flex: 1 1 100%; }
@media (max-width: 767px) {
  .bento__card--full { flex-direction: column; align-items: stretch; }
  .bento__card--full .live { align-self: flex-start; }
}

/* На планшете — по две карточки в ряд */
@media (max-width: 1023px) {
  .bento__card, .bento__card--wide, .bento__card--full { grid-column: span 3; }
}
@media (max-width: 639px) {
  /* minmax(0, 1fr), а не 1fr. У 1fr есть автоматический минимум — он равен
     min-content содержимого, поэтому одна широкая карточка растягивала
     колонку за пределы экрана: на 412px сетка считалась 517px шириной при
     контейнере 388px, и страница получалась шире вьюпорта. На десктопе
     этого не видно, там колонок шесть и каждая узкая. */
  .bento { grid-template-columns: minmax(0, 1fr); }
  .bento__card, .bento__card--wide, .bento__card--full { grid-column: span 1; }
  .bento__card { padding: 1.25rem; }
}
/* Тот же минимум, но через min-width: карточки — элементы сетки, и без
   нулевого минимума они не дают колонке сжиматься. */
.bento > * { min-width: 0; }

/* Метки-теги внизу карточки: короткие факты, не лозунги.
   auto-отступ есть только у мини-элемента, поэтому теги прижаты к низу
   вместе с ним, а не растягивают карточку второй пустой строкой. */
.feat-item__tags {
  display: flex; flex-wrap: wrap; gap: .4rem;
}
.feat-item__tags span {
  font-size: .7rem; font-weight: 500; color: #a9b3c6;
  padding: .18rem .5rem; border: 1px solid var(--line-soft); border-radius: .375rem;
  background: rgb(var(--tint) / .03);
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.bento__card:hover .feat-item__tags span {
  border-color: rgba(129, 140, 248, .3); color: #c7d2fe;
}

/* Шапка карточки: плашка иконки + заголовок с описанием */
.bento__head { display: grid; grid-template-columns: auto 1fr; gap: .9rem; align-items: start; }
.bento__head h3 {
  font-size: 1rem; font-weight: 600; color: var(--ink); letter-spacing: -.01em;
}
.bento__head p {
  margin-top: .3rem; font-size: .85rem; line-height: 1.6; color: var(--ink-dim);
}

/* Плашка иконки: мягкая фиолетово-синяя подложка со свечением */
.feat-icon {
  display: grid; place-items: center; width: 2.5rem; height: 2.5rem; flex: none;
  border-radius: .7rem; color: var(--fg-accent);
  background: linear-gradient(140deg, rgba(99, 102, 241, .3), rgba(139, 92, 246, .18));
  box-shadow:
    inset 0 0 0 1px rgba(129, 140, 248, .22),
    0 6px 18px -10px rgba(99, 102, 241, .9);
  transition: box-shadow .25s var(--ease), color .25s var(--ease);
}
.bento__card:hover .feat-icon {
  color: #fff;
  box-shadow:
    inset 0 0 0 1px rgba(129, 140, 248, .45),
    0 8px 22px -8px rgba(99, 102, 241, 1);
}

/* Цветное пятно за секцией — даёт глубину, не мешая карточкам */
.feat-glow {
  /* 46rem шириной плюс центрирование translate(-50%) — пятно всегда шире
     экрана и вылезает за края на 60-100px. Раньше его обрезал только
     body { overflow-x: hidden }, но тот переносится на вьюпорт и держит
     лишь прокрутку: документ оставался шире экрана, из-за чего браузер на
     телефоне мог ужать страницу и оставить поле сбоку. Обрезка — на секции,
     правило рядом. */
  position: absolute; inset: -6rem 50% auto; translate: -50% 0;
  width: 46rem; height: 26rem; z-index: -1; pointer-events: none;
  border-radius: 50%; filter: blur(110px);
  background: radial-gradient(circle, rgba(99, 102, 241, .2), rgba(139, 92, 246, .08) 48%, transparent 72%);
}

/* ------------------------------------------- мини-элементы внутри карточек
   Каждый блок показывает конкретные данные, а не украшает карточку. */

/* Пинг по локациям: точка + город + замер */
.ping-list {
  display: grid; gap: .35rem; margin-top: auto;
  padding: .7rem .8rem; border-radius: .6rem;
  background: rgb(var(--tint) / .03); border: 1px solid var(--line-soft);
}
.ping-row {
  display: grid; grid-template-columns: .5rem 1fr auto; align-items: center; gap: .6rem;
  font-size: .8rem;
}
.ping-city { color: var(--fg-card); }
.ping-ms {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .76rem; font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* Три оттенка зелёного: чем лучше пинг, тем насыщеннее точка */
.ping-dot { width: .45rem; height: .45rem; border-radius: 999px; flex: none; }
.ping-dot--best { background: #34d399; box-shadow: 0 0 8px rgba(52, 211, 153, .9); }
.ping-dot--good { background: #4ade80; box-shadow: 0 0 6px rgba(74, 222, 128, .7); }
.ping-dot--ok   { background: #a3e635; box-shadow: 0 0 6px rgba(163, 230, 53, .6); }

/* Мини-терминал: превью того, что видит пользователь в панели */
.cli {
  margin-top: auto; overflow: hidden; border-radius: .6rem;
  background: radial-gradient(120% 100% at 50% 0%, rgba(15, 23, 42, .9), rgba(2, 6, 14, .98));
  border: 1px solid var(--line);
  box-shadow: inset 0 12px 30px -20px #000;
}
.cli__bar {
  display: flex; align-items: center; gap: .3rem;
  padding: .45rem .6rem; border-bottom: 1px solid var(--line-soft);
  background: rgb(var(--tint) / .03);
}
.cli__dot { width: .45rem; height: .45rem; border-radius: 999px; background: #2b3140; }
/* Заголовок окна терминала: на 3.97:1 не проходит AA, поэтому светлее */
.cli__title {
  margin-left: .4rem; font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .66rem; color: #7e899c;
}
.cli__body {
  padding: .65rem .75rem; display: grid; gap: .22rem;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .7rem; line-height: 1.5; color: #94a3b8;
}
.cli__body p { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cli__input { color: #22d3ee; }
.cli__caret {
  display: inline-block; width: .4rem; height: .8rem; margin-left: .15rem;
  background: #22d3ee; vertical-align: text-bottom;
  animation: caret 1.1s steps(2, start) infinite;
}
@keyframes caret { 50% { opacity: 0; } }

/* Светлая тема: мини-терминал повторяет приём настоящей консоли выше —
   белый фон, тот же набор цветов строк и никакой чёрной вмятины от
   внутренней тени. Иначе на белой карточке это был единственный тёмный
   прямоугольник на всей странице. */
html[data-theme="light"] .cli {
  background:
    linear-gradient(180deg, rgba(15, 23, 42, .03), rgba(15, 23, 42, 0)),
    var(--bg-raised);
  border-color: var(--line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8);
}
html[data-theme="light"] .cli__bar { background: rgb(15 23 42 / .03); border-bottom-color: var(--line-soft); }
html[data-theme="light"] .cli__dot { background: #cbd5e1; }
html[data-theme="light"] .cli__title { color: #5b6679; }
html[data-theme="light"] .cli__body { color: var(--ink-dim); }
html[data-theme="light"] .cli__input { color: #0e7490; }
html[data-theme="light"] .cli__caret { background: #0e7490; }

/* Индикатор «инженеры на связи»: медленная пульсация, не мигает.
   Позиционирование relative нужно самой точке — кольцо расходится от неё. */
.live {
  position: relative;
  display: flex; align-items: center; gap: .55rem; margin-top: auto;
  padding: .55rem .75rem; border-radius: .6rem;
  background: rgba(16, 185, 129, .08); border: 1px solid rgba(52, 211, 153, .25);
}
.live__dot {
  position: relative;
  width: .5rem; height: .5rem; border-radius: 999px; flex: none;
  background: #34d399; box-shadow: 0 0 10px rgba(52, 211, 153, .9);
}
.live__dot::after {
  content: ""; position: absolute; inset: 0; border-radius: 999px;
  background: #34d399; animation: livePulse 2.4s var(--ease) infinite;
}
.live__text { font-size: .8rem; font-weight: 500; color: #a7f3d0; }
.live__meta { margin-left: auto; font-size: .68rem; color: #6ee7b7; opacity: .8; }
/* На светлой подложке мятный текст сливался с ней — тот же приём, что у
   бейджей: подпись темнее и насыщеннее. */
html[data-theme="light"] .live__text { color: #047857; }
html[data-theme="light"] .live__meta { color: #059669; opacity: 1; }
html[data-theme="light"] .live__dot { background: #10b981; box-shadow: none; }
html[data-theme="light"] .live__dot::after { background: #10b981; }
@keyframes livePulse {
  0%   { transform: scale(1); opacity: .7; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* Плашки-«пилюли» с иконкой в кружке и подсказкой по наведению.
   Подсказка — обыный ::after с абсолютным позиционированием: работает и по
   наведению, и по фокусу с клавиатуры (у плашки стоит tabindex). */
.trust { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .6rem; }
.trust__pill {
  /* flex: none — обязателен. Плашка по умолчанию сжимается (flex-shrink: 1),
     а текст в ней nowrap, и при нехватке места содержимое выдавливалось
     наружу: плашка шириной 146px показывала 209px текста и тянула за собой
     переполнение всей страницы. Ряд переносится — для того он и wrap. */
  flex: none;
  position: relative;
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .35rem .7rem .35rem .4rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: rgb(var(--tint) / .03);
  font-size: .78rem; font-weight: 500; color: var(--ink-dim);
  white-space: nowrap; cursor: help;
  transition: border-color .2s var(--ease), background .2s var(--ease), color .2s var(--ease);
}
.trust__pill:hover, .trust__pill:focus-visible {
  border-color: rgba(129, 140, 248, .45);
  background: rgba(99, 102, 241, .1); color: var(--ink);
}
.trust__icon {
  display: grid; place-items: center; width: 1.35rem; height: 1.35rem; flex: none;
  border-radius: 999px; color: var(--fg-accent);
  background: linear-gradient(140deg, rgba(99, 102, 241, .3), rgba(34, 211, 238, .18));
  box-shadow: inset 0 0 0 1px rgba(129, 140, 248, .25);
}
.trust__icon svg { width: .8rem; height: .8rem; }
.trust__pill:hover .trust__icon {
  box-shadow: inset 0 0 0 1px rgba(129, 140, 248, .5), 0 0 12px -2px rgba(99, 102, 241, .8);
}

/* Подсказка: появляется над плашкой. Крайняя плашка прижимает её к правому
   краю, чтобы длинный текст не уехал за границу окна. */
.trust__pill[data-tip]::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + .6rem); left: 50%;
  width: max-content; max-width: 17rem;
  padding: .5rem .7rem; border-radius: .5rem;
  background: var(--panel-solid); border: 1px solid var(--line-firm);
  box-shadow: 0 16px 34px -14px rgba(0, 0, 0, .95), var(--glow-accent);
  color: var(--ink); font-size: .74rem; font-weight: 400; line-height: 1.45;
  letter-spacing: 0; text-transform: none; text-align: left; white-space: normal;
  opacity: 0; pointer-events: none; translate: -50% .3rem;
  transition: opacity .18s var(--ease), translate .18s var(--ease);
}
.trust__pill:hover::after, .trust__pill:focus-visible::after { opacity: 1; translate: -50% 0; }
.trust__pill:last-child::after { left: auto; right: 0; translate: 0 .3rem; }
.trust__pill:last-child:hover::after, .trust__pill:last-child:focus-visible::after { translate: 0 0; }

/* Подсказка гасится прозрачностью, но из раскладки она не уходит: ::after с
   width: max-content остаётся элементом шириной до 17rem и растягивает
   прокручиваемую область, даже когда его opacity равен нулю. На телефоне из-за
   этого документ оказывался шире вьюпорта.

   content: none убирает псевдоэлемент целиком — и переполнение, и саму
   подсказку. Это честно: на устройстве без наведения она всё равно не
   показывалась, то есть была только причиной переполнения, но не
   содержимым. Тот же приём у узкого окна на ПК: подсказке там тоже некуда
   поместиться. */
@media (hover: none), (max-width: 639px) {
  .trust__pill[data-tip]::after { content: none; }
}

/* --------------------------------------------------- social proof (доверие)
   Фактические цифры платформы. Точки-разделители вместо длинной линии. */
.social-proof {
  display: inline-flex; flex-wrap: wrap; align-items: center; gap: .45rem .8rem;
  padding: .5rem .85rem; border-radius: 999px;
  background: rgb(var(--tint) / .03); border: 1px solid var(--line);
  font-size: .76rem; color: var(--ink-dim);
}
.social-proof__item { display: inline-flex; align-items: center; gap: .4rem; }
.social-proof__item b { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.social-proof__item svg { color: var(--ink-faint); }
.social-proof__dot {
  width: .45rem; height: .45rem; border-radius: 999px; flex: none;
  background: #10b981; box-shadow: 0 0 8px rgba(16, 185, 129, .55);
}
.social-proof__sep { width: 3px; height: 3px; border-radius: 999px; background: var(--line-firm); }

/* ------------------------------------------- HERO: пятна света и карточки цен
   Пятна размыты и лежат под контентом (z-index:-1), поэтому дают глубину,
   но не мешают выделять и читать текст поверх. */
.hero__glow {
  position: absolute; pointer-events: none; z-index: -1;
  border-radius: 50%; filter: blur(90px);
}
.hero__glow--head {
  width: 38rem; height: 26rem; top: -8rem; left: 8%;
  background: radial-gradient(circle, rgba(99, 102, 241, .32), rgba(99, 102, 241, 0) 68%);
}
/* Пятно теперь под рядом карточек: карточки цен ушли из правой колонки
   на всю ширину, и пятно справа от заголовка повисло бы в пустоте. */
.hero__glow--games {
  width: 46rem; height: 22rem; top: auto; bottom: -6rem; left: 50%;
  translate: -50% 0;
  background: radial-gradient(circle, rgba(139, 92, 246, .26), rgba(34, 211, 238, .10) 48%, transparent 72%);
}
@media (max-width: 1023px) {
  .hero__glow--games { width: 34rem; opacity: .75; }
}

/* ---- три игры рядом: шапка и сетка ----
   Сетка на три равные колонки на всю ширину секции: в прежней правой
   колонке (~26rem) три карточки не поместились бы — в каждой знак,
   три тега ресурсов, цена и кнопка. На узких экранах одна колонка,
   переход плавный на auto-fit, без ручных брейкпоинтов на каждом шаге. */
/* Секция обрезает свои пятна. Каждое из них шире экрана (38rem при
   left: 8%) и вылезает за края на десятки пикселей; раньше их держал только
   body { overflow-x: hidden }, но тот переносится на вьюпорт и гасит лишь
   прокрутку — документ оставался шире экрана, и браузер на телефоне ужимал
   страницу, оставляя поле сбоку. clip, а не hidden: он не создаёт
   прокручиваемый контейнер и не влияет ни на что, кроме видимости краёв. */
.hero { overflow: clip; }

.hero-games { position: relative; margin-top: 3.5rem; }
.hero-games__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;
}
.hero-games__note { font-size: .72rem; color: var(--ink-faint); }
.hero-games__grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  list-style: none; margin: 0; padding: 0;
}

/* --------------------------------------------------------- слайдер игр
   Лента едет на transform, а не на нативной прокрутке. Причина практическая:
   бесконечная прокрутка с копиями слайдов всегда упирается в границу
   содержимого, и переход через неё приходится делать скачком scrollLeft —
   полоса при этом моргает, пролистывая весь ряд. На transform границы нет
   вовсе: позиция — целое число, уходящее в любую сторону, а слайд, ушедший
   за край, просто появляется с противоположной. Никаких копий и прыжков.

   Смещение соседей задаёт data-offset на ОБЁРТКЕ слайда, а не на самой
   карточке. Раньше transform был и там, и там: наведение перебивало
   смещение соседа и тот прыгал до полного размера ровно в момент, когда
   пользователь его трогал. */
.game-slider { position: relative; margin-top: 3.5rem; }

/* ---- первый экран целиком --------------------------------------------
   Секция с играми должна помещаться на первый экран, а не подгибаться
   под низ ноутбука. Замерял: при вьюпорте 1246×683 низ карточки приходился
   на 678px при высоте 683 — оставалось 5px, и любая разница в шрифтах,
   зуме браузера или высоте панели уводила карточку за край.

   Подтянуто три промежутка, и все три — на широких экранах: на телефоне
   воздуха нужно больше, а высоты там всё равно не хватает только из-за
   телефона, а не из-за раскладки.

   Селекторы с двойным классом и по элементу не случайно: Tailwind CDN
   подставляет utilities в <head> после app.css, и при равной
   специфичности его pt-28 и mt-10 побеждали бы наши правила.

   Собрано 72px: отступ сверху 112→76, блок фактов 40+24→28+20, лента
   56→36. Карточка теперь кончается на 606px — в 683px экране остаётся
   77px запаса. */
@media (min-width: 1024px) {
  section.hero > div.relative { padding-top: 4.75rem; }
  section.hero .trust { margin-top: 1.75rem; padding-top: 1.25rem; }
  .game-slider { margin-top: 2.25rem; }
}
.game-slider__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: .9rem;
}
.game-slider__controls { display: flex; align-items: center; gap: .9rem; }
.game-slider__note { font-size: .72rem; color: var(--ink-faint); }
/* Ширина карточки в абсолютных единицах: в процентах поля трека и basis
   слайда зависели бы друг от друга (поля считаются от родителя, basis — от
   уже уменьшенного контент-бокса), и ряд разъезжался бы по-браузерно. */
.game-slider__stage {
  position: relative; overflow: hidden;
  --card-w: min(38rem, 60vw);
  --slide-gap: 1.25rem;
  --step: calc(var(--card-w) + var(--slide-gap));
  /* симметричные поля: активная карточка всегда по центру, а по краям
     торчат одинаковые куски соседей */
  padding-inline: max(1.5rem, calc(50% - var(--card-w) / 2));
  /* overflow: hidden срезает по краю поля, а соседи опущены на 1.35–2.1rem,
     а активная карточка на наведении ещё и подпрыгивает на 2px — без
     запаса снизу срезалась нижняя рамка соседа, сверху верхняя рамка
     активной. Поля дают место внутри среза, а отрицательные отступы
     возвращают блоку прежнюю высоту и положение, так что вёрстка не
     сдвигается ни на пиксель. */
  padding-block: 10px 30px;
  margin-block: -10px -30px;
  /* Затухание у краёв окна. overflow:hidden срезал соседей вертикальной
     чертой ровно по границе, и это читалось как обрез, а не как продолжение
     ленты за экран. Маска гасит их прозрачностью к краям — градиент только
     горизонтальный, так что верх и низ карточек не теряют контраста.
     Ширину берём от ширины сцены в процентах: активная карточка занимает
     центральные ~60%, поэтому fades на 5% заведомо её не касаются. */
  --edge-fade: clamp(1.25rem, 5%, 5rem);
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0, #000 var(--edge-fade),
    #000 calc(100% - var(--edge-fade)), transparent 100%);
  mask-image: linear-gradient(90deg,
    transparent 0, #000 var(--edge-fade),
    #000 calc(100% - var(--edge-fade)), transparent 100%);
}
.game-slider__track {
  position: relative;
  display: flex;
  height: 100%;
  cursor: grab;
  will-change: transform;
}
.game-slider__stage.is-dragging { cursor: grabbing; }

.game-slider__slide {
  /* Слайд остаётся в потоке: у absolute-обёртки трек терял высоту и
     схлопывался в ноль — тогда ни мышь, ни касание в него не попадали,
     и лента переставала листаться жестом совсем. */
  flex: 0 0 var(--card-w);
  width: var(--card-w);
  /* Горизонталь ставит скрипт через --slot-x, и перехода по transform здесь
     намеренно нет: при переходе через край слот слайда скачком меняется на
     ±n, и любой переход протащил бы карточку через всю ленту поверх
     активной. Опускание и масштаб соседей живут на самой карточке — их
     переход от этого не гаснет. */
  transform: translate3d(var(--slot-x, 0px), 0, 0);
  transition: opacity .42s var(--ease), filter .42s var(--ease);
  will-change: transform;
}
.game-slider__stage.is-dragging .game-slider__slide {
  /* при перетаскивании полоса идёт за пальцем один в один, без догоняющей
     анимации — иначе жест ощущается «резиновым». На карточку это правило
     не действует: там transform отвечает за опускание соседей и должен
     плавно доезжать даже посреди жеста. */
  transition: opacity .3s var(--ease), filter .3s var(--ease);
}
.game-slider__slide > .hero-game { height: 100%; }

/* Соседи опущены и приглушены. Расстояние проставляет скрипт в data-offset:
   на CSS расстояние по кольцу не вычислить. */
.game-slider__slide[data-offset="0"] { z-index: 2; }
.game-slider__slide[data-offset="1"] {
  --shift-y: 1.35rem; --scale: .965;
  opacity: .72; filter: saturate(.95);
}
.game-slider__slide[data-offset="2"] {
  --shift-y: 2.1rem; --scale: .94;
  opacity: .45; filter: saturate(.82);
}

@media (max-width: 700px) {
  .game-slider__stage { --card-w: 80vw; }
  /* На телефоне опускать соседей незачем: их почти не видно, а карточка
     съезжает вниз вместе с ними и ломает первый экран. */
  .game-slider__slide[data-offset="1"],
  .game-slider__slide[data-offset="2"] {
    --shift-y: 0px; --scale: .97; opacity: .62; filter: none;
  }
}

/* Уважение к настройке движения: без переходов лента переставляется мгновенно,
   но по кольцу всё равно переезжает — это не «эффект», а смена содержимого. */
@media (prefers-reduced-motion: reduce) {
  .game-slider__slide { transition: opacity .2s linear; }
  .hero-game {
    transition: border-color .2s linear, background .2s linear,
                box-shadow .2s linear;
  }
  .game-slider__slide[data-offset="1"],
  .game-slider__slide[data-offset="2"] { --shift-y: 0px; --scale: 1; }
}

/* Ссылка внутри перетаскиваемой ленты не должна вести себя как ссылка для
   мыши: тянуть карточку — это листать, а не уводить курсор с адреса.
   touch-action: pan-y оставляет жестам страницы (вертикальный скролл)
   нетронутыми, а горизонтальный ведёт лента. */
.game-slider__slide { -webkit-user-drag: none; touch-action: pan-y; }

/* ---- карточка игры в шапке ----
   Вся карточка — одна ссылка, поэтому внутри только span'ы: вложенный <a>
   невалиден по HTML и скринридер читает «ссылка в ссылке». Подпись «Собрать
   сервер» выглядит как кнопка, но остаётся частью той же ссылки. */
.hero-game {
  display: flex; flex-direction: column; gap: .9rem;
  height: 100%; padding: 1.15rem 1.2rem 1.2rem;
  position: relative; isolation: isolate; overflow: hidden;
  border-radius: var(--radius);
  background: rgb(var(--tint) / .035);
  border: 1px solid var(--line);
  text-decoration: none; color: inherit;
  /* Опускание и масштаб соседа — отдельный transform с длинным переходом.
     Раньше они делили transform с горизонтальной позицией слайда, а переход
     по позиции на время перетаскивания гасится: карточка скачком меняла
     состояние — то взлетала, то падала — вместо плавного перехода. */
  transform: translateY(var(--shift-y, 0px)) scale(var(--scale, 1));
  /* Кривая здесь намеренно другая, не --ease и не «догоняющая»: обе
     стартуют слишком резко и карточка взлетала на 10px за кадр — выглядело
     как рывок. Плавная симметричная кривая растягивает опускание и подъём
     примерно на треть секунды. */
  transition: border-color .22s var(--ease), background .22s var(--ease),
              box-shadow .22s var(--ease),
              transform .36s cubic-bezier(.4, 0, .2, 1),
              translate .22s var(--ease);
}
/* ---- Подложка правой половины карточки -------------------------------
   Раньше справа от текста было пустое место: карточка читалась как
   обрезанный список. Теперь там два слоя — цветовое свечение игры и её
   же сцена (games.py ART_*), оба гаснут маской к тексту слева, поэтому
   подпись остаётся читаемой, а рамка и радиус не меняются. */

/* Цветовая подложка: фирменный акцент игры мягко разливается от правого
   нижнего угла. */
.hero-game::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background: radial-gradient(92% 70% at 100% 96%,
    color-mix(in srgb, var(--g1) 30%, transparent) 0%,
    color-mix(in srgb, var(--g2) 16%, transparent) 44%,
    transparent 76%);
  /* Затухание строго горизонтальное: под углом 100deg левый верх уже
     наполовину непрозрачен, и граница слоя выходила неровной. */
  -webkit-mask-image: linear-gradient(90deg, transparent 34%, #000 100%);
  mask-image: linear-gradient(90deg, transparent 34%, #000 100%);
}
/* Сцена игры: SVG рисуется currentColor, поэтому красится акцентом
   карточки. Собственная preserveAspectRatio=xMidYMax прижимает её к низу,
   а маска гасит левый край, где начинается текст. */
.hero-game__art {
  position: absolute; z-index: 0;
  top: 0; bottom: 0; left: 32%; right: 0;
  width: auto; height: 100%;
  color: var(--g1); opacity: .28;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(100deg, transparent 0%, #000 64%);
  mask-image: linear-gradient(100deg, transparent 0%, #000 64%);
}
/* Растровый кадр: кадрируем по центру и чуть выше середины — в кадре
   персонаж в прыжке, при съезде вниз срезались бы голова и реактор.

   Прозрачность поднята с .36 до .5: на низкой кадр смешивался с фоном
   карточки и терял цвет — замерял среднюю насыщенность пикселей кадра,
   выходило 0.05 против 0.32 у исходника. Поднятие прозрачности влияет
   прежде всего на то, что держит насыщенность сильнее фильтра: тот же
   кадр при .36 и .5 различается втрое по цвету.

   Подписей это не задевает: маска ниже гасит кадр до нуля у левого края,
   а текст живёт именно там. Проверял контраст текста на карточке после
   правки — не просел. */
.hero-game__art--img {
  /* Кадр приглушается ДВАЖДЫ: своей прозрачностью и подложкой ::after
     под текстом. При .5 и подложке ~76% от кадра оставалось 12% — он был
     виден только на самом краю, а всё остальное читалось чёрным пятном.
     Считаем вместе: картинка = подложка × прозрачность кадра. Планируем
     0.38 × 0.85 ≈ 0.32 вместо прежних 0.24 × 0.5 = 0.12 — почти втрое
     больше рисунка, и именно это просил пользователь. */
  opacity: .85; left: 18%;
}
/* Два затухания: к тексту слева и от верхней рамки карточки. Без верхнего
   светлое небо кадра обрывалось о бордюр и читалось как полоса. */
.hero-game__art--img {
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, .45) 30%, #000 74%),
                      linear-gradient(180deg, transparent 0%, #000 26%);
  mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, .45) 30%, #000 74%),
              linear-gradient(180deg, transparent 0%, #000 26%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.hero-game__art--img img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 42%;
  /* Кадр лежит при прозрачности .36, а поверх — фон карточки, и вместе
     они съедали цвет: замерял среднюю насыщенность пикселей кадра —
     0.107 в тёмной теме и 0.04 в светлой, то есть картинка была почти
     серой. Поднимаем насыщенность на самой картинке, а не её прозрачность:
     число .36 подобрано под читаемость текста слева, и трогать его —
     значит возвращать претензию на текст под фотографией.

     Контраст чуть-чуть тоже: насыщенный кадр без контраста мутнеет,
     серые полутона не вытягивают яркость пятна на кадре. */
  filter: saturate(1.9) contrast(1);
}
/* Светлая тема бледнее тёмной вдвое: тёмная подложка сама по себе
   добавляет цвета полутонам, белая — нет. Отсюда и меньшая насыщенность
   кадра по замеру (0.04 против 0.107).

   Контраст фильтра убран в ноль обеих тем: поверх кадра теперь лежит
   затемнение .hero-game::after, и добавлять контраст к тому, что уже
   затемнено, значило выдёргивать полутона обратно — карточка выглядела
   контрастной и мутной одновременно. Насыщенность оставлена: скрим гасит
   её равномерно и цвет остаётся, а чёрно-белые провалы уходят. */
html[data-theme="light"] .hero-game__art--img img {
  filter: saturate(2.2) contrast(1);
}

/* Текст и плашки — над обоими слоями. Правило обязательное: без него
   содержимое карточки остаётся position: static и по правилам наложения
   рисуется ПОД любым позиционированным слоем, включая ::after с
   затемнением. В тёмной теме это читалось бы как «текст стал тусклее»,
   в светлой — как вымытый в белое. */
.hero-game > *:not(.hero-game__art) { position: relative; z-index: 1; }

/* Затемнение под текстом.
   Был один диагональный градиент 100deg: плотный слева и прозрачный
   справа. На карточке шириной ~460px это давало почти сплошную черноту
   слева — арт не читался вовсе, — и одновременно хвост каждой строки
   (описание игры, пункты списка доходят до ~75% ширины) выходил на
   голый кадр. Замер: подложка под текстом (8,16,24), у подзаголовка на
   второй карточке контраст 3.32 при норме AA 4.5.

   Теперь это две шкалы вместо одной:
     * горизонтальная держит плотность до 70% ширины — то есть под всем
       текстом, — и отпускает правый край, где только кадр;
     * вертикальная добавляет вес к низу, где цена и чек-лист, и слегка
       притушивает верх.

   Обе шкалы идут длинными пологими участками, а не крутыми ступенями:
   резкий переход читался как граница блока, и карточка выглядела так,
   будто под текстом приклеена чёрная плашка. */
.hero-game::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      color-mix(in srgb, var(--bg-card) 30%, transparent) 0%,
      color-mix(in srgb, var(--bg-card) 40%, transparent) 58%,
      color-mix(in srgb, var(--bg-card) 62%, transparent) 100%),
    linear-gradient(100deg,
      color-mix(in srgb, var(--bg-card) 46%, transparent) 0%,
      color-mix(in srgb, var(--bg-card) 42%, transparent) 45%,
      color-mix(in srgb, var(--bg-card) 34%, transparent) 74%,
      color-mix(in srgb, var(--bg-card) 16%, transparent) 88%,
      color-mix(in srgb, var(--bg-card) 3%, transparent) 97%,
      transparent 100%);
}
/* Размытия кадра здесь нет намеренно. backdrop-filter на всей карточке
   даёт эффект матового стекла: картинка превращается в молочную пелену,
   а поверх неё светлый текст светлой темы пропадал совершенно. Плюс он
   съедал картинку — а именно её и просили показать. Читаемость держит
   сама подложка, а не стекло. */

.hero-game:hover, .hero-game:focus-visible {
  /* Свечение и бордер фирменные, а не цвета игры: выбор не должен
     выглядеть по-разному в зависимости от того, какая игра на карточке. */
  border-color: rgb(var(--brand) / .55);
  background: rgb(var(--brand) / .06);
  box-shadow: 0 14px 34px -18px rgb(var(--brand) / .7);
  /* подъём отдельным свойством translate: transform занят опусканием
     соседа и перетирал бы его, из-за чего у неактивной карточки наведение
     сбрасывало состояние */
  translate: 0 -2px;
}
.hero-game:focus-visible { outline: 2px solid rgb(var(--brand) / .75); outline-offset: 2px; }

.hero-game__head { display: flex; align-items: center; gap: .8rem; }
/* Знак игры: квадратный, скруглённый, в обводку — object-fit заполняет
   плитку по контуру, поэтому у gmod/minecraft/samp знаки одного калибра. */
.hero-game__logo {
  display: grid; place-items: center; flex: none;
  width: 2.75rem; height: 2.75rem; border-radius: .7rem;
  overflow: hidden; color: #fff;
  background: linear-gradient(140deg, var(--g1), var(--g2));
  box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--g1) 90%, transparent),
              inset 0 0 0 1px rgb(var(--tint) / .08);
}
.hero-game__logo .game-mark { width: 100%; height: 100%; object-fit: cover; }
.hero-game__name {
  display: block; font-size: .95rem; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Подзаголовок на карточке остаётся темой: цвет здесь не задаётся.
   Попытка осветлить его ради тёмной темы (#d3dae6) сработала обратным
   образом в светлой: белый текст на светлой подложке стал нечитаемым, а
   карточка — молочной. Тема задаёт --ink-faint, и в каждой из них своё
   правильное значение.

   Тени под текстом нет — по просьбе. Её добавляли, чтобы подзаголовок не
   падал по контрасту там, где подложка отпускает кадр, но в светлой теме
   тёмный ореол под серым текстом читается как грязь и двоение, а не как
   страховка читаемости: карточка выглядит испорченной. Читаемость держит
   сама подложка. */
.hero-game__tagline {
  margin-top: .15rem; font-size: .72rem; line-height: 1.35;
  color: var(--ink-faint);
  /* Две строки вместо одной: на широкой карточке «GTA San Andreas
     Multiplayer — ролевая классика» не помещалась в одну и обрезалась
     многоточием, хотя места под вторую строку было навалом. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
/* Правило с тенью под чек-листом убрано вместе с тенью подзаголовка —
   по той же просьбе и по той же причине: тёмный ореол в светлой теме. */

/* Теги ресурсов: та же минимальная конфигурация, из которой посчитана цена */
.hero-game__specs { display: flex; flex-wrap: wrap; gap: .35rem; }
.hero-game__spec {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .2rem .5rem; border-radius: .4rem;
  background: rgb(var(--tint) / .045); border: 1px solid var(--line-soft);
  font-size: .7rem; font-weight: 500; color: var(--fg-chip);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.hero-game__spec svg { width: .8rem; height: .8rem; flex: none; color: var(--ink-faint); }

/* Подвал: цена крупно + действие. margin-top:auto прижимает его к низу,
   и при разной длине названий карточки остаются одной высоты. */
.hero-game__foot {
  display: flex; align-items: baseline;
  gap: .75rem; margin-top: auto; padding-top: .3rem;
}
/* Цена в витрине. Зачёркнутая мельче новой: она перестаёт быть главным
   числом на карточке, а новой становится та, которую платят. Раньше
   зачёркнутая была того же кегля, что и обычная цена, и глаз цеплялся за
   неё — то есть первым прочитывалось то, чего человек не платит. */
.hero-game__prices {
  display: flex; align-items: baseline; gap: .5rem;
}
.hero-game__was, .hero-game__now {
  font-weight: 700; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.hero-game__was {
  position: relative;
  font-size: .95rem; font-weight: 600;
  /* Цвет не --ink-faint: замерял по пикселям, тот давал 3.96 в тёмной и
     4.18 в светлой теме при норме 4.5. Мелкий кегль не даёт права на
     «крупный текст», а зачёркнутая цена всё ещё должна читаться. */
  color: var(--ink-dim);
}
/* Черта своя на ::after, а не готовое text-decoration: у того толщина и
   цвет зависят от шрифта, а на карточке шрифт обычный, и штатная черта
   ложилась бы по самому краю цифр. Чуть выше середины строки: у цифр
   оптический центр выше геометрического, и по центру черта режет их. */
.hero-game__was::after {
  content: ""; position: absolute;
  left: -.05em; right: -.05em; top: 54%;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg,
    rgb(248 113 113 / .55), rgb(248 113 113 / .95));
  transform-origin: left center;
  animation: was-strike .42s .1s var(--ease) both;
}
.hero-game__now { font-size: 1.5rem; color: var(--ink); }
.hero-game__now.is-solo { font-size: 1.35rem; }
/* Ноль у пробы — не цена, а её отсутствие: красим акцентом и убираем
   «/мес». Бесплатного месяца пробы не бывает, а «0 ₽/мес» обещало бы
   ровно его. */
.hero-game__now.is-zero { color: #34d399; }
html[data-theme="light"] .hero-game__now.is-zero { color: #047857; }
.hero-game__now small { font-size: .62em; font-weight: 600; opacity: .62; }
/* Метка рядом: «−20%» одной цифрой не читалось бы как скидка, а слово
   «проба» — тем более. Пробная метка зелёная: это единственное место на
   странице, где зелёный значит «бесплатно».

   Подложка обязательна: метка лежит поверх снимка карточки, а не на
   пустом поле. Замерял по пикселям — зелёный на светлой теме давал
   контраст 1.04 против снимка, то есть читался только там, где попадал на
   светлый участок кадра. */
.hero-game__save {
  align-self: flex-start;
  padding: .12rem .4rem; border-radius: .3rem;
  font-size: .66rem; font-weight: 600; letter-spacing: .01em;
  /* Фиолетовый по тёмной подложке, а не по фиолетовой заливке: на заливке
     он давал 3.6 при норме 4.5. */
  color: #a5b4fc;
  background: rgb(6 8 14 / .74);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  animation: wiz-in .34s .34s var(--ease) both;
}
.hero-game__save.is-trial { color: #34d399; }
html[data-theme="light"] .hero-game__save {
  color: #4338ca;
  background: rgb(255 255 255 / .88);
}
/* Зелёный на светлой подложке берём темнее прежнего #059669: тот давал
   2.74 против белого, а подложка не белая, а чуть тёмная. */
html[data-theme="light"] .hero-game__save.is-trial {
  color: #047857; background: rgb(255 255 255 / .88);
}
.hero-game__price small {
  margin-left: .15rem; font-size: .72rem; font-weight: 500; color: var(--fg-muted);
}

/* Самодостаточный знак (GTA SAMP): у файла своя подложка и надпись, и он не
   квадратный (123x128). Плитка с градиентом превращала его в квадрат внутри
   квадрата, а object-fit: cover ещё и срезал «San Andreas». Такой знак
   рисуется целиком и без плитки: подложки нет, обрезки нет, размер задаёт
   высота, ширина идёт по пропорциям.

   Класс ставит шаблон по games.py::GameSpec.logo_tile — правило живёт в
   данных, а не в вёрстке, поэтому добавление нового самодостаточного
   знака не требует правок CSS.

   Блок стоит здесь, а не рядом с остальными компонентами, и порядок здесь
   не случаен: `.hero-game__logo` объявлен выше по файлу и имеет ту же
   специфичность, что и `--raw`. Правило, объявленное позже, выигрывает —
   и плитка с фиолетовым градиентом возвращалась поверх самодостаточного
   знака именно в карточке игры в шапке. Перенос блока ниже всех трёх
   плиток чинит это для всех сразу и оставляет одно место на правку. */
.hero-game__logo--raw,
.game-card__icon--raw,
.game-mark-tile--raw {
  background: none; box-shadow: none;
  overflow: visible; border-radius: 0;
  width: auto; height: auto;
}
.hero-game__logo--raw .game-mark,
.game-card__icon--raw .game-mark,
.game-mark-tile--raw .game-mark {
  width: auto; height: 2.5rem; max-width: 100%;
  object-fit: contain; object-position: center;
}
/* Самодостаточный знак крупнее плитки примерно на 8%: у samp.png по краю
   идёт тёмная рамка, поэтому при равной геометрии его светлая часть
   кажется мельче соседних квадратов. Это подгонка по глазу, а не по
   математике: оба знака остаются соразмерными, но ряд выглядит ровно. */
.hero-game__logo--raw .game-mark,
.game-mark-tile--raw .game-mark { height: 2.95rem; }

/* Знак без плитки, окрашенный цветом темы (CS2).
   PNG используется как маска: заливка берётся из --ink, поэтому в тёмной
   теме силуэт светлый, а в светлой — тёмный, и знак не зависит от фона.
   Плитки нет вовсе, поэтому `background: none !important` из .game-mark-raw
   пришлось перебить — иначе маска красила бы пустоту. Размер 2.75rem
   совпадает с плиткой остальных карточек: силуэт занимает весь кадр, в отличие
   от samp.png с рамкой, которому нужен запас. */
.game-mark--ink {
  background-color: var(--ink) !important;
  /* Источник формы даёт игра через --ink-mask: литерал пришлось бы
     дублировать на каждый знак. У PNG маска берёт альфа-канал, поэтому
     цвет пикселей файла не важен — им красит background-color выше. */
  -webkit-mask: var(--ink-mask) center / contain no-repeat;
  mask: var(--ink-mask) center / contain no-repeat;
  /* Размер по умолчанию берётся из --mark-h обёртки, а не из константы:
     знак-маска должен попадать в плитку, где --mark-h равен её стороне.
     С константой CS2 и CS 1.6 в подвале рисовались вдвое выше строки. */
  width: var(--ink-w, var(--mark-h, 2.75rem)) !important;
  height: var(--ink-h, var(--mark-h, 2.75rem)) !important;
  display: var(--ink-d, block);
}
/* В плитке знак не вылезает за её сторону: ширина широких надписей идёт от
   пропорций файла, а маска рисует картинку по contain — то есть при
   ограничении по ширине слово просто ужимается и центрируется. */
.game-mark-raw .game-mark--ink { max-width: var(--mark-h, 2.75rem); }
.game-mark--ink--cs2  { --ink-mask: url("/static/img/games/cs2.png"); }
.game-mark--ink--cs16 { --ink-mask: url("/static/img/games/cs16.png"); }

/* Знак-надпись (DayZ, Project Zomboid). Квадратный .game-mark--ink такую
   надпись срезал бы по краям, поэтому размеры уходят в переменные: ширина
   считается сама из пропорций файла (--ink-ratio), высота — из --mark-h
   обёртки, а там, где обёртки нет, из значения по умолчанию 2.95rem (столько
   же, сколько получают остальные самодостаточные знаки в шапке). */
.game-mark--ink--word {
  --ink-w: auto;
  --ink-h: var(--mark-h, 2.95rem);
  --ink-d: inline-block;
  aspect-ratio: var(--ink-ratio);
}
.game-mark--ink--dayz {
  --ink-mask: url("/static/img/games/dayz.png");
  --ink-ratio: 512 / 243;
}
.game-mark--ink--zomboid {
  --ink-mask: url("/static/img/games/zomboid.png");
  --ink-ratio: 512 / 272;
}
/* В списке выбора игры карточка узкая, а слово вдвое шире квадратного знака —
   оно отжимало бы название. Здесь высота меньше, ширина идёт следом. */
.game-mark-tile--raw .game-mark--ink--word { --ink-h: 2.1rem; }

/* Знак-широкая надпись (Terraria, пропорции 3.4:1). Это <img>, а не маска:
   логотип цветной, и монохромный силуэт потерял бы всю графику. Задаём
   ширину и не задаём высоту — у <img> с width и height:auto высота сама
   считается по пропорциям файла.

   В квадратных плитках (подвал, карточка сервера, шапка сервера) ширина
   берётся от --mark-h: там он равен стороне плитки, и слово укладывается в
   коробку целиком. В шапке карточки и в списке выбора игры --mark-h не задан,
   там своя ширина: по общей для самодостаточных знаков высоте Terraria была бы
   шире самой карточки. */
.game-mark--wide {
  width: var(--mark-w, 8rem) !important;
  height: auto !important;
  max-width: 100% !important;
}
.game-mark-raw .game-mark--wide { --mark-w: var(--mark-h); }
.game-mark-tile--raw .game-mark--wide { --mark-w: 4.6rem; }

.gradient-text {
  background: linear-gradient(105deg, #fff 8%, #a5b4fc 45%, #22d3ee 92%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.text-brand-grad {
  background: linear-gradient(100deg, #818cf8, #22d3ee);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* ==========================================================================
   6. КНОПКИ И ПОЛЯ
   ========================================================================== */
.btn-primary, .btn-ghost, .btn-danger, .btn-ok, .btn-sub {
  display: inline-flex; align-items: center; gap: .45rem; justify-content: center;
  padding: .55rem 1rem; border-radius: .55rem; font-size: .875rem; font-weight: 500;
  border: 1px solid transparent; cursor: pointer; white-space: nowrap;
  text-decoration: none;
  transition: background .18s var(--ease), border-color .18s var(--ease),
              color .18s var(--ease), box-shadow .18s var(--ease), transform .12s var(--ease);
}
.btn-primary {
  color: #fff;
  background: linear-gradient(180deg, #6366f1, #4f46e5);
  box-shadow: inset 0 1px 0 rgb(var(--tint) / .18), 0 6px 18px -10px rgba(99, 102, 241, .9);
}
.btn-primary:hover { background: linear-gradient(180deg, #6d6ff5, #4b45e8); box-shadow: var(--glow-accent-strong), inset 0 1px 0 rgb(var(--tint) / .2); }
.btn-sub { color: #c7d2fe; background: rgba(99, 102, 241, .14); border-color: rgba(129, 140, 248, .3); }
.btn-sub:hover { background: rgba(99, 102, 241, .24); color: #fff; border-color: rgba(129, 140, 248, .5); }
.btn-ghost { color: var(--ink-dim); background: rgb(var(--tint) / .04); border-color: var(--line); }
.btn-ghost:hover { background: rgb(var(--tint) / .09); color: var(--ink); border-color: var(--line-firm); }
.btn-ok { color: #6ee7b7; background: rgba(16, 185, 129, .12); border-color: rgba(52, 211, 153, .32); }
.btn-ok:hover { background: rgba(16, 185, 129, .22); color: #fff; box-shadow: 0 0 20px -6px rgba(52, 211, 153, .7); }
.btn-danger { color: #fca5a5; background: rgba(239, 68, 68, .12); border-color: rgba(248, 113, 113, .32); }
.btn-danger:hover { background: rgba(239, 68, 68, .22); color: #fff; box-shadow: 0 0 20px -6px rgba(248, 113, 113, .7); }
.btn-primary:active, .btn-sub:active, .btn-ghost:active, .btn-ok:active, .btn-danger:active { transform: translateY(1px); }
.btn-primary:disabled, .btn-ghost:disabled, .btn-danger:disabled, .btn-ok:disabled, .btn-sub:disabled {
  opacity: .42; cursor: not-allowed; transform: none;
}
.btn-primary:focus-visible, .btn-ghost:focus-visible, .btn-danger:focus-visible,
.btn-ok:focus-visible, .btn-sub:focus-visible, .btn-icon:focus-visible {
  outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 2px;
}

.btn-icon {
  display: inline-grid; place-items: center; height: 2.15rem; width: 2.15rem;
  padding: 0;                       /* обязателен: с .btn-* приходит padding .55rem 1rem,
                                      и при фиксированной ширине иконка схлопывалась в ноль */
  border-radius: .5rem; border: 1px solid var(--line);
  background: rgb(var(--tint) / .03); color: var(--ink-dim); cursor: pointer;
  transition: all .18s var(--ease); text-decoration: none;
}
.btn-icon:hover { background: rgb(var(--tint) / .09); color: var(--ink); border-color: var(--line-firm); }
.btn-icon:disabled { opacity: .4; cursor: not-allowed; }

/* Возврат к списку серверов в правом верхнем углу шапки. Это навигация,
   а не действие над сервером, поэтому выглядит как подпись со стрелкой,
   а не как ещё одна кнопка в ряду команд. */
.back-link {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .35rem .1rem; font-size: .82rem; font-weight: 500;
  color: var(--ink-dim); text-decoration: none; border-radius: .5rem;
  transition: color .18s var(--ease);
}
.back-link:hover { color: var(--ink); }
.back-link:focus-visible { outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 2px; }

/* Поля ввода: спокойная подложка + акцентное кольцо фокуса.
   Цвет набранного текста берётся из токена: литерал #e2e8f0 в светлой теме
   давал светло-серый текст на почти белом поле — введённое было не видно. */
.field, .select-field, .textarea-field {
  width: 100%; padding: .65rem .9rem; font-size: .875rem; color: var(--ink);
  background: rgb(var(--tint) / .04); border: 1px solid var(--line);
  border-radius: .625rem; outline: none;
  transition: border-color .18s var(--ease), background .18s var(--ease), box-shadow .18s var(--ease);
}
.field::placeholder, .textarea-field::placeholder { color: var(--ink-faint); }

/* Поле выбора файла. Без этих правил браузер рисует своё системное
   «Выбрать файл / Файл не выбран» — серую рамку и подпись на английском,
   которые выпадают из формы и выглядят как чужой элемент в готовой
   карточке. Сама кнопка браузера перекрашивается через ::file-selector-button,
   имя выбранного файла остаётся системным. */
/* Свой выбор файла: системный контрол прячем, рядом — кнопка и имя
   выбранного файла. Инпут остаётся в DOM и в форме (иначе файл не
   уйдёт), но занимает один пиксель поверх кнопки и ловит клики по ней. */
.field-file {
  display: flex; align-items: center; gap: .7rem;
  padding: .45rem .45rem .45rem .5rem;
  background: rgb(var(--tint) / .04);
  border: 1px solid var(--line); border-radius: .625rem;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.field-file:focus-within { border-color: rgb(var(--brand) / .55); }
.field-file.is-on { border-color: rgb(var(--brand) / .45); background: rgb(var(--brand) / .06); }
.field-file__input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.field-file__name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: .82rem; color: var(--ink-faint);
}
.field-file.is-on .field-file__name { color: var(--ink); }

/* Поле с кнопкой «глаз» справа. Отступ под поле, а не под обёртку: иначе
   текст наезжал бы на иконку, а фокус-подсветка оставалась бы на инпуте. */
.field-wrap { position: relative; }
.field-wrap .field { padding-right: 2.6rem; }
.field-eye {
  position: absolute; top: 50%; right: .35rem; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 1.9rem; height: 1.9rem; padding: 0; border: 0; border-radius: .45rem;
  background: transparent; color: var(--ink-faint); cursor: pointer;
  transition: color .16s var(--ease), background .16s var(--ease);
}
.field-eye:hover { color: var(--ink); background: rgb(var(--tint) / .07); }
.field-eye:focus-visible { outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 1px; }
.field-eye svg { width: 1rem; height: 1rem; pointer-events: none; }

/* Индикатор надёжности пароля: четыре сегмента + подпись. Сегменты, а не
   сплошная полоса, — по одной заливке не видно, сколько пунктов закрыто. */
.pw-meter { display: flex; align-items: center; gap: .6rem; margin-top: .5rem; }
.pw-meter__bars { display: flex; gap: .25rem; flex: 1; }
.pw-meter__bar {
  height: 3px; flex: 1; border-radius: 999px;
  background: rgb(var(--tint) / .09); transition: background .2s var(--ease);
}
.pw-meter__label {
  flex: none; font-size: .72rem; font-weight: 500; color: var(--ink-faint);
  transition: color .2s var(--ease);
}
.pw-meter[data-level="1"] .pw-meter__bar:nth-child(-n+1) { background: #ef4444; }
.pw-meter[data-level="2"] .pw-meter__bar:nth-child(-n+2) { background: #f59e0b; }
.pw-meter[data-level="3"] .pw-meter__bar:nth-child(-n+3) { background: #22c55e; }
.pw-meter[data-level="4"] .pw-meter__bar:nth-child(-n+4) { background: #10b981; }
.pw-meter[data-level="1"] .pw-meter__label { color: #fca5a5; }
.pw-meter[data-level="2"] .pw-meter__label { color: #fcd34d; }
.pw-meter[data-level="3"] .pw-meter__label,
.pw-meter[data-level="4"] .pw-meter__label { color: #6ee7b7; }

/* Подсказка под полем: совпадение паролей и требования к длине. */
.pw-hint { display: block; margin-top: .4rem; font-size: .72rem; color: var(--ink-faint); }
.pw-hint.is-ok { color: #6ee7b7; }
.pw-hint.is-bad { color: #fca5a5; }
.field.is-invalid { border-color: rgba(248, 113, 113, .7); box-shadow: 0 0 0 3.5px rgba(239, 68, 68, .15); }

/* ---- быстрый вход (OAuth) ----
   Знаки брендов рисуются инлайном (social.py): в lucide их нет.
   Провайдеры без настроенного бэкенда показаны, но не нажимаются — так же,
   как неактивные соцсети в подвале: витрина не пропадает и не врёт. */
.oauth { margin-top: 1.5rem; }
.oauth__sep {
  display: flex; align-items: center; gap: .8rem;
  font-size: .72rem; color: var(--ink-faint);
}
.oauth__sep::before, .oauth__sep::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}
.oauth__list { display: flex; gap: .6rem; margin-top: .9rem; }
.oauth__btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  flex: 1; padding: .6rem .7rem; border-radius: .6rem;
  font-size: .82rem; font-weight: 500; text-decoration: none; cursor: pointer;
  color: var(--ink-dim); background: rgb(var(--tint) / .04);
  border: 1px solid var(--line);
  transition: all .18s var(--ease);
}
/* Глиф знака задан currentColor: кнопка красит текст в цвет подписи,
   а знак на своей фирменной плитке обязан остаться белым. Скругление
   плитки — как в подвале, иначе знаки выглядят набором из двух стилей. */
.oauth__btn svg {
  width: 1.05rem; height: 1.05rem; flex: none; color: #fff;
  border-radius: .22rem;
}
.oauth__btn:hover { color: var(--ink); background: rgb(var(--tint) / .08); border-color: var(--line-firm); }
.oauth__btn.is-off {
  cursor: not-allowed; opacity: .55;
}
.oauth__btn.is-off:hover { color: var(--ink-dim); background: rgb(var(--tint) / .04); border-color: var(--line); }
.oauth__note { margin-top: .7rem; text-align: center; font-size: .7rem; color: var(--ink-faint); }

.field:hover, .select-field:hover, .textarea-field:hover { border-color: var(--line-firm); }
.field:focus, .select-field:focus, .textarea-field:focus {
  border-color: rgba(129, 140, 248, .7); background: rgb(var(--tint) / .06);
  box-shadow: 0 0 0 3.5px rgba(99, 102, 241, .18);
}
/* Нативный select со своей стрелкой — системный выпадающий список выглядит
   инородно в тёмной теме */
.select-field {
  appearance: none; -webkit-appearance: none; padding-right: 2.1rem; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right .7rem center;
}
.select-field option { background: var(--bg-card); color: var(--ink); }
.textarea-field { resize: vertical; line-height: 1.6; }
.label {
  display: block; margin-bottom: .4rem; font-size: .75rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: #94a3b8;
}

/* Чекбокс-переключатель */
.switch { display: inline-flex; align-items: center; gap: .7rem; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch__track {
  position: relative; width: 2.25rem; height: 1.3rem; flex: none;
  background: rgb(var(--tint) / .1); border: 1px solid var(--line);
  border-radius: 999px; transition: background .2s var(--ease), border-color .2s var(--ease);
}
.switch__track::after {
  content: ""; position: absolute; top: 50%; left: 2px; translate: 0 -50%;
  width: 1rem; height: 1rem; border-radius: 999px; background: #94a3b8;
  transition: translate .2s var(--ease), background .2s var(--ease);
}
.switch input:checked + .switch__track { background: rgba(99, 102, 241, .4); border-color: rgba(129, 140, 248, .6); }
.switch input:checked + .switch__track::after { translate: .95rem -50%; background: #fff; }
.switch input:focus-visible + .switch__track { outline: 2px solid rgba(129, 140, 248, .7); outline-offset: 2px; }

/* ==========================================================================
   7. ВЫБОР ИГРЫ И СЛАЙДЕРЫ
   ========================================================================== */
/* Карточки переключения игры.

   Раньше выбранное состояние красилось в цвет самой игры (--g1), и три
   карточки давали три разных акцента: оранжевый у Garry's Mod, зелёный у
   Minecraft, синий у GTA SAMP. Рядом стояли три одинаковые по смыслу вещи,
   выглядевшие как три разных компонента. Теперь выбор — это всегда фирменный
   индиго: цвет игры остаётся только на знаке игры (это его опознавательный
   признак), а всё, что означает «выбрано», единообразно фиолетовое. */
.pick-grid { display: grid; gap: .7rem; grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 639px) { .pick-grid { grid-template-columns: 1fr; } }

.pick {
  position: relative; display: flex; align-items: center; gap: .7rem;
  width: 100%; text-align: left; cursor: pointer;
  padding: .7rem .8rem; border-radius: .75rem;
  background: rgb(var(--tint) / .03); border: 1px solid var(--line);
  color: var(--ink-dim); font-size: .875rem; font-weight: 500;
  /* Полоса сверху — псевдоэлемент с собственным border-radius: на скруглённых
     углах она вылезала за рамку, и над карточкой появлялась красная прямая
     черта. Обрезка по border-radius контейнера убирает её. */
  overflow: hidden;
  transition: border-color .24s var(--ease), background .24s var(--ease),
              box-shadow .24s var(--ease), color .24s var(--ease), transform .18s var(--ease);
}
.pick:hover { color: var(--ink); border-color: var(--line-firm); transform: translateY(-1px); }

/* Акцентная полоса по верхней кромке — тоже фирменная, иначе она снова
   разводит три карточки по трём цветам. */
.pick::before {
  content: ""; position: absolute; inset: 0 0 auto; height: 2px;
  background: linear-gradient(90deg, var(--brand-400), rgb(var(--brand)));
  opacity: 0; transform: scaleX(.4); transform-origin: center;
  transition: opacity .28s var(--ease), transform .28s var(--ease);
}
.pick.is-active {
  color: var(--ink);
  background: rgb(var(--brand) / .10);
  border-color: rgb(var(--brand));
  /* Лёгкое свечение фиолетовым: отделяет выбранную карточку, но не оставляет
     пятна вокруг — радиус размытия съедает края, а не заливает их. */
  box-shadow: 0 8px 22px -10px rgb(var(--brand) / .45);
}
.pick.is-active::before { opacity: 1; transform: scaleX(1); }

.pick__name { min-width: 0; flex: 1; }
/* Метки пробы на плитке игры удалены вместе с раздачей пробы по одной
   на игру: право стало одно на аккаунт, и подпись «проба использована»
   на одной из трёх плиток утверждала бы, что две другие открыты. */
.pick__price {
  display: block; font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .72rem; color: var(--ink-faint); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color .24s var(--ease);
}
.pick.is-active .pick__price { color: var(--brand-400); }

/* Индикатор single-select. Прежде здесь была галочка в круге: у трёх карточек
   подряд она читалась как «отмечено», а не «выбрано», и не совпадала с
   серыми пустыми кружками невыбранных. Теперь это радио — кольцо, у
   выбранного внутри появляется точка. Разметки не нужно: кольцо и точка
   рисуются псевдоэлементами, иконка не грузится. */
.pick__radio {
  position: relative; flex: none;
  display: grid; place-items: center;
  width: 1.15rem; height: 1.15rem; border-radius: 999px;
  border: 1.5px solid var(--line-firm);
  transition: border-color .22s var(--ease), background .22s var(--ease);
}
.pick__radio::after {
  content: ""; width: .5rem; height: .5rem; border-radius: 999px;
  background: rgb(var(--brand));
  scale: 0; transition: scale .22s var(--ease);
}
.pick:hover .pick__radio { border-color: var(--line-firm); }
.pick.is-active .pick__radio {
  border-color: rgb(var(--brand));
  background: rgb(var(--brand) / .14);
}
.pick.is-active .pick__radio::after { scale: 1; }

/* Ряд параметра: подпись + значение + ползунок + степпер */
.ctrl { display: grid; gap: .55rem; }
.ctrl__top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.ctrl__label {
  display: flex; align-items: center; gap: .5rem;
  font-size: .8rem; font-weight: 600; color: var(--fg-card);
}
.ctrl__label svg { width: .9rem; height: .9rem; color: var(--ink-faint); }
.ctrl__value {
  display: inline-flex; align-items: baseline; gap: .3rem;
  padding: .15rem .55rem; border-radius: .45rem;
  background: rgba(99, 102, 241, .12); border: 1px solid rgba(129, 140, 248, .28);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .85rem; font-weight: 700; color: var(--fg-on-tint);
  font-variant-numeric: tabular-nums;
  transition: background .2s var(--ease), border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.ctrl__value.is-bumped { background: rgba(99, 102, 241, .3); box-shadow: 0 0 18px -6px rgba(129, 140, 248, .9); }
.ctrl__unit { font-size: .7rem; font-weight: 500; color: var(--ink-faint); }

/* Степпер −/+ рядом с ползунком: точная правка без перетаскивания.
   Размер учитывает общий масштаб интерфейса: --ui-scale = 0.75 съедает
   четверть, поэтому 2rem (32px) превращаются в 24 реальных пикселя —
   ровно минимум для комфортного попадания пальцем. */
.stepper {
  display: inline-grid; grid-template-columns: repeat(2, 2rem); flex: none;
  border: 1px solid var(--line); border-radius: .55rem; overflow: hidden;
  background: rgb(var(--tint) / .03);
}
.stepper button {
  display: grid; place-items: center; height: 2rem; color: var(--ink-dim);
  cursor: pointer; transition: background .15s var(--ease), color .15s var(--ease);
}
.stepper button + button { border-left: 1px solid var(--line); }
.stepper button:hover { background: rgb(var(--tint) / .1); color: var(--fg-on-tint); }
.stepper button:active { background: rgba(99, 102, 241, .3); }
.stepper button svg { width: .8rem; height: .8rem; }

/* Ползунок в духе Linear: тонкая дорожка, акцентная заливка, кольцо под бегунком.
   --fill (0%..100%) ставит pricing.js.
   Заливка сделана ВТОРЫМ слоем фона, а не стопом градиента: стопы
   линейного градиента обязаны идти по возрастанию, поэтому стоп на
   var(--fill) после фиксированных 0%/55% подтягивался вперёд — и
   дорожка обрывалась на 55% при любом значении ползунка. Слой с
   background-size --fill рисует заливку ровно по ширине заполнения. */
.range {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 20px; background: transparent; outline: none; cursor: pointer;
}
/* Акцент заливки фиксированный (индиго → фиолетовый). Раньше дорожка
   красилась в цвета игры (--g1/--g2), из-за чего ползунки не совпадали
   с кнопкой «Оплатить и запустить». */
.range::-webkit-slider-runnable-track {
  height: 6px; border-radius: 999px;
  background:
    linear-gradient(90deg, #6366f1, #8b5cf6) no-repeat 0 0 / var(--fill, 0%) 100%,
    rgb(var(--tint) / .09);
}
.range::-moz-range-track {
  height: 6px; border-radius: 999px;
  background:
    linear-gradient(90deg, #6366f1, #8b5cf6) no-repeat 0 0 / var(--fill, 0%) 100%,
    rgb(var(--tint) / .09);
}
/* Firefox рисует свою полосу прогресса — без этого она перекрывает слой
   заливки серым и заполнение снова не совпадает с бегунком. */
.range::-moz-range-progress {
  height: 6px; border-radius: 999px; background: linear-gradient(90deg, #6366f1, #8b5cf6);
}
.range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  /* отступ выравнивает круг по центру дорожки: (6 - 18) / 2 */
  width: 18px; height: 18px; margin-top: -6px; border-radius: 50%;
  background: #fff; border: none; cursor: grab;
  box-shadow: 0 0 0 3px rgba(99, 102, 241, .28),
              0 2px 6px -2px rgba(0, 0, 0, .8);
  transition: box-shadow .2s var(--ease), transform .14s var(--ease);
}
.range::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%; border: none; background: #fff; cursor: grab;
  box-shadow: 0 0 0 3px rgba(99, 102, 241, .28),
              0 2px 6px -2px rgba(0, 0, 0, .8);
}
.range:hover::-webkit-slider-thumb,
.range:hover::-moz-range-thumb {
  transform: scale(1.08);
  box-shadow: 0 0 0 4px rgba(99, 102, 241, .4), 0 0 14px -4px rgba(139, 92, 246, .75);
}
.range:active::-webkit-slider-thumb,
.range:active::-moz-range-thumb { cursor: grabbing; transform: scale(1.04); }
.range:focus-visible::-webkit-slider-thumb,
.range:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px rgba(99, 102, 241, .6), 0 0 0 6px rgba(99, 102, 241, .2);
}

/* --------------------------------------------------- период аренды
   Сегментированный переключатель: срок + процент скидки в одной кнопке. */
/* Тарификация: час / сутки / месяц в одну линию.
   Раньше здесь был переключатель срока (1 сутки … 12 мес) — пять кнопок.
   Теперь срока нет, а показать надо три единицы, поэтому это не кнопки,
   а статичные ячейки: выбирать нечего, только показать, по чему считает
   биллинг. Числа подставляет pricing.js из ответа /api/quote. */
.tariff-row {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .3rem;
  padding: .25rem; border-radius: .65rem;
  background: rgb(var(--tint) / .035); border: 1px solid var(--line);
}
.tariff-row__cell {
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  padding: .45rem .3rem; border-radius: .45rem; text-align: center;
  color: var(--ink-dim);
}
.tariff-row__cell b {
  color: var(--fg-on-tint); font-size: .95rem; font-weight: 700;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.tariff-row__cell span { font-size: .65rem; }
/* Первая ячейка — почасовая, она главная: именно столько человек видит
   в прайсе хостингов, и именно по ней сравнивают между собой. */
.tariff-row__cell:first-child b { color: #a7f3d0; }
@media (max-width: 420px) {
  .tariff-row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .15rem; }
  .tariff-row__cell b { font-size: .8rem; }
  .tariff-row__cell span { font-size: .6rem; }
}

/* Баланс и «на сколько хватит».
   Не украшение: блок отвечает на вопрос, который человек задаёт перед
   оплатой, — хватит ли денег. Поэтому при нехватке он не прячется, а
   краснеет: заблокировать кнопку молча означало бы «почему не работает». */
.balance-box {
  padding: .8rem .9rem; border-radius: .7rem;
  background: rgb(var(--tint) / .035); border: 1px solid var(--line);
}
.balance-box[hidden] { display: none; }
.balance-box__row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; font-size: .78rem; padding: .12rem 0;
}
.balance-box__key { color: var(--ink-dim); }
.balance-box__val { color: var(--fg-on-tint); font-variant-numeric: tabular-nums; }
.balance-box__runway {
  margin-top: .5rem; padding-top: .5rem; font-size: .72rem; line-height: 1.45;
  color: var(--ink-dim); border-top: 1px solid var(--line);
}
.balance-box.is-short {
  background: rgb(239 68 68 / .09); border-color: rgb(239 68 68 / .35);
}
.balance-box.is-short .balance-box__runway { color: #fca5a5; }
.balance-box.is-short .balance-box__runway b { color: #fecaca; }

/* Плашка экономии: появляется только когда скидка реально есть */
.sum-saving {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem .6rem; border-radius: .45rem;
  background: rgba(52, 211, 153, .12); border: 1px solid rgba(52, 211, 153, .3);
  color: #6ee7b7; font-size: .78rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
  animation: pop .34s var(--ease);
}
.sum-saving.hidden { display: none; }

/* Итог сводки. Раньше это был Tailwind-блок с text-4xl, и при скидке
   рядом стояла только строка «Промокод −20%» в разбивке — по итогу было
   не видно, что он меньше. Теперь зачёркнутая старая цена, крупный итог и
   плашка со скидкой, ровно как на витрине игр.

   Итог при скидке крупнее на ступень: зачёркнутое отодвигается, а платить
   надо то, что стоит справа, и оно должно читаться первым. */
.sum-total-row {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: .35rem .6rem; margin-top: .35rem;
}
.sum-total {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 2.25rem; font-weight: 700; letter-spacing: -.02em;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.sum-total-row.is-off .sum-total { font-size: 2.75rem; }
/* Зачёркнутая цена — кеглем и цветом как .hero-game__was на витрине,
   включая собственную черту: одна и та же привычка на всей странице. */
.sum-off-was {
  position: relative;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 1.05rem; font-weight: 600;
  color: var(--ink-dim); font-variant-numeric: tabular-nums;
}
.sum-off-was[hidden] { display: none; }
.sum-off-was::after {
  content: ""; position: absolute;
  left: -.06em; right: -.06em; top: 54%;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg,
    rgb(248 113 113 / .55), rgb(248 113 113 / .95));
  transform-origin: left center;
  animation: was-strike .42s .1s var(--ease) both;
}
/* Плашка скидки. Фиолетовым по фиолетовому не читалось: замерял 3.60 в
   тёмной и 3.30 в светлой теме при норме 4.5. В тёмной берём светлый
   фиолетовый на тёмной подложке, в светлой — тёмный на светлой: тот же
   тон, но с разбором по светлоте, как и с зелёной меткой пробы. */
.sum-off-badge {
  padding: .18rem .45rem; border-radius: .35rem;
  font-size: .72rem; font-weight: 700; letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  color: #a5b4fc;
  background: rgb(var(--brand) / .16);
  border: 1px solid rgb(var(--brand) / .34);
  animation: wiz-in .34s .28s var(--ease) both;
}
html[data-theme="light"] .sum-off-badge {
  color: #4338ca;
  background: rgb(var(--brand) / .12);
  border-color: rgb(var(--brand) / .3);
}
/* Проба: та же плашка, но зелёная — она про «бесплатно», а не про
   промокод. Фиолетовый на карточке игры означает «−20% по промокоду»,
   зелёный — «проба бесплатно», и путать их незачем. */
.sum-off-badge.is-trial {
  color: #34d399;
  background: rgb(52 211 153 / .14);
  border-color: rgb(52 211 153 / .34);
}
/* Зелёный в светлой теме — #065f46, а не #047857: плашка лежит на тинтованной
   подложке, а не на белой (как на карточке игры), и emerald-700 давал там
   4.15 при норме 4.5. Заодно подложка светлее — 10% вместо 12%. */
html[data-theme="light"] .sum-off-badge.is-trial {
  color: #065f46;
  background: rgb(5 150 105 / .1);
  border-color: rgb(5 150 105 / .28);
}
.sum-off-badge[hidden] { display: none; }

/* Розовая плашка «БЫЛО» (.sum-was) удалена вместе с разметкой: проба
   теперь оформлена так же, как промокод, через sum-off-was и
   sum-off-badge, а различаются только цветом метки и её текстом. */
/* ------------------------------------------- выбор стартовой карты
   Мини-карточки сеткой + ручной ввод. Акцент — фиолетово-синий,
   чтобы читалось как часть конструктора, а не как чужой блок. */
.map-toggle {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .2rem .5rem; border-radius: .4rem; cursor: pointer;
  font-size: .72rem; font-weight: 500; color: var(--ink-dim);
  background: transparent; border: 1px solid var(--line-soft);
  transition: all .18s var(--ease);
}
.map-toggle:hover { color: var(--fg-on-tint); border-color: var(--line-firm); }
.map-toggle:focus-visible { outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 2px; }
.map-toggle.is-active {
  color: var(--fg-on-tint); background: rgba(99, 102, 241, .18);
  border-color: rgba(129, 140, 248, .5);
}

.map-grid {
  display: grid; gap: .5rem; grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 767px) { .map-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 420px) { .map-grid { grid-template-columns: 1fr; } }

.map-card {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: .15rem; text-align: left;
  padding: .55rem .65rem; border-radius: .6rem; cursor: pointer;
  background: rgb(var(--tint) / .03); border: 1px solid var(--line);
  transition: border-color .2s var(--ease), background .2s var(--ease),
              box-shadow .2s var(--ease), transform .18s var(--ease);
}
.map-card:hover { border-color: rgba(129, 140, 248, .5); transform: translateY(-1px); }
.map-card:focus-visible { outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 2px; }
.map-card.is-active {
  background: linear-gradient(180deg, rgba(99, 102, 241, .22), rgba(99, 102, 241, .06));
  border-color: rgba(129, 140, 248, .65);
  box-shadow: 0 0 18px -7px rgba(99, 102, 241, .9);
}
/* Текст вынесен в обёртку: подложка кладётся под него отдельным слоем.
   Под самим текстом — скрим, иначе подписи лежат прямо на снимке карты.
   Замерял по пикселям: снимок светлый (небо, бетон, трава), и id карты
   с подписью давали 2.3–3.4 в тёмной теме и 2.6–3.4 в светлой при норме
   4.5. Затухание снимка слева не помогало: маска гасит его у левого края,
   а текст кончается правее, где снимок ещё вовсе. Скрим идёт под текстом и
   растворяется у его правого края, где снимок и должен быть виден. */
/* ---- подложка-кадр карты ----------------------------------------------
   Кадр кладётся слоем справа и гаснет к тексту слева, подписи лежат над
   ним своим слоем. Размер карточки не меняется — она остаётся в одной
   колонке сетки, как соседние.

   Подложка одна — у gm_construct, на который прислали кадр; у остальных
   карт поля thumb нет. */
.map-card__bg {
  position: absolute; z-index: 0;
  top: 0; bottom: 0; left: 18%; right: 0;
  background-position: 50% 42%; background-size: cover;
  opacity: .55;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, .4) 46%, #000 88%),
                      linear-gradient(180deg, transparent 0%, #000 26%);
  mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, .4) 46%, #000 88%),
              linear-gradient(180deg, transparent 0%, #000 26%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  transition: opacity .28s var(--ease);
}

/* Прозрачность кадра различается по темам, и разница не декоративная.

   В светлой теме снимок раньше гасили до .2: он и сам светлый (небо,
   бетон, трава), и лежал под белой карточкой, из-за чего подписи пропадали.
   Но гасить нужно было не снимок, а подложку под текстом, и в итоге
   карточка читалась как пустая белая плашка с серым текстом — правая
   половина выглядела недоделанной. Снимок поднят до .55, а скрим под
   подписями теперь непрозрачен на всю их ширину, так что текст никуда не
   делся, а картинка вернулась.

   В тёмной теме .36: на почти чёрном фоне снимок при большей прозрачности
   начинает спорить с фоном и подписями. */
html[data-theme="dark"] .map-card__bg { opacity: .36; }
html[data-theme="dark"] .map-card.is-mapped:hover .map-card__bg { opacity: .5; }
html[data-theme="dark"] .map-card.is-mapped.is-active .map-card__bg { opacity: .58; }
html[data-theme="light"] .map-card.is-mapped:hover .map-card__bg { opacity: .68; }
html[data-theme="light"] .map-card.is-mapped.is-active .map-card__bg { opacity: .76; }

/* Скрима под подписями нет — по просьбе. Раньше здесь лежал горизонтальный
   градиент от непрозрачного --bg-card слева к полной прозрачности справа:
   он и прятал половину снимка (в светлой теме карточка выглядела пустой
   белой плашкой), и давал вертикальную границу в правом нижнем углу,
   где обёртка заканчивалась вместе с последней строкой. Цена вопроса —
   подписи теперь лежат прямо на снимке; чем это грозит, написано у
   .map-card__name. */
.map-card__body {
  position: relative; z-index: 1;
  /* Растягивается на всю высоту карточки: при обёртке по содержимому
     она занимала только верхнюю часть, и кадр под последней строкой
     обрывался по её нижней кромке. */
  flex: 1;
  display: flex; flex-direction: column; gap: .15rem;
}

.map-card.is-mapped .map-card__body {
  position: relative; z-index: 1;
  padding-right: 44%;
}

.map-card__name {
  display: flex; align-items: center; justify-content: space-between; gap: .4rem;
  font-size: .8rem; font-weight: 600; color: var(--ink);
}
.map-card.is-active .map-card__name { color: var(--ink); }
.map-card__id {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .65rem; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.map-card.is-active .map-card__id { color: var(--fg-accent); }
.map-card__hint { font-size: .68rem; line-height: 1.35; color: var(--ink-faint); }

/* Галочка активной карты: всегда в разметке, но скрыта, пока карта не выбрана */
.map-card__tick {
  flex: none; color: var(--g1); opacity: 0; scale: .6;
  transition: opacity .2s var(--ease), scale .2s var(--ease);
}
.map-card.is-active .map-card__tick { opacity: 1; scale: 1; }
.map-card.is-active .map-card__hint { color: var(--fg-accent); }

/* Линейка границ под ползунком: видно, сколько стоит добавление ресурсов.
   Цвет не темнее slate-500 — подписи обязаны читаться на тёмном фоне. */
/* Подписи «min — max» под ползунком.
   Внутренний отступ 9px = половина бегунка (18px): у <input type=range>
   полезный ход бегунка короче дорожки ровно на его радиус, поэтому подписи
   без отступа уезжали за края дорожки и упирались в край карточки. С таким
   отступом «4» и «64 сл.» встают точно под концами дорожки. */
.ctrl__scale {
  display: flex; justify-content: space-between;
  padding: 0 9px; gap: .75rem;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .65rem; color: var(--ink-faint);
  /* подписи не должны рваться на две строки: «32 ГБ» превращалось в «32» + «ГБ» */
  white-space: nowrap;
}
.ctrl__scale span { min-width: 0; }
.ctrl__scale span:last-child { text-align: right; }

/* ==========================================================================
   8. СТАТУСЫ, БЕЙДЖИ, МЕТРИКИ
   ========================================================================== */
.chip {
  display: inline-flex; align-items: center; gap: .35rem; padding: .18rem .5rem;
  border-radius: .35rem; font-size: .72rem; font-weight: 500;
  background: rgb(var(--tint) / .04); border: 1px solid var(--line-soft); color: var(--ink-dim);
}
.badge {
  display: inline-flex; align-items: center; gap: .4rem; padding: .2rem .55rem;
  border-radius: 999px; font-size: .7rem; font-weight: 600; letter-spacing: .01em;
  white-space: nowrap;
}
.badge-online {
  background: rgba(16, 185, 129, .12); color: #6ee7b7; border: 1px solid rgba(52, 211, 153, .3);
  box-shadow: 0 0 18px -6px rgba(52, 211, 153, .9);
}
.badge-offline { background: rgba(148, 163, 184, .08); color: #94a3b8; border: 1px solid rgba(148, 163, 184, .22); }
.badge-starting {
  background: rgba(234, 179, 8, .12); color: #fcd34d; border: 1px solid rgba(250, 204, 21, .3);
  box-shadow: 0 0 18px -6px rgba(250, 204, 21, .8);
}
.badge-stopping { background: rgba(249, 115, 22, .12); color: #fdba74; border: 1px solid rgba(251, 146, 60, .3); }
/* Установка: фиолетовый платформы и медленное свечение. Отличается от
   «запускается» жёлтым намеренно — установка длится минутами, и человек
   должен понимать, что ждать долго, а не что что-то сломалось. */
.badge-installing {
  background: rgb(var(--brand) / .14); color: var(--brand-400); border: 1px solid rgb(var(--brand) / .38);
  box-shadow: 0 0 18px -6px rgb(var(--brand) / .9);
}
.badge-restarting {
  background: rgba(251, 191, 36, .12); color: #fcd34d; border: 1px solid rgba(251, 191, 36, .32);
}
.badge-error, .badge-crashed, .badge-failed {
  background: rgba(239, 68, 68, .12); color: #fca5a5; border: 1px solid rgba(248, 113, 113, .3);
  box-shadow: 0 0 18px -6px rgba(248, 113, 113, .8);
}
.badge-paid { background: rgba(16, 185, 129, .12); color: #6ee7b7; border: 1px solid rgba(52, 211, 153, .3); }
.badge-pending { background: rgba(234, 179, 8, .12); color: #fcd34d; border: 1px solid rgba(250, 204, 21, .3); }
/* Отменённый заказ. Правила не было, а статус в схеме БД есть
   (pending|paid|failed|canceled): бейдж рендерился с прозрачным фоном и
   серым текстом и выглядел как сломанный, а не как статус. */
.badge-canceled { background: rgba(148, 163, 184, .1); color: #cbd5e1; border: 1px solid rgba(148, 163, 184, .26); }
/* Пробный заказ: помечает строку истории, где деньги не снимались. Тон
   тот же, что у оплаченного, — знак один, но подпись своя: «оплачен» про
   бесплатный сервер читалось бы как обман. */
.badge-trial { background: rgba(16, 185, 129, .12); color: #6ee7b7; border: 1px solid rgba(52, 211, 153, .3); }

/* ==========================================================================
   9. ЗАКАЗЫ: карточка, чипсы характеристик, цена, действия
   ========================================================================== */

/* Карточка заказа и карточка сервера на кабинете. Отдельное правило, а не
   .glass, потому что требование к рамке — slate-800/60, то есть заметно
   плотнее базовой --line (white/10), и тень должна быть мягче и глубже:
   карточка заказа — это большой блок с текстом, а не плашка. Значение
   slate-800/60 переписано числами: 30 41 59 — это тот же slate-800. */
.order-card {
  position: relative; overflow: hidden;
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgb(var(--tint) / .04), rgb(var(--tint) / .014));
  background-color: var(--bg-raised);
  border: 1px solid rgb(30 41 59 / .6);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  box-shadow: 0 18px 40px -30px rgb(0 0 0 / .9), inset 0 1px 0 rgb(var(--tint) / .05);
  transition: border-color .22s var(--ease), box-shadow .22s var(--ease),
              transform .22s var(--ease);
}
.order-card:hover {
  border-color: rgb(51 65 85 / .85);
  box-shadow: 0 26px 52px -28px rgb(0 0 0 / .95), inset 0 1px 0 rgb(var(--tint) / .07);
  transform: translateY(-1px);
}
/* В светлой теме та же рамка почти сливается с фоном, поэтому там своя. */
html[data-theme="light"] .order-card {
  border-color: rgba(15, 23, 42, .12);
  box-shadow: 0 18px 40px -32px rgb(15 23 42 / .35);
}
html[data-theme="light"] .order-card:hover { border-color: rgba(15, 23, 42, .22); }

/* Технические характеристики — одна строка мелких чипов под названием.
   Раньше это был ряд текстовых подписей с иконками, который переносился
   на вторую строку и ломал вертикальный ритм карточки: высота плавала от
   заказа к заказу. Здесь строка не переносится принципиально — на узком
   экране чипсы прокручиваются по горизонтали (полоса скрыта), и карточка
   всегда одной высоты. */
.specs {
  display: flex; align-items: center; gap: .375rem;
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none; -ms-overflow-style: none;
  /* у flex-контейнера flex-shrink по умолнию равен 1, и чипсы сжимались бы
     в узкую колонку вместо того, чтобы уйти в прокрутку */
  padding-bottom: .1rem;
}
.specs::-webkit-scrollbar { display: none; }
.spec-chip {
  display: inline-flex; align-items: center; gap: .3rem; flex: none;
  padding: .2rem .55rem; border-radius: 999px;
  font-size: .7rem; font-weight: 500; line-height: 1.4; white-space: nowrap;
  background: rgb(var(--tint) / .045);
  border: 1px solid var(--line-soft);
  color: var(--ink-dim);
  transition: background .18s var(--ease), border-color .18s var(--ease),
              color .18s var(--ease), transform .18s var(--ease);
}
.spec-chip svg { width: .8rem; height: .8rem; flex: none; opacity: .7; }
/* Значение — цифры, они должны читаться первыми: чуть светлее подписи
   и с табличными цифрами, иначе колонка чипсов «прыгает» при смене заказа. */
.spec-chip b {
  color: var(--ink); font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: .01em;
}
/* Чип с названием игры: фирменный фиолетовый, как и подпись стартовой
   карточки. Цветом игры подписывались все чипы подряд, и карточка заказа
   выглядела пёстрой — игра опознаётся по знаку и по названию, а не по
   оттенку подписи. */
.spec-chip--game { color: var(--fg-accent); background: rgb(var(--tint) / .06); }
.spec-chip--game svg { opacity: .9; }
.spec-chip--game b { color: inherit; }
/* Адрес — чип другого характера: его копируют, поэтому он реагирует
   и подсвечивается, и не должен выглядеть частью характеристик. */
.spec-chip--addr {
  font-family: ui-monospace, 'JetBrains Mono', monospace;
  color: #a5f3ea; background: rgba(45, 212, 191, .1);
  border-color: rgba(45, 212, 191, .28); cursor: pointer;
}
.spec-chip--addr:hover { background: rgba(45, 212, 191, .2); border-color: rgba(45, 212, 191, .5); color: #d5fffa; }
.spec-chip--addr b { color: inherit; }

/* Цена: главный ответ карточки, поэтому крупнее всего остального текста
   и с табличными цифрами — суммы выравниваются между заказами.
   Белая — на тёмной карточке; в светлой теме карточка светлеет, и цвет
   переопределён в блоке темы. */
.order-price__value {
  font-size: 1.75rem; font-weight: 800; line-height: 1.05; color: #fff;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.order-price__period {
  margin-top: .25rem; font-size: .72rem; font-weight: 500; color: var(--ink-faint);
}

/* Действия — одна строка внизу карточки. Отдельный блок, а не просто flex:
   рамка над ним и подсветка при наведении на карточку связывают кнопки с
   заказом, а не оставляют их «прибитыми» к низу стеклянной панели. */
.order-actions {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-top: 1.15rem; padding-top: .95rem;
  border-top: 1px solid var(--line-soft);
  transition: border-color .22s var(--ease);
}
.order-card:hover .order-actions { border-top-color: var(--line); }
/* Кнопки уезжают вправо от описания: главное действие — оплата или
   панель управления, и глаз должен приземляться в правый нижний угол. */
.order-actions__spacer { flex: 1 1 auto; min-width: 0; }
@media (prefers-reduced-motion: reduce) {
  .order-card, .order-card:hover, .spec-chip, .order-actions { transition: none; transform: none; }
}

.dot { width: .5rem; height: .5rem; border-radius: 999px; position: relative; flex: none; }
.dot-online  { background: #34d399; box-shadow: 0 0 10px #34d399; }
.dot-online::after {
  content: ""; position: absolute; inset: 0; border-radius: 999px; background: #34d399;
  animation: pulsering 1.8s ease-out infinite;
}
.dot-offline { background: #64748b; }
.dot-installing { background: var(--brand-400); box-shadow: 0 0 10px rgb(var(--brand) / .8); }
.dot-installing::after {
  content: ""; position: absolute; inset: 0; border-radius: 999px;
  background: var(--brand-400); animation: pulsering 2.6s ease-out infinite;
}
.dot-error   { background: #f87171; box-shadow: 0 0 10px rgba(248, 113, 113, .8); }
/* Перезапуск: янтарный и пульсирует. Не красный — демон ещё пытается,
   и паниковать рано; но и не зелёный — сервер не обслуживает игроков. */
.dot-restarting { background: #fbbf24; box-shadow: 0 0 10px rgba(251, 191, 36, .8); }
.dot-restarting::after {
  content: ''; position: absolute; inset: 0; border-radius: 999px;
  background: #fbbf24; opacity: .55; animation: dot-pulse 1.4s ease-out infinite;
}
@keyframes dot-pulse {
  from { transform: scale(1); opacity: .55; }
  to   { transform: scale(2.6); opacity: 0; }
}
@keyframes pulsering {
  0%   { transform: scale(.7); opacity: .85; }
  100% { transform: scale(2.4); opacity: 0; }
}

/* Прогресс-бар с градиентом; .warn/.crit переключают палитру */
.meter {
  height: 6px; border-radius: 999px; background: rgb(var(--tint) / .07);
  overflow: hidden; box-shadow: inset 0 0 0 1px rgb(var(--tint) / .04);
}
.meter > i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #6366f1, #22d3ee);
  box-shadow: 0 0 12px -2px rgba(99, 102, 241, .9);
  transition: width .6s var(--ease);
}
.meter.warn > i { background: linear-gradient(90deg, #f59e0b, #f97316); box-shadow: 0 0 12px -2px rgba(249, 115, 22, .9); }
.meter.crit > i { background: linear-gradient(90deg, #ef4444, #f43f5e); box-shadow: 0 0 12px -2px rgba(244, 63, 94, .9); }
/* Тонкая полоса: в плитках метрик шапки сервера полоса в 6px занимала
   половину высоты плитки, а значения там меняются медленно. */
.meter--thin { height: 3px; }
.meter--thin > i { box-shadow: none; }

/* --------------------------------------------------- ПЛИТКА МЕТРИКИ
   Пять значений в один ряд над контентом сервера. Подпись, значение,
   полоса и пояснение умещаются в четыре строки по .6rem — плитка в
   полтора раза ниже прежней и читается как строка приборов, а не как
   пять отдельных карточек. */
.srv-stat {
  display: flex; flex-direction: column; gap: .18rem;
  padding: .5rem .7rem .55rem;
  border: 1px solid var(--line-soft); border-radius: .65rem;
  background: rgb(var(--tint) / .025);
}
.srv-stat__head {
  display: flex; align-items: center; gap: .35rem;
  font-size: .65rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-faint);
}
.srv-stat__head svg { width: .75rem; height: .75rem; }
.srv-stat__value {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 1rem; font-weight: 700; line-height: 1.2; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.srv-stat__note { font-size: .64rem; color: var(--ink-faint); }

/* Метрики сервера одним блоком — по тем же правилам, что и .stat-band на
   кабинете: одна рамка на всю полосу, ячейки делит тонкая линия, а не
   собственные рамки. Собственная рамка у ячейки вернула бы ровно то, от
   чего ушли, — набор отдельных плиток. */
.srv-band {
  display: grid; grid-template-columns: 1fr;
  border: 1px solid var(--line-soft); border-radius: .8rem;
  background: rgb(var(--tint) / .025);
  overflow: hidden;
}
.srv-band > .srv-stat {
  border: 0; border-radius: 0; background: none;
  padding: .6rem .85rem .65rem;
}
.srv-band > .srv-stat + .srv-stat { border-top: 1px solid var(--line-soft); }
@media (min-width: 640px) {
  .srv-band { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* Три метрики в одну строку: разделители только вертикальные, иначе
     горизонтальные черты у второй и третьей ячеек разрезали бы блок. */
  .srv-band > .srv-stat + .srv-stat { border-top: 0; }
  .srv-band > .srv-stat:not(:first-child) { border-left: 1px solid var(--line-soft); }
}

/* =====================================================================
   Круговые метрики (CPU / RAM / Uptime)
   ---------------------------------------------------------------------
   Заменяют плоские полоски: у круга есть и длина дуги, и толщина, и цвет,
   поэтому состояние читается боковым зрением — по толщине и цвету — даже
   не глядя на цифру.

   Шкала построена на SVG: circumference = 2πr. Анимация идёт через
   transition по stroke-dashoffset, поэтому при смене значения дуга
   «перетекает», а не прыгает. Никакого requestAnimationFrame и никакого
   canvas — обновление дергает только один атрибут на элементе.
   ===================================================================== */
/* Три кольца — ОДИН блок. Раньше каждое было отдельной карточкой со своей
   рамкой, и рядом они читались как три конкурирующие плитки, а не как одна
   шкала одного сервера. Теперь рамка одна — на контейнере, а кольца разделены
   тонкими вертикальными чертами, как ячейки таблицы. */
.srv-rings {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  /* gap: 0 — иначе между кольцами остаются пустые полосы шириной в
     пол-rem, сквозь которые видно фиолетово-голубое свечение панели.
     Читались они как «непонятные полосы»: на странице не было ни одного
     элемента, который их рисует, хотя на экране они были отчётливо
     видны. Ячейки соприкасаются, а разделяет их волосок border-left. */
  gap: 0;
  margin-top: 1rem;
  border: 1px solid var(--block-line);
  border-radius: var(--radius);
  background: var(--block-bg);
  backdrop-filter: var(--block-blur);
  -webkit-backdrop-filter: var(--block-blur);
  box-shadow: var(--block-shadow), var(--block-sheen);
  overflow: hidden;
}

.gauge {
  --ring-c1: #38bdf8;
  --ring-c2: #818cf8;
  --ring-glow: rgba(56, 189, 248, .62);
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: .35rem;
  padding: .9rem .5rem .8rem;
  transition: background .25s ease;
  /* Имя класса переименовано из .ring: Tailwind CDN генерирует утилиту
     .ring как box-shadow 0 0 0 3px rgb(59 130 246 / .5) и подставляет её
     в конец документа, то есть перебивала наши стили. Каждая метрика
     обводилась синим кольцом в 3px — по краям ячеек это и читалось как
     «непонятные полосы». Явно гасим тень, даже если утилита вернётся. */
  box-shadow: none;
}
/* Разделителей между метриками нет намеренно: даже тонкая черта
   --line-soft на тёмной теме читалась как синяя перегородка и делила
   единый блок обратно на три. Метрики разделяет воздух: gap контейнера
   и горизонтальные отступы внутри ячеек. */

/* На hover подсвечивается ячейка, а не обводится рамкой: цвет рамки тянул
   на себя внимание и спорил с цветом дуги, который и есть смысл кольца. */
.gauge:hover { background: rgb(var(--tint) / .05); }

.gauge__wrap { position: relative; width: 86px; height: 86px; }
.gauge__wrap svg { width: 100%; height: 100%; transform: rotate(-90deg); overflow: visible; }

.gauge__track {
  fill: none;
  /* color-mix, а НЕ rgb(var(--ink-faint) / .16): --ink-faint — это HEX
     (#78829a), и подставить его внутрь rgb() нельзя. Такое объявление
     не ошибка парсера, а просто выброс: браузер отбрасывал всё правило
     и рисовал окружность без обводки, то есть кольцо выглядело как
     мазок вместо шкалы. Работает только с triplet-токенами вроде
     --tint (255 255 255) — оттого rgb(var(--tint) / …) и был живым. */
  stroke: color-mix(in srgb, var(--ink-faint) 16%, transparent);
  stroke-width: 9;
}
.gauge__arc {
  fill: none;
  /* stroke НЕ задаётся здесь: у каждого кольца свой градиент
     (#ringGrad-cpu / -ram / -uptime), и он приходит атрибутом в разметке.
     Общий url(#ringGrad) в CSS перебивал бы атрибут и указывал на
     несуществующий id — дуга красилась бы в никуда и кольцо выглядело
     пустым при любой загрузке. */
  stroke-width: 9;
  stroke-linecap: round;
  /* перетекание значения вместо прыжка */
  transition: stroke-dashoffset .7s cubic-bezier(.22,1,.36,1),
              stroke .4s ease;
  filter: drop-shadow(0 0 7px var(--ring-glow));
}

/* Пульсация только пока сервер онлайн и нагрузка высокая — иначе
   анимация мигает на ровной нагрузке и только отвлекает */
.gauge.is-live .gauge__arc { animation: ringPulse 2.4s ease-in-out infinite; }
@keyframes ringPulse {
  0%, 100% { filter: drop-shadow(0 0 5px var(--ring-glow)); opacity: 1; }
  50%      { filter: drop-shadow(0 0 13px var(--ring-glow)); opacity: .88; }
}

/* Значение в центре кольца */
.gauge__mid {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: .05rem; text-align: center; pointer-events: none;
}
.gauge__val {
  font-size: 1.02rem; font-weight: 700; line-height: 1.1;
  /* Токен темы, а не #fff: на светлой теме карточка белая, и белый текст
     на ней просто исчезал. */
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.gauge__unit { font-size: .58rem; color: var(--ink-faint); letter-spacing: .04em; }
/* Значение-предложение («2.9 / 11.7 ГБ») в кольцо 86px не вписывается
   в прежнем кегле: уменьшаем и разрешаем перенос по строке. */
.gauge__val--sm { font-size: .68rem; line-height: 1.2; max-width: 74px; }

.gauge__label {
  display: flex; align-items: center; gap: .3rem;
  font-size: .68rem; font-weight: 600; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: .06em;
}
.gauge__label svg { width: .72rem; height: .72rem; }
.gauge__note {
  font-size: .62rem; color: var(--ink-faint); text-align: center;
  line-height: 1.25;
}
.gauge__note b { color: color-mix(in srgb, var(--ink) 82%, transparent); font-weight: 600; }
.gauge__auto {
  display: inline-block; margin-top: .1rem;
  font-size: .58rem; color: var(--ink-faint);
  border: 1px solid var(--pill-line); border-radius: .3rem;
  background: var(--pill-bg);
  padding: 0 .25rem;
}

/* Узкий экран: три кольца в ряд не помещаются, они сжимаются в нечитаемые
   кружки с обрезанными подписями. Ниже 420px складываем в одну колонку и
   меняем вертикальный разделитель на горизонтальный — иначе он превратится
   в рамку по левому краю каждой нижней ячейки. */
@media (max-width: 420px) {
  .srv-rings { grid-template-columns: 1fr; }
  .gauge__wrap { width: 72px; height: 72px; }
}

/* Пороговые состояния: цвет дуги и мягкого свечения */
.gauge.is-warn { --ring-c1: #f59e0b; --ring-c2: #f97316; --ring-glow: rgba(249, 115, 22, .68); }
.gauge.is-crit { --ring-c1: #ef4444; --ring-c2: #f43f5e; --ring-glow: rgba(244, 63, 94, .72); }
.gauge.is-idle .gauge__arc { filter: none; opacity: .45; }

/* Доступность: уважаем «уменьшить движение» */
@media (prefers-reduced-motion: reduce) {
  .gauge, .gauge__arc { transition: none !important; }
  .gauge.is-live .gauge__arc { animation: none; }
  .gauge:hover { background: none; }
}

/* ---------- Светлая тема: кольца должны читаться на белом ----------
   Голубой #38bdf8 на белом даёт ~2:1 — для графики нужен 3:1 (WCAG
   1.4.11), и на белой карточке кольцо почти сливалось с фоном. Ниже —
   затемнённые варианты: контраст выше, насыщенность сохранена, и смысл
   состояния тот же (спокойный → янтарный → красный). */
html[data-theme="light"] .gauge {
  --ring-c1: #0369a1;
  --ring-c2: #4338ca;
  --ring-glow: rgba(3, 105, 161, .45);
}
html[data-theme="light"] .gauge.is-warn {
  --ring-c1: #b45309;
  --ring-c2: #c2410c;
  --ring-glow: rgba(180, 83, 9, .45);
}
html[data-theme="light"] .gauge.is-crit {
  --ring-c1: #be123c;
  --ring-c2: #b91c1c;
  --ring-glow: rgba(190, 18, 60, .45);
}
/* Дорожка на светлом чуть плотнее: при .16 кольцо-фон-как-подложка
   терялось, и заполненная дуга читалась как парящая без основания. */
html[data-theme="light"] .gauge__track { stroke: color-mix(in srgb, var(--ink-faint) 28%, transparent); }

/* ---------- Шапка кабинета: приветствие и баланс в одном блоке ----------
   Раньше это были две отдельные вещи: заголовок сам по себе и кнопка
   напротив через justify-between, а баланс жил в другом месте страницы
   (и в пустом состоянии — в отдельной полосе). Теперь это один блок с
   общей рамкой: приветствие слева, баланс сразу за ним, прижатые друг
   к другу, а кнопка отодвинута вправо через margin-left:auto.
   Прижимание сделано осознанно: space-between разводил подписи по краям,
   и на широком экране строка выглядела пустой серединой. */
.top-bar {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .85rem 1.1rem;
  margin-top: 1rem;
  padding: .9rem 1.15rem;
  /* Те же токены, что у .glass: раньше шапка красилась своим градиентом
     поверх непрозрачного --bg-raised и выглядела плотнее карточек. */
  border: 1px solid var(--block-line); border-radius: var(--radius);
  background: var(--block-bg);
  backdrop-filter: var(--block-blur);
  -webkit-backdrop-filter: var(--block-blur);
  box-shadow: var(--block-shadow), var(--block-sheen);
}
.top-bar__hello { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.top-bar__hi {
  font-size: 1.35rem; font-weight: 800; letter-spacing: -.015em;
  line-height: 1.15;
  /* Токен темы, а не #fff: на светлой теме белым по белому заголовок
     и имя пользователя просто исчезали. */
  color: var(--ink);
}
.top-bar__note { font-size: .72rem; color: var(--ink-dim); }
.top-bar__note b { color: #fcd34d; font-weight: 600; }

/* Баланс — у правого края блока. margin-left:auto прижимает его к краю,
   поэтому середина строки остаётся воздухом, а не пустотой: кнопка стоит
   сразу за приветствием и прижимается к нему, а не улетает на край. */
.top-bar__bal {
  display: inline-flex; align-items: center; gap: .45rem;
  margin-left: auto;
  padding: .45rem .8rem;
  border: 1px solid var(--pill-line); border-radius: 999px;
  background: var(--pill-bg);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  font-size: .8rem; color: var(--ink-dim);
  white-space: nowrap;
}
.top-bar__bal svg { width: .95rem; height: .95rem; color: var(--ink-faint); flex: none; }
.top-bar__bal b {
  color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums;
}
/* =====================================================================
   ОДИН ФОН ВМЕСТО БЛОКОВ — кабинет (.dash-surface)
   ---------------------------------------------------------------------
   Подложка одна на весь раздел, а внутренние блоки (.glass, шапка,
   полоса метрик, полоса пустого состояния) становятся прозрачными:
   своей рамки, подложки и тени у них больше нет.

   Почему так, а не «просто убрать разделители»: пять блоков подряд
   читались как пять несвязанных карточек, а одна поверхность с
   воздухом между разделами — как один экран. Правила намеренно
   ограничены .dash-surface: .glass используется и на странице сервера,
   и в биллинге, там блоки остаются на месте.
   ===================================================================== */
.dash-surface {
  border: 1px solid var(--block-line);
  border-radius: 1.25rem;
  background: var(--block-bg);
  backdrop-filter: var(--block-blur);
  -webkit-backdrop-filter: var(--block-blur);
  box-shadow: var(--block-shadow), var(--block-sheen);
}

/* Внутренние блоки на этой поверхности: только отступы, без рамок. */
.dash-surface .glass,
.dash-surface .top-bar,
.dash-surface .srv-rings,
.dash-surface .stat-bar {
  background: none;
  border: 0;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Подложка блока одна на весь раздел — и блоков нет вовсе, поэтому
   прежние отступы, рассчитанные на «плитку + внешний margin», местами
   удваивали пустоту. Оставляем один ритм: 2.5rem между разделами. */
.dash-surface > * + * { margin-top: 2.5rem; }
.dash-surface > .top-bar { margin-top: 0; }
.dash-surface > .fact-band { margin-top: .85rem; }
/* Мелкие плашки остаются плашками: баланс и факты под кольцами —
   это подписи, а не блоки, и без подложки они теряются в общей массе. */
.dash-surface .top-bar__bal,
.dash-surface .fact {
  background: var(--pill-bg);
  border: 1px solid var(--pill-line);
}
/* Таблицы: рамки нет — колонки держат градиентные разделители строк и
   подложка. Раньше здесь стояла своя рамка, но на фоне единой панели она
   читалась как вторая коробка внутри коробки. Мягкая подложка остаётся:
   без неё область таблицы не отличается от пустого места. */
.dash-surface .table-wrap {
  border: 0;
  border-radius: var(--radius);
  background: rgb(var(--tint) / .022);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: inset 0 0 0 1px rgb(var(--tint) / .035);
}
html[data-theme="light"] .dash-surface .table-wrap {
  background: rgb(255 255 255 / .40);
}

/* =====================================================================
   СТЕКЛО И СВЕТ: кабинет одним полотном
   ---------------------------------------------------------------------
   Разделы разведены не рамками, а светом: линия-градиент над заголовком,
   подсветка строки при наведении, мягкое свечение статусов. Панель при
   этом остаётся одной цельной подложкой — вложенных прямоугольников нет.
   ===================================================================== */
.dash-surface {
  position: relative;
  isolation: isolate;
  /* Без рамки и без скруглений: панель — это фон рабочей области, а не
     карточка. Любой радиус по краям читался как «здесь лежит блок» и
     отодвигал подложку от меню. */
  border: 0;
  border-radius: 0;
}

/* Мягкое свечение изнутри панели: два размытых пятна по углам дают
   ощущение объёма, которого не даёт плоская заливка. */
.dash-surface::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  background:
    radial-gradient(58% 42% at 12% 0%,
      rgb(var(--accent) / .13), transparent 70%),
    radial-gradient(48% 38% at 88% 6%,
      rgb(34 211 238 / .09), transparent 72%);
  pointer-events: none;
  z-index: -1;
}
/* Световая кромка: раньше псевдоэлемент обводил панель по всем четырём
   сторонам и читался рамкой — ровно то, чего здесь быть не должно.
   Теперь это только верхняя фаска (стекло ловит свет сверху) плюс
   тонкая линия у правого края, где панель упирается в окно. */
.dash-surface::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: inherit;
  background:
    linear-gradient(180deg, rgb(255 255 255 / .13), transparent 14%),
    linear-gradient(270deg, rgb(255 255 255 / .10), transparent 3%);
  pointer-events: none;
}
html[data-theme="light"] .dash-surface::after {
  background:
    linear-gradient(180deg, rgb(255 255 255 / .80), transparent 16%),
    linear-gradient(270deg, rgb(255 255 255 / .55), transparent 3%);
}

/* ---------- Разделитель разделов: линия света над заголовком ---------- */
.dash-surface .sec-head { position: relative; padding-top: 2rem; }
.dash-surface .sec-head::before {
  content: "";
  position: absolute; top: 0; left: -.25rem; right: -.25rem; height: 1px;
  background: linear-gradient(90deg,
    transparent,
    rgb(var(--accent) / .50) 18%,
    rgb(34 211 238 / .34) 55%,
    transparent);
  box-shadow: 0 0 14px rgb(99 102 241 / .40);
}

/* ---------- Строки таблиц: мягкие светящиеся разделители ---------- */
/* Рамка снизу у ячейки заменена градиентом, который гаснет по краям:
   линия идёт не от кромки до кромки, а растворяется — это и есть
   «свет», а не «граница». Данные остаются резкими: меняется только
   подложка строки. */
.dash-surface .rtable tbody td {
  border-bottom: 0;
  background-image: linear-gradient(90deg,
    transparent, rgb(var(--tint) / .14) 10%,
    rgb(var(--tint) / .14) 90%, transparent);
  background-size: 100% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
}
.dash-surface .rtable tbody tr:last-child td { background-image: none; }
.dash-surface .rtable thead th {
  border-bottom: 0;
  background-image: linear-gradient(90deg,
    transparent, rgb(var(--accent) / .40) 20%,
    rgb(34 211 238 / .28) 60%, transparent);
  background-size: 100% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
}
/* Подсветка строки: мягкое пятно фирменного цвета вместо серой заливки. */
.dash-surface .table-row:hover {
  background:
    linear-gradient(90deg,
      rgb(var(--accent) / .13), rgb(34 211 238 / .05) 55%, transparent);
  box-shadow: inset 2px 0 0 rgb(var(--accent) / .75);
}

/* ---------- Кольца: обычный разделитель ----------
   Между кольцами стояла светящаяся линия с градиентом, и она трижды
   читалась как «непонятная полоса»: сначала сплошной синий брусок
   (съедало его box-shadow), потом бледный, но всё равно голубой —
   на белой подложке он выделялся сильнее самих колец. Свет здесь не
   нужен: разделённые кольца и так читаются, а полоса лишь спорила с
   ними. Осталась обычная линия --line-soft — та же, что делит строки
   таблиц и отделяет разделы. Никакого градиента и свечения. */
.dash-surface .srv-rings { position: relative; }
.dash-surface .gauge + .gauge { border-left: 1px solid var(--line-soft); }
.dash-surface .gauge { position: relative; }

/* ---------- Статусы: мягкое свечение кромки вместо жёсткой рамки ---------- */
.dash-surface .badge {
  box-shadow: 0 0 0 1px rgb(var(--tint) / .04),
              0 0 14px -4px currentColor;
}
.dash-surface .badge-paid,
.dash-surface .badge-trial,
.dash-surface .badge-online {
  box-shadow: 0 0 0 1px rgba(52, 211, 153, .18),
              0 0 16px -6px rgba(52, 211, 153, .75);
}
.dash-surface .badge-pending {
  box-shadow: 0 0 0 1px rgba(250, 204, 21, .18),
              0 0 16px -6px rgba(250, 204, 21, .70);
}
.dash-surface .badge-offline {
  box-shadow: 0 0 0 1px rgb(var(--tint) / .05),
              0 0 14px -6px rgb(var(--tint) / .35);
}

/* ---------- Шапка и плашки: мягкая подсветка, без жёсткой рамки ---------- */
.dash-surface .top-bar__bal {
  box-shadow: 0 0 0 1px rgb(var(--tint) / .04),
              0 0 20px -10px rgb(34 211 238 / .55);
}
.dash-surface .fact {
  box-shadow: 0 0 18px -12px rgb(var(--tint) / .50);
}

/* ---------- Контейнер содержимого внутри панели ----------
   Панель — фон во всю ширину рабочей области, содержимое — колонка
   привычной ширины. Раньше панель и содержимоое были одним элементом,
   и таблицы растягивались до краёв окна. */
.dash-inner {
  width: 100%;
  max-width: 78rem;
  margin: 0 auto;
  padding: 0 2.25rem;
}
@media (max-width: 640px) {
  .dash-inner { padding: 0 1.15rem; }
}

/* ---------- Подвал продолжает ту же подложку ----------
   Подвал лежит вне .dash-surface и рисовал собственный тёмный фон, а
   между панелью и ним стоял mt-20: снизу страница обрывалась другой
   заливкой, и единого полотна не было. Здесь та же подложка, тот же
   blur, и промежуток убран — стык не виден.

   Правило через :has(), потому что .dash-surface есть только на
   кабинете: на странице сервера и в биллинге подвал остаётся прежним,
   там блоки живут свои. */
body:has(.dash-surface) #footer {
  margin-top: 0;
  border-top: 0;
  background: var(--block-bg);
  backdrop-filter: var(--block-blur);
  -webkit-backdrop-filter: var(--block-blur);
}
/* Отступы подвала выровнены по контейнеру содержимого. */
body:has(.dash-surface) #footer > div {
  max-width: 78rem;
  padding-left: 2.25rem;
  padding-right: 2.25rem;
  margin-left: auto;
  margin-right: auto;
}
@media (max-width: 640px) {
  body:has(.dash-surface) #footer > div { padding-left: 1.15rem; padding-right: 1.15rem; }
}
/* Нижняя кромка: подвал — низ всей страницы, обрывать её нечем. */
body:has(.dash-surface) .dash-surface::after {
  background: linear-gradient(180deg, rgb(255 255 255 / .13), transparent 14%);
}

/* ---------- Заголовок секции: название слева, действие справа ----------
   Общий вид для блоков, у которых есть заголовок и действие («Мои серверы»
   + «Создать сервер»). Кнопка прижата к правому краю тем же
   margin-left:auto, что и баланс в шапке. */
.sec-head {
  display: flex; align-items: center; gap: .75rem;
  margin-top: 1.75rem;
}
.sec-head__title {
  display: inline-flex; align-items: center; gap: .5rem;
  margin: 0;
  font-size: 1.15rem; font-weight: 700;
  color: var(--ink);  /* не #fff: на светлой теме заголовок исчезал */
}
.sec-head__icon { width: 1.05rem; height: 1.05rem; color: var(--ink-faint); }
.sec-head__cta { margin-left: auto; flex: none; }

/* Узкий экран: прижимать действие к краю бессмысленно — строки переносятся,
   и «правый край» оказывается за правым краем экрана. Тогда кнопка
   занимает строку целиком, а заголовок остаётся сверху. */
@media (max-width: 560px) {
  .sec-head { flex-wrap: wrap; }
  .sec-head__cta { margin-left: 0; width: 100%; justify-content: center; }
  .top-bar { align-items: stretch; flex-direction: column; }
  .top-bar__bal { margin-left: 0; align-self: flex-start; }
}

/* Там, где колец не ровно три (сводка дашборда: одна-три штуки),
   ширина колонок подстраивается сама, иначе единственное кольцо
   растягивалось бы на всю полосу. */
.srv-rings--auto {
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
}

/* Факт рядом с кольцами — короткая подпись без шкалы (число ядер, ОС).
   Кольцо для таких значений бессмысленно: у «4 ядра» нет максимума,
   до которого можно дотянуть дугу. */
.fact-band {
  display: flex; flex-wrap: wrap; gap: .4rem;
  margin-top: .6rem;
}
/* Полоса сводки по кластеру. Отбита сверху линией и своим отступом: она
   продолжает кольца, но относится к другой сущности (кластер, а не машина
   панели), и в общем ряду плашек разницы не видно. */
.fact-band--park {
  margin-top: 1rem; padding-top: .9rem;
  border-top: 1px solid var(--pill-line);
}
.fact--warn { color: #f59e0b; border-color: rgb(245 158 11 / .35); }
.fact--warn b { color: inherit; }
.fact {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .35rem .7rem;
  /* Плашка внутри блока: та же полупрозрачная подложка, но без тени —
     иначе факт под кольцами читался бы как ещё один самостоятельный блок. */
  border: 1px solid var(--pill-line); border-radius: 999px;
  background: var(--pill-bg);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  font-size: .7rem; color: var(--ink-faint);
}
.fact b { color: var(--ink); font-weight: 600; }
.fact svg { width: .8rem; height: .8rem; flex-shrink: 0; }

/* Sparkline: микрографик на canvas внутри ячейки метрики */
.spark {
  display: block; width: 100%; height: 2.25rem; margin-top: .4rem;
  border-radius: .3rem; opacity: .95;
}
/* Значение метрики + мини-график */
.metric { display: flex; flex-direction: column; min-width: 0; }
.metric__head { display: flex; align-items: center; gap: .35rem; font-size: .62rem; font-weight: 700;
                letter-spacing: .08em; text-transform: uppercase; color: var(--ink-faint); }
.metric__head svg { width: .75rem; height: .75rem; }
.metric__value {
  margin-top: .3rem; font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .9rem; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}

/* --------------------------------------------------- ФОРМЫ ВХОДА: знак
   Знаки страниц входа и регистрации — контуры из icons8, поставленные маской
   (тот же приём, что у знаков игр, .game-mark--ink). Подложку-плитку убрали:
   фиолетовый квадрат под знаком тяжелее самой формы и спорил с заголовком.

   Маской, а не <img>, потому что файл чёрный: на тёмной карточке он не
   читался бы совсем, а красить его белым значило бы сломать светлую тему.
   Маска берёт альфу, поэтому цвет пикселей файла не важен — фигуру красит
   background-color из --ink, тот же токен, что у текста. */
.auth-mark {
  display: block;
  width: 2.25rem; height: 2.25rem;
  background-color: var(--ink);
  -webkit-mask: var(--auth-mask) center / contain no-repeat;
  mask: var(--auth-mask) center / contain no-repeat;
}
.auth-mark--login    { --auth-mask: url("/static/img/icons/login.png"); }
.auth-mark--register { --auth-mask: url("/static/img/icons/user-plus.png"); }

/* --------------------------------------------------- СПИСОК СЕРВЕРОВ: таблица
   Одна таблица на весь список. До этого каждый сервер был отдельной
   плашкой: знак, название, статус и линия чипов (адрес, слоты, память,
   ядра, аптайм, срок) в одной строке, управление справа. Два сервера
   читались как две несвязанные карточки, и чтобы сравнить слоты или
   память, приходилось глазами перескакивать с одной строки на другую.
   Таблица превращает каждый параметр в колонку — значения выстраиваются
   вертикально, сравнение получается само собой.

   Метрик (полос CPU/RAM, микрографик) в списке нет: у остановленного
   сервера они всё равно нулевые, следить за нагрузкой уместно на странице
   самого сервера.

   Общие правила таблицы (.rtable, .table-wrap) — те же, что у истории
   заказов: на телефоне строки превращаются в карточки, а подпись колонки
   берётся из data-label ячейки. Здесь только то, что таблица серверов
   добавляет к ним. */
/* Девять колонок требуют чуть более плотных отступов, чем у истории
   заказов, и минимальной ширины: ниже неё колонки сжимаются до нечитаемых
   значений, поэтому прокручивается .table-wrap, а не ломается строка.

   Весь интерфейс нарисован в масштабе zoom .75, поэтому ноутбук на 1440
   физических пикселей даёт около 1216 CSS-пикселей под таблицу, и набор
   колонок впритык: ещё 40 пикселей лишнего — и последняя кнопка уезжает
   под обрез. Отсюда и плотные отступы, и значки без украшений. */
.srv-table { min-width: 830px; }
.srv-table thead th, .srv-table tbody td { padding-left: .5rem; padding-right: .5rem; }

.srv-cell {
  display: flex; align-items: center; gap: .65rem; min-width: 0;
  /* Потолок ширины обязателен: таблица живёт по схеме table-layout: auto и
     подстраивает колонки под содержимое. Одно длинное имя тогда растягивало
     колонку «Сервер» до 600 px, таблица уезжала за обёртку, и колонки
     справа (слоты, память, vCPU, аренда, действия) просто не попадали на
     экран — прокрутка вбок их доставала, но человек о них не узнавал.
     С ограничением truncate на заголовке наконец включается. */
  /* Потолок имени подобран так, чтобы таблица влезала в обёртку даже при
     самом длинном имени. Проверка на живых данных ловила: «Сервер»
     растёт от 268 px (короткое имя) почти до 320, и таблица выходила за
     правый край на 9 px — ровно настолько, чтобы появилась горизонтальная
     прокрутка. Запас нужен именно здесь, у колонки, которая растёт. */
  max-width: 18rem;
}
/* Узкий столбец имени на ноутбуке. Шесть колонок в 820-1023 px всё равно не
   помещались, и такие ширины отданы под карточную раскладку ниже, но между
   1024 и 1176 px таблица стоит впритык: без запаса последняя кнопка уезжала
   под обрез, как только имя сервера становилось длиннее. */
@media (max-width: 1279px) {
  .srv-cell { max-width: 15rem; }
}
.srv-cell__mark {
  display: grid; place-items: center; overflow: hidden; flex: none;
  width: 2.25rem; height: 2.25rem; border-radius: .6rem;
}
.srv-cell__body { min-width: 0; display: flex; flex-direction: column; gap: .15rem; }
.srv-cell__body h3 {
  font-size: .95rem; font-weight: 700; color: var(--ink); line-height: 1.25;
}

/* Числовые колонки: цифры с табличными разрядами, чтобы слоты и мегабайты
   стояли друг под другом, а не прыгали по ширине от строки к строке. */
.srv-table__num {
  text-align: right; white-space: nowrap;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.srv-table__num .srv-table__date {
  font-size: .8rem; color: var(--ink-dim); white-space: nowrap;
}
/* Единица тарифа мельче самой суммы: «14 ₽/сутки» — это цена, а
   «/сутки» её единица. На одной строке с датой она бы читалась как
   часть даты. */
.srv-table__dim { font-size: .68rem; color: var(--ink-dim); margin-left: .1rem; }

/* Расход и «хватит на» в заголовке секции серверов. display:block, потому
   что .stat-bar__note рассчитан на строку под балансом в шапке, а здесь
   он встаёт в строку между заголовком и кнопкой — иначе разорвал бы её. */
.sec-head__spend {
  display: block; margin: 0 auto 0 .5rem; text-align: right;
}

/* Управление: питание и переход в панель одним блоком у правого края.
   Питание отвечает на статус слева, поэтому стоит первым. */
.srv-table__ctl { white-space: nowrap; text-align: right; }
.srv-table__ctl > .srv-power { display: inline-flex; vertical-align: middle; margin-right: .3rem; }
/* Значок перехода в панель — квадратный, как и кнопки питания рядом. */
.srv-table__ctl > .btn-icon { display: inline-grid; vertical-align: middle; }

/* На телефоне .rtable превращает строку в карточку, и ячейки встают в
   «подпись — значение». Управлению и названию подпись не нужна: кнопки
   сами объясняют себя, а имя и знак и так стоят в шапке карточки.

   Здесь же снимается минимальная ширина таблицы: карточка занимает всю
   ширину экрана, и при оставшихся 830px значения уезжали за правый край
   вместе с горизонтальной прокруткой — на экране оставались только
   подписи колонок. */
@media (max-width: 767px) {
  .srv-table { min-width: 0; }
  .srv-table tbody td[data-label=""] { padding-top: .6rem; }
  .srv-table tbody td[data-label=""]:first-child { padding-bottom: .55rem; }
  .srv-table__num { text-align: right; }
  /* Селектор с tbody td: у .rtable выше специфичность, и правило
     space-between из карточки перебивало бы одиночный класс. */
  .rtable tbody td.srv-table__ctl { justify-content: flex-end; }
  .srv-table__row:last-child td[data-label=""] { padding-bottom: .6rem; }
}

/* Список серверов раскладывается в карточки намного раньше — уже с 1024 px.
   Причина в числах: при zoom .75 на 820 px под таблицей остаётся около
   766 зрительных пикселей, а шести колонок нужно 881. Таблица ужималась до
   предела, и «Аренда» с кнопками питания всё равно уезжали под обрез —
   человек проматывал карточку вправо и терял строку. Карточная раскладка
   даёт всё содержимое на экране и на узком ноутбуке, и на телефоне;
   шириной 768 px, где она включалась раньше, список оставался обрезанным.

   Правило адресное: таблица заказов помещается в этих ширинах и остаётся
   таблицей. */
@media (max-width: 1023px) {
  .srv-table, .srv-table tbody, .srv-table tr, .srv-table td { display: block; width: 100%; }
  .srv-table { min-width: 0; }
  .srv-table thead { display: none; }
  .srv-table tr {
    margin: 0 0 .6rem; padding: .35rem .2rem;
    background: rgb(var(--tint) / .022);
    border: 1px solid var(--line); border-radius: .7rem;
  }
  .srv-table tbody td {
    display: flex; align-items: center; justify-content: space-between; gap: 1rem;
    padding: .45rem .7rem; border-bottom: 1px dashed var(--line-soft);
    text-align: right;
  }
  .srv-table tbody td::before {
    content: attr(data-label);
    flex: none; font-size: .66rem; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--ink-faint);
  }
  .srv-table tbody td[data-label=""]::before { content: none; }
  .srv-table tbody tr:last-child td { border-bottom: none; }
  /* В карточке имя сервера — главное, и обрезать его незачем: строки нет,
     ширина вся. */
  .srv-cell { max-width: none; }
  /* Ресурсы в карточке не растягиваются по краям. Ячейка — flex, и без этого
     число и наработка становились тремя отдельными элементами и уезжали
     в разные стороны строки: подпись слева, «16 сл. · 4 ГБ · 2 vCPU»
     посередине, «5ч 18с» у правого края. Здесь ячейка становится блоком,
     подпись встаёт на свою строку, а числа выравниваются вправо. */
  .srv-table tbody td.srv-res { display: block; }
  .srv-table tbody td.srv-res::before { display: block; margin-bottom: .1rem; }
  .srv-res__nums, .srv-res__sub { text-align: right; }
}

/* Питание сервера: три значка подряд у правого края строки.
   Отдельный блок, чтобы не наследовать размеры btn-icon из нижних рядов. */
/* Пометка «заказано N» рядом с фактическим лимитом: человек видит, что
   получил 2 ГБ вместо заказанных 10, и понимает, что это распределение
   ноды, а не обрыв оплаченного тарифа. */
.srv-stat__auto {
  margin-left: .4rem; padding: .05rem .35rem;
  border: 1px dashed var(--line-firm); border-radius: .35rem;
  font-size: .6rem; color: var(--ink-faint);
}

.srv-power { display: flex; align-items: center; gap: .28rem; flex: none; }
.srv-power .btn-icon { height: 1.95rem; width: 1.95rem; }
/* В таблице кнопки на пару пикселей меньше: вместе со значком перехода
   в панель они иначе выдавливают последнюю колонку за край на ноутбуке 1440. */
.srv-table__ctl .srv-power .btn-icon { height: 1.85rem; width: 1.85rem; }

/* Плашка игры в шапке. Название игры — единственный надёжный признак того,
   какая игра реально запущена (имя сервера пользователь задаёт сам), поэтому
   оно живёт рядом с заголовком и берёт цвет из акцента игры. */
/* Название игры. Токен --fg-accent переключается сам (светло-лавандовый
   на тёмном, тёмно-фиолетовый на белом), поэтому отдельного правила для
   светлой темы здесь не нужно — раньше оно было, потому что цвет считался
   из акцента игры. */
/* Заказы сверх видимого листа. Скрыты, а не удалены из разметки: счётчик в
   заголовке и экспорт считают по всем строкам таблицы. */
.orders-more-hidden { display: none; }
.orders-more {
  display: flex; justify-content: center;
  padding: .75rem 0 .25rem;
  border-top: 1px solid var(--pill-line);
}
.orders-more button svg { transition: transform .18s ease; }
.orders-more.is-open button svg { transform: rotate(180deg); }

.srv-game {
  /* align-self обязателен: .srv-cell__body — flex-колонка, а в ней по
     умолчанию align-items: stretch, и плашка игры растягивалась во всю
     ширину ячейки («Minecraft» на пустой полосе). Заголовку h3 ширина при
     этом нужна — он обрезается многоточием, — поэтому правило точечное. */
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: .3rem; padding: .2rem .5rem;
  border-radius: .35rem; font-size: .7rem; font-weight: 600; white-space: nowrap;
  color: var(--fg-accent);
  background: rgb(var(--brand) / .12);
  border: 1px solid rgb(var(--brand) / .32);
  /* Название игры тоже умеет быть длинным («ARK: Survival Evolved»,
     «Counter-Strike 2»), и без потолка оно вылезало за колонку, растягивая
     таблицу. min-width: 0 обязателен — иначе flex-элемент не сжимается. */
  max-width: 100%; min-width: 0; overflow: hidden;
}
.srv-game__name { overflow: hidden; text-overflow: ellipsis; }
.srv-game svg { width: .8rem; height: .8rem; flex: none; opacity: .8; }

/* Машина, на которой живёт сервер. Приглушена сильнее игры: это справка
   для администратора, а не то, чем сервер узнают. Обрезается, а не
   переносится — подпись сервера и так может быть длинной. */
.srv-machine {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: .25rem;
  max-width: 100%;
  font-size: .68rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.srv-machine svg { width: .75rem; height: .75rem; flex: none; opacity: .75; }

/* Пометка «панель не отвечает» под кольцами нагрузки. Показывается, когда
   опрос статуса падает: без неё страница молча показывала последние числа,
   а они неотличимы от показаний остановленного сервера — человек решал,
   что сервер погас, и нажимал «Старт» на работающем.
   Предупреждающий цвет и рамка слева — тот же язык, что у прочих
   предупреждений панели. */
.metrics-stale {
  margin-top: .9rem; padding: .5rem .75rem;
  border-left: 2px solid rgb(var(--accent) / .6);
  border-radius: .35rem;
  background: rgb(var(--accent) / .07);
  font-size: .72rem; line-height: 1.35; color: var(--ink-dim);
}

/* Вся строка ведёт в панель управления сервером, поэтому наводится и
   ведёт себя как ссылка. Без этого указателя человек не понимает, что
   область кликабельна, и идёт искать значок в последней колонке —
   самой дальней от начала таблицы. */
.srv-table__row[data-srv-open] { cursor: pointer; }
.dash-surface .srv-table__row[data-srv-open]:hover,
.dash-surface .srv-table__row[data-srv-open]:focus-within {
  background:
    linear-gradient(90deg,
      rgb(var(--accent) / .13), rgb(34 211 238 / .05) 55%, transparent);
  box-shadow: inset 2px 0 0 rgb(var(--accent) / .75);
}
.table-row.srv-table__row[data-srv-open]:hover { background: rgb(var(--tint) / .035); }

/* Ресурсы в одной ячейке: три числа об одном пресете плюс наработка под
   ними. Отдельными колонками они занимали больше половины таблицы и
   выталкивали «Аренда» с кнопками под обрез. */
.srv-res { white-space: nowrap; }
.srv-res__nums {
  display: block;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.srv-res__u { color: var(--ink-dim); font-size: .78em; }
.srv-res__sub {
  display: block;
  font-size: .7rem; color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   СВЕТЛАЯ ТЕМА
   ==========================================================================

   Всё, что держится на токенах, переключается само: токены переопределены
   в html[data-theme="light"]. Здесь только два слоя:

   1) Утилиты Tailwind из шаблонов. В разметке они записаны один раз и
      рассчитаны на тёмный фон (text-white, text-slate-400, bg-white/[0.03],
      border-white/[0.07] и т. п.). Правть каждое вхождение в 15 шаблонах —
      значит забыть часть, а здесь перечисление закрыто списком: новое
      появление такого класса не сломает тему, но и не подхватится
      автоматически.
   2) Остатки: компоненты, где в теме важен не только цвет, но и контраст
      (тени, скроллбары, выделение текста).

   Порядок важен: этот блок идёт после компонентов, поэтому перебивает их
   по равной специфичности, а селектор с [data-theme] сильнее любого
   одиночного класса Tailwind. */
html[data-theme="light"] {
  /* --- текст: был «белым на чёрном», стал «тёмным на белом» --- */
  color: var(--ink);
}
html[data-theme="light"] .text-white        { color: #0b1120; }
html[data-theme="light"] .text-slate-200   { color: #1e293b; }
html[data-theme="light"] .text-slate-300   { color: #334155; }
/* В тёмной теме чем выше номер, тем тусклее подпись; в светлой наоборот —
   чем выше, тем контрастнее. Старая раскладка (400 → светлее 600) давала на
   белом 2.6:1 и читалась только при увеличении. */
/* Тот же приглушённый тон, что у --ink-faint: 4.44:1 на фоне карточки
   не проходит AA. Держим оба значения одинаковыми, иначе приглушённая
   подпись расходится по контрасту в зависимости от того, токеном или
   утилитой Tailwind она нарисована. */
html[data-theme="light"] .text-slate-400   { color: var(--ink-faint); }
html[data-theme="light"] .text-slate-500   { color: #475569; }
html[data-theme="light"] .text-slate-600   { color: #334155; }

/* Яркие акцентные утилиты Tailwind. На тёмном фоне подобраны светлые
   (aqua-300, emerald-300, brand-300…), на белом они дают 1.5–2.5:1. Тот же
   оттенок, но на порядок темнее — контраст 4.5:1 и выше. Подложки под ними
   полупрозрачные, поэтому бледнеют вместе с фоном, а не остаются тёмными. */
html[data-theme="light"] .text-brand-300,
html[data-theme="light"] .text-brand-200  { color: #4338ca; }
html[data-theme="light"] .text-aqua-300,
html[data-theme="light"] .text-aqua-400   { color: #0e7490; }
html[data-theme="light"] .text-emerald-200,
html[data-theme="light"] .text-emerald-300,
html[data-theme="light"] .text-emerald-500 { color: #047857; }
html[data-theme="light"] .text-emerald-400 { color: #059669; }
html[data-theme="light"] .text-rose-200    { color: #9f1239; }
html[data-theme="light"] .text-rose-300,
html[data-theme="light"] .text-rose-400    { color: #be123c; }
html[data-theme="light"] .text-amber-100   { color: #92400e; }
html[data-theme="light"] .text-amber-200,
html[data-theme="light"] .text-amber-300   { color: #a16207; }

/* Те же утилиты, но помеченные «важными» в разметке: `!text-emerald-300`
   и прочие варианты с префиксом `!`. Tailwind собирает их как
   `color: … !important`, поэтому перекрытия выше, записанные обычным
   правилом, проигрывают по специфичности — и в светлой теме бейджи
   «задан» и «2 из 3» в админке оставались мятными #6ee7b7 на белом,
   с контрастом около 1.4:1. Здесь важность повторяется, иначе правило
   снова проиграет.
   Имя класса с «!» экранируется обратной косой чертой. */
html[data-theme="light"] .\!text-brand-300,
html[data-theme="light"] .\!text-brand-200   { color: #4338ca !important; }
html[data-theme="light"] .\!text-aqua-300,
html[data-theme="light"] .\!text-aqua-400    { color: #0e7490 !important; }
html[data-theme="light"] .\!text-emerald-200,
html[data-theme="light"] .\!text-emerald-300,
html[data-theme="light"] .\!text-emerald-500 { color: #047857 !important; }
html[data-theme="light"] .\!text-emerald-400 { color: #059669 !important; }
html[data-theme="light"] .\!text-rose-200     { color: #9f1239 !important; }
html[data-theme="light"] .\!text-rose-300,
html[data-theme="light"] .\!text-rose-400    { color: #be123c !important; }
html[data-theme="light"] .\!text-amber-100   { color: #92400e !important; }
html[data-theme="light"] .\!text-amber-200,
html[data-theme="light"] .\!text-amber-300   { color: #a16207 !important; }

/* Компоненты, а не утилиты: тот же приём, но для селекторов, которые живут
   в app.css. Без них светлая тема оставляла светлый текст на светлой
   карточке — плашки на карточках фич читались с контрастом 1.4–2.1:1.
   Тёмные подложки переопределяются тоже: мини-терминал .cli на карточке
   «Консоль и файлы» оставался чёрным пятном посреди белой страницы (см.
   блок .cli ниже). Метрики пароля на насыщенной заливке не трогаются:
   там светлый текст на тёмном остаётся правильным. */
html[data-theme="light"] .feat-item__tags span          { color: #475569; }
html[data-theme="light"] .bento__card:hover .feat-item__tags span { color: #3730a3; }
/* Плашка иконки фичи при наведении. В тёмной теме значок становится белым
   на подложке из полупрозрачного фиолетового — это единственный способ
   сделать его ярче. В светлой теме подложка светлая, и белая иконка на ней
   просто исчезала: цветом темы идёт фиолетовый, как у меток выше. */
html[data-theme="light"] .bento__card:hover .feat-icon  { color: #3730a3; }
html[data-theme="light"] .label                        { color: #475569; }
html[data-theme="light"] .guide-code                   { color: #0e7490; background: rgb(var(--tint) / .05); }
html[data-theme="light"] .period__off                  { color: #047857; }
html[data-theme="light"] .pw-hint.is-ok                { color: #047857; }
html[data-theme="light"] .pw-hint.is-bad               { color: #be123c; }
html[data-theme="light"] .pw-meter[data-level="1"] .pw-meter__label { color: #be123c; }
html[data-theme="light"] .pw-meter[data-level="2"] .pw-meter__label { color: #a16207; }
html[data-theme="light"] .pw-meter[data-level="3"] .pw-meter__label,
html[data-theme="light"] .pw-meter[data-level="4"] .pw-meter__label { color: #047857; }
html[data-theme="light"] .txn-icon--in                 { color: #047857; }
html[data-theme="light"] .txn-icon--out                { color: #475569; }
html[data-theme="light"] .user-role                    { color: #3730a3; }
html[data-theme="light"] .onboard-hero__icon           { color: #4338ca; }
html[data-theme="light"] .receipt-sum__value          { color: #0b1120; }
html[data-theme="light"] .order-price__value          { color: #0b1120; }
html[data-theme="light"] .nav-link-active svg          { color: #4338ca; }
html[data-theme="light"] .foot-link:hover svg          { color: #4338ca; }
html[data-theme="light"] .foot-legal:hover             { color: #3730a3; }
/* Знаки подвала не перекрашиваются: глиф задан currentColor и лежит
   на фирменной плитке, поэтому в светлой теме он остаётся белым. */
html[data-theme="light"] .foot-social__btn:hover svg,
html[data-theme="light"] .foot-social__btn:focus-visible svg { color: #fff; }

/* Те же утилиты, но с прозрачностью в самом классе (text-rose-200/60 и т. п.).
   Их не цепляет правило выше — имя класса другое. Приглушённость сохраняем
   через color-mix, иначе /60 стал бы читаться так же громко, как /100. */
html[data-theme="light"] .text-emerald-500\/70 { color: color-mix(in srgb, #047857 70%, transparent); }
html[data-theme="light"] .text-rose-200\/70    { color: color-mix(in srgb, #9f1239 70%, transparent); }
html[data-theme="light"] .text-rose-200\/60    { color: color-mix(in srgb, #9f1239 60%, transparent); }
html[data-theme="light"] .text-amber-100\/90 { color: color-mix(in srgb, #92400e 90%, transparent); }
html[data-theme="light"] .text-amber-200\/90 { color: color-mix(in srgb, #a16207 90%, transparent); }
html[data-theme="light"] .text-amber-200\/60 { color: color-mix(in srgb, #a16207 60%, transparent); }
html[data-theme="light"] .text-amber-300\/90 { color: color-mix(in srgb, #a16207 90%, transparent); }

/* Плашки и рамки предупреждений из шаблонов — они тоже заданы утилитами
   Tailwind с янтарным оттенком и в светлой теме бледнеют вместе с фоном. */
html[data-theme="light"] .bg-amber-400\/\[0\.07\] { background-color: rgba(245, 158, 11, .12); }
html[data-theme="light"] .border-amber-400\/30     { border-color: rgba(245, 158, 11, .45); }

  /* белый текст на цветных плашках должен остаться белым: там, где фон
     задаёт сам компонент (кнопки, бейджи, плитки знаков), цвет вынесен
     ниже в .btn-*/.badge/… */
html[data-theme="light"] .cl-line,
html[data-theme="light"] .cl-game { color: #e2e8f0; }

  /* --- поверхности: белые полупрозрачные накладки становутся тёмными --- */
html[data-theme="light"] .bg-white\/\[0\.02\] { background-color: rgb(var(--tint) / .025); }
html[data-theme="light"] .bg-white\/\[0\.03\] { background-color: rgb(var(--tint) / .03); }
html[data-theme="light"] .bg-white\/\[0\.05\] { background-color: rgb(var(--tint) / .05); }
html[data-theme="light"] .border-white\/\[0\.05\] { border-color: rgb(var(--tint) / .09); }
html[data-theme="light"] .border-white\/\[0\.06\] { border-color: rgb(var(--tint) / .10); }
html[data-theme="light"] .border-white\/\[0\.07\] { border-color: rgb(var(--tint) / .11); }
html[data-theme="light"] .border-white\/\[0\.08\] { border-color: rgb(var(--tint) / .12); }
html[data-theme="light"] .border-white\/\[0\.09\] { border-color: rgb(var(--tint) / .13); }

/* Белая подпись нужна только там, где фон действительно цветной и плотный.
   Раньше сюда попадали ещё и полупрозрачные плашки (бейджи, .btn-ok,
   .btn-danger) и цена в карточке игры: в светлой теме их подложка бледнеет
   до оттенка, а белая надпись на бледном — это белый по белому. */
html[data-theme="light"] .btn-primary,
html[data-theme="light"] .pick.is-active .pick__check { color: #fff; }

/* Цветные плашки с полупрозрачной подложкой: в тёмной теме подпись светлее
   подложки, в светлой — темнее и насыщеннее того же оттенка. */
html[data-theme="light"] .btn-ok   { color: #047857; }
html[data-theme="light"] .btn-ok:hover   { color: #065f46; }
html[data-theme="light"] .btn-danger { color: #b91c1c; }
html[data-theme="light"] .btn-danger:hover { color: #991b1b; }
html[data-theme="light"] .btn-sub   { color: #4338ca; }
html[data-theme="light"] .btn-sub:hover   { color: #3730a3; }
html[data-theme="light"] .badge-online,
html[data-theme="light"] .badge-paid,
html[data-theme="light"] .badge-trial { color: #047857; }
html[data-theme="light"] .badge-starting,
html[data-theme="light"] .badge-pending { color: #a16207; }
html[data-theme="light"] .badge-stopping { color: #c2410c; }
html[data-theme="light"] .badge-installing { color: #4338ca; }
html[data-theme="light"] .badge-error,
html[data-theme="light"] .badge-crashed,
html[data-theme="light"] .badge-failed  { color: #b91c1c; }
html[data-theme="light"] .badge-canceled { color: #475569; }
/* Адрес сервера в чипе: в тёмной теме бирюзовый на бирюзовой подложке,
   в светлой то же самое читается слишком слабо. */
html[data-theme="light"] .spec-chip--addr {
  color: #0f766e; background: rgba(13, 148, 136, .1); border-color: rgba(13, 148, 136, .3);
}
html[data-theme="light"] .spec-chip--addr:hover {
  color: #115e59; background: rgba(13, 148, 136, .18); border-color: rgba(13, 148, 136, .5);
}
html[data-theme="light"] .badge-offline { color: #475569; }
/* Те же два приёма на карточке сервера: плашка игры смешивается с белым,
   «Выйти» в панели — светло-красный, и на светлом фоне оба бледнели. */
html[data-theme="light"] .side-link--danger { color: #b91c1c; }
html[data-theme="light"] .side-link--danger:hover { color: #991b1b; }
/* Кнопка «Заблокировать» в админке несёт !text-rose-300 — на тёмном это
   нормально, на белом почти не читается. Селектор по классу, а не по
   конкретному тексту-rose: под кнопкой может быть любая из этих розовых. */
html[data-theme="light"] .btn-ghost[class*="text-rose-3"],
html[data-theme="light"] .btn-ghost[class*="text-red-3"] { color: #b91c1c !important; }
html[data-theme="light"] .btn-ghost[class*="text-emerald-3"],
html[data-theme="light"] .btn-ghost[class*="text-green-3"]  { color: #047857 !important; }

/* Значки в карточках справки: тот же приём — подложка бледнеет вместе с
   фоном, поэтому подпись значка делаем насыщенной. */
html[data-theme="light"] .help-card__icon { color: #6d28d9; }
html[data-theme="light"] .guide-hub__icon { color: #6d28d9; }

/* Цвет игры как текст. Акценты подобраны под тёмный фон, и на белом
   оранжевый даёт ~2.8:1. Затемняем тот же оттенок вниз, не трогая сами
   акценты: градиенты плашек и кнопок отсюда берут --g1 напрямую. */
html[data-theme="light"] .map-card__tick {
  color: color-mix(in srgb, var(--g1) 62%, #1e293b);
}

/* Соединитель цепочки в подвале и номер шага: на белом светло-лавандовый
   почти не виден, поэтому берём тот же оттенок насыщеннее. */
html[data-theme="light"] .foot-steps__item:not(:last-child)::after {
  background-image: linear-gradient(90deg, rgba(99, 102, 241, .75), rgba(99, 102, 241, .25));
  box-shadow: none;
}
html[data-theme="light"] .foot-steps__item:not(:last-child)::before {
  border-color: rgba(79, 70, 229, .8);
}
html[data-theme="light"] .foot-steps__badge {
  background: rgba(99, 102, 241, .12);
  border-color: rgba(99, 102, 241, .35);
  color: #4338ca;
}

/* Статусы тикетов в админ-панели: подложки полупрозрачные и в светлой теме
   бледнеют вместе с фоном, поэтому подпись делаем насыщенной. */
html[data-theme="light"] .thread-status--ai       { color: #4338ca; background: rgba(99, 102, 241, .14); }
html[data-theme="light"] .thread-status--waiting  { color: #a16207; background: rgba(245, 158, 11, .16); }
html[data-theme="light"] .thread-status--resolved { color: #047857; background: rgba(16, 185, 129, .16); }
html[data-theme="light"] .foot-pay__note         { color: #92400e; background: rgba(245, 158, 11, .16); }

/* Виджет поддержки: та же история с цветными плашками. Панель в светлой теме
   светлая (--bg-raised), поэтому светло-жёлтый баннер и мятный ответ
   оператора просто исчезали. */
html[data-theme="light"] .support-banner {
  color: #92400e;
  background: rgba(245, 158, 11, .12);
  border-color: rgba(245, 158, 11, .45);
}
html[data-theme="light"] .support-msg--admin {
  color: #065f46;
  background: rgba(16, 185, 129, .14);
  border-color: rgba(5, 150, 105, .35);
}
/* Подпись источника на индиговой плашке остаётся светлой: там фон цветной
   в обеих темах, и тёмный текст на индиго не читался бы. */
html[data-theme="light"] .support-log--admin .support-msg--user .support-msg__src { color: rgb(255 255 255 / .75); }

/* Шапка и боковая панель держат фон классом Tailwind (bg-ink-900/80) —
   он тёмный в обеих темах, поэтому светлой её нужно перекрасить явно. */
html[data-theme="light"] .site-header {
  background: rgb(255 255 255 / .82);
  border-bottom-color: var(--line);
}

/* Тени на светлом фоне почти не читаются — берём мягче и с нейтральным
   оттенком, иначе карточки выглядят грязными пятнами. */
html[data-theme="light"] .glass,
html[data-theme="light"] .game-card,
html[data-theme="light"] .help-card,
html[data-theme="light"] .bento__card,
html[data-theme="light"] .thread-card,
html[data-theme="light"] .starter-card,
html[data-theme="light"] .map-card,
html[data-theme="light"] .balance-card { box-shadow: 0 10px 30px -18px rgb(15 23 42 / .35); }

/* Скроллбар и выделение текста — системные, токены интерфейса их не касаются. */
html[data-theme="light"] ::-webkit-scrollbar-thumb { background: rgb(15 23 42 / .22); }
html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: rgb(15 23 42 / .34); }
html[data-theme="light"] ::selection { background: rgb(var(--brand) / .22); color: #0b1120; }

/* Печать всегда остаётся на белом — переопределение темы не должно
   превращать квитанцию в чёрный лист. */
@media print {
  html[data-theme="light"] body { background: #fff !important; color: #000 !important; }
}

/* ==========================================================================
   9. КОНСОЛЬ, ТАБЛИЦЫ, АДАПТИВ
   ========================================================================== */
.console {
  font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
  font-size: .78rem; line-height: 1.55;
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(15, 23, 42, .92), rgba(2, 6, 14, .98));
  border: 1px solid var(--line); border-radius: 1rem;
  overflow-y: auto; padding: 1rem; color: #94a3b8;
  box-shadow: inset 0 20px 50px -30px #000, inset 0 0 60px -40px rgb(var(--brand));
}
.console-line { white-space: pre-wrap; word-break: break-word; line-height: 1.5; border-radius: .2rem; }
.console-line:hover { background: rgb(var(--tint) / .04); }
.cl-game { color: #a5b4fc; }
.cl-join { color: #6ee7b7; }
.cl-quit { color: #fca5a5; }
.cl-warn { color: #fcd34d; }
.cl-err  { color: #fb7185; font-weight: 600; }
.cl-dock { color: #22d3ee; }
.cl-cmd  { color: #f0abfc; font-weight: 700; }
.console-in {
  font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: .78rem;
  background: transparent; border: none; outline: none; color: #22d3ee; width: 100%;
}

/* Светлая тема: консоль была чёрной на белой странице — единственное
   тёмное пятно в интерфейсе. Фон уходит на белый карточный, а цвета
   строк берутся те же тона, что у плашек статуса в светлой теме, иначе
   бледно-мятный текст на белом читается хуже, чем он читался на чёрном.
   Значения подобраны по контрасту к #f5f7fb, а не к белому: консоль стоит
   на карточке, и «5.9:1 на белом» тут означало бы 4.9:1 на реальном фоне. */
html[data-theme="light"] .console {
  background:
    linear-gradient(180deg, rgba(15, 23, 42, .03), rgba(15, 23, 42, 0)),
    var(--bg-raised);
  color: var(--ink-dim);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8), inset 0 20px 40px -34px rgb(15 23 42 / .5);
}
html[data-theme="light"] .console-line:hover { background: rgb(15 23 42 / .05); }
html[data-theme="light"] .cl-game { color: #4338ca; }
html[data-theme="light"] .cl-join { color: #047857; }
html[data-theme="light"] .cl-quit { color: #b91c1c; }
html[data-theme="light"] .cl-warn { color: #a16207; }
html[data-theme="light"] .cl-err  { color: #be123c; }
html[data-theme="light"] .cl-dock { color: #0e7490; }
html[data-theme="light"] .cl-cmd  { color: #7e22ce; }
html[data-theme="light"] .console-in { color: #0e7490; }

/* Скроллящаяся обёртка таблиц: тонкий скроллбар вместо обрезки колонок */
.table-wrap { overflow-x: auto; overscroll-behavior-x: contain; }
.table-wrap::-webkit-scrollbar { height: 8px; }
.rtable { width: 100%; border-collapse: separate; border-spacing: 0; }
.rtable thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--panel-solid); backdrop-filter: blur(8px);
  padding: .75rem 1.15rem; text-align: left;
  font-size: .68rem; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-faint);
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
.rtable tbody td { padding: .8rem 1.15rem; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.table-row { transition: background .18s var(--ease); }
.table-row:hover { background: rgb(var(--tint) / .035); }
.table-row:last-child td { border-bottom: none; }

/* Скрытие строк таблиц атрибутом hidden.
   Раньше JS ставил row.hidden = true, и строка оставалась на экране: display
   табличных элементов приходит из UA-стилей, и правило [hidden] из них же
   оказывалось слабее, чем display: table-row у <tr>. Явное правило снимает
   вопрос — иначе фильтр «поиск/статус» менял счётчик, но не картинку. */
.rtable tr[hidden], .rtable tbody [hidden] { display: none; }

/* Мобильная трансформация таблицы: строки становятся карточками.
   Заголовок колонки берётся из data-label у ячеек. */
@media (max-width: 767px) {
  .rtable, .rtable tbody, .rtable tr, .rtable td { display: block; width: 100%; }
  .rtable thead { display: none; }
  .rtable tr {
    margin: 0 0 .6rem; padding: .35rem .2rem;
    background: rgb(var(--tint) / .022);
    border: 1px solid var(--line); border-radius: .7rem;
  }
  .rtable tbody td {
    display: flex; align-items: center; justify-content: space-between; gap: 1rem;
    padding: .45rem .7rem; border-bottom: 1px dashed var(--line-soft);
    text-align: right;
  }
  .rtable tbody td::before {
    content: attr(data-label);
    flex: none; font-size: .66rem; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--ink-faint);
  }
  .rtable tbody td[data-label=""]::before { content: none; }
  .rtable tbody tr:last-child td { border-bottom: none; }
}

/* ------------------------------------------------ БИЛЛИНГ: баланс и таблица */
/* Лицевой счёт: единственный крупный счётчик на странице, поэтому
   читается как «главный» элемент, а не как ещё одна карточка. */
.balance-card {
  padding: 1.4rem 1.5rem;
  border: 1px solid rgba(129, 140, 248, .22);
  border-radius: 1rem;
  /* Подложка снизу — обязательна. Раньше её не было вовсе, и карточка
     просвечивала фотографией героя: в тёмной теме это читалось как стекло,
     а в светлой залипающий индиговый градиент поверх снимка давал тёмное
     пятно посреди светлой страницы. Тот же --bg-raised, что у .glass, —
     карточка встаёт в один ряд с соседней. */
  background-color: var(--bg-raised);
  background-image:
    radial-gradient(120% 140% at 0% 0%,
      color-mix(in srgb, var(--fg-accent) 14%, transparent), transparent 60%);
  backdrop-filter: blur(12px) saturate(150%);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
}
.balance-card__label {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-faint);
}
.balance-card__value {
  margin-top: .5rem;
  font-size: 2.4rem; font-weight: 800; line-height: 1;
  /* Токен темы, а не белый литерал: на светлой подложке сумма была
     белым по белому и почти не читалась. */
  color: var(--ink); font-variant-numeric: tabular-nums;
}

/* Чип с идентификатором: моноширинный текст + значок копирования.
   Подпись приглушена (5.1:1 по WCAG AA), значок гаснет и просыпается
   вместе с наведением — так же, как плашки способов оплаты в футере. */
.id-chip {
  display: inline-flex; align-items: center; gap: .35rem;
  max-width: 100%; padding: .18rem .4rem;
  border: 1px solid var(--line); border-radius: .4rem;
  background: rgb(var(--tint) / .025);
  font-size: .68rem; color: var(--ink-faint);
  transition: color .16s var(--ease), border-color .16s var(--ease),
              background .16s var(--ease);
}
.id-chip:hover { color: var(--ink); border-color: var(--line-firm); background: rgb(var(--tint) / .06); }
.id-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.id-chip svg { width: .75rem; height: .75rem; flex: none; opacity: .5; transition: opacity .16s var(--ease); }
.id-chip:hover svg { opacity: 1; }
.id-chip:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }

/* Знак движения денег */
.txn-icon {
  display: grid; place-items: center; flex: none;
  width: 1.6rem; height: 1.6rem; border-radius: .5rem;
}
.txn-icon--in { color: #34d399; background: rgba(52, 211, 153, .12); }
.txn-icon--out { color: #94a3b8; background: rgba(148, 163, 184, .1); }

/* Страницы пагинации */
.page-btn {
  display: inline-grid; place-items: center; min-width: 2.1rem; height: 2.1rem;
  padding: 0 .55rem; border-radius: .5rem;
  border: 1px solid var(--line); background: rgb(var(--tint) / .025);
  color: var(--ink-dim); font-size: .78rem; font-weight: 600;
  text-decoration: none;
  transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.page-btn:hover { color: var(--fg-on-tint); border-color: var(--line-firm); background: rgb(var(--tint) / .07); }
.page-btn.is-current {
  color: var(--fg-on-tint); border-color: rgba(129, 140, 248, .5); background: rgba(99, 102, 241, .25);
}

/* Быстрые суммы пополнения */
.topup-amount {
  padding: .45rem .8rem; border-radius: .55rem;
  border: 1px solid var(--line); background: rgb(var(--tint) / .03);
  color: var(--ink-dim); font-size: .82rem; font-weight: 600;
  transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.topup-amount:hover { color: var(--fg-on-tint); border-color: rgba(129, 140, 248, .45); background: rgba(99, 102, 241, .16); }

/* Строка квитанции */
.receipt-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.receipt-row dt { flex: none; font-size: .72rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-faint); }
.receipt-row dd { min-width: 0; text-align: right; color: var(--ink-dim); overflow-wrap: anywhere; }

/* Сумма чека. Раньше это была обычная строка перечня, тем же кеглем, что
   дата, — и главный ответ документа прятался среди шести подписей. Здесь он
   стоит первым и держит рамку, отделяющую его от служебных строк. */
.receipt-sum {
  margin-top: 1rem; padding: .9rem 1rem;
  border: 1px solid var(--line); border-radius: .75rem;
  background: rgb(var(--tint) / .035);
}
.receipt-sum__head { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.receipt-sum__label {
  font-size: .7rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-faint);
}
/* Сумма зачисления — самое крупное в чеке, поэтому белое на тёмном.
   В светлой теме подложка блока светлеет вместе с фоном, и белое становится
   белым по белому: цвет переопределён в блоке темы. */
.receipt-sum__value {
  margin-top: .35rem;
  font-size: 1.9rem; font-weight: 800; line-height: 1.05; color: #fff;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------- модалки */
.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 1rem; }
.modal[hidden] { display: none; }
.modal__backdrop {
  position: absolute; inset: 0;
  background: rgba(5, 7, 12, .72);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}

.modal__box {
  position: relative; width: min(30rem, 100%);
  max-height: calc(100vh - 2rem); overflow-y: auto;
  padding: 1.5rem;
  border: 1px solid var(--line-firm); border-radius: 1rem;
  background: var(--bg-raised, #12151f);
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .95);
  animation: modal-in .22s var(--ease);
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to { opacity: 1; transform: none; }
}

/* ---- согласие на cookie ---------------------------------------------
   Плашка у нижнего края, а не модалка. Модалка тут была неуместна: она
   останавливает страницу и требует решения прежде, чем человек что-то
   прочитал, — а согласие всё равно спрашивают один раз, и мешать чтению
   незачем.

   Плашка едет снизу вверх: движение совпадает с направлением, откуда
   человек обычно тянет взгляд к уведомлениям, и читается как «сообщение»,
   а не как требование. Уход — вниз же, по той же оси.

   Значок и заголовок появляются сразу, текст и кнопки — следом: длинный
   абзац, выезжающий вместе с заголовком, мелькает сплошной полосой. */
.cookie-bar {
  position: fixed; z-index: 55;
  left: 1rem; right: 1rem; bottom: 1rem;
  margin-inline: auto; width: fit-content; max-width: 62rem;
  display: flex; align-items: center; gap: 1rem;
  padding: .85rem 1.1rem;
  border: 1px solid rgb(var(--brand) / .26);
  border-radius: 1rem;
  background: var(--panel-solid);
  box-shadow: 0 22px 50px -22px #000,
              0 0 0 1px rgb(var(--brand) / .06);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  animation: cookie-rise-in .42s var(--ease) both;
}
.cookie-bar[hidden] { display: none; }
/* Уход — по той же оси, откуда пришло появление. По умолчанию анимация
   задана на .cookie-bar, а .is-leaving её переопределяет: без этого
   классу пришлось бы гасить базовую анимацию, и плашка мигала бы при
   показе, а не при уходе. */
.cookie-bar.is-leaving { animation: cookie-drop-out .3s var(--ease) both; }
@keyframes cookie-drop-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(14px); }
}
.cookie-bar__icon {
  flex: none; display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; border-radius: .8rem;
  color: rgb(var(--brand) / .95);
  background: rgb(var(--brand) / .12);
  border: 1px solid rgb(var(--brand) / .26);
}
.cookie-bar__icon svg { width: 1.15rem; height: 1.15rem; }
.cookie-bar__body { min-width: 0; }
.cookie-bar__title { font-size: .92rem; font-weight: 700; color: var(--ink); }
.cookie-bar__text {
  margin-top: .15rem; font-size: .8rem; line-height: 1.5;
  color: var(--ink-dim); text-wrap: pretty;
}
.cookie-bar__actions { flex: none; display: flex; gap: .5rem; }
.cookie-bar__actions .btn-primary, .cookie-bar__actions .btn-sub { white-space: nowrap; }

@keyframes cookie-rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* Плашка занимает низ экрана, где живут уведомления и кнопка «Наверх».
   Пока она на экране, обоим нужно место: иначе тост поверх согласия
   перекрыл бы кнопки и человек решил бы, что интерфейс сломан. */
body.has-cookie-bar #toast-host { bottom: 6.5rem; }
body.has-cookie-bar .to-top { bottom: 6.5rem; }

/* Узкий экран: текст и кнопки в столбец, иначе три колонки в 360px
   превращаются в полосы по три слова. */
@media (max-width: 720px) {
  .cookie-bar {
    left: .6rem; right: .6rem; bottom: .6rem;
    flex-wrap: wrap; align-items: flex-start; gap: .75rem;
    padding: .9rem;
  }
  .cookie-bar__body { flex: 1 1 12rem; }
  .cookie-bar__actions { flex: 1 1 100%; }
  .cookie-bar__actions .btn-primary,
  .cookie-bar__actions .btn-sub { flex: 1 1 auto; }
  body.has-cookie-bar #toast-host { bottom: 10rem; }
  body.has-cookie-bar .to-top { bottom: 10rem; }
}

/* Уменьшил движение в системе — плашка появляется и уходит сразу на месте. */
@media (prefers-reduced-motion: reduce) {
  .cookie-bar, .cookie-bar.is-leaving { animation: none; }
}
.btn-icon--sm { width: 1.9rem; height: 1.9rem; border-radius: .5rem; }

/* Печать квитанции: на бумаге остаётся только содержимое модалки. */
@media print {
  body { background: #fff !important; }
  body > *:not(#receipt-modal) { display: none !important; }
  #receipt-modal { position: static; display: block !important; padding: 0; background: #fff; }
  #receipt-modal .modal__backdrop,
  #receipt-modal [data-close],
  #receipt-modal [data-print] { display: none !important; }
  #receipt-modal .modal__box {
    width: 100%; max-height: none; border: none; box-shadow: none;
    padding: 0; background: #fff; color: #111; animation: none;
  }
  #receipt-modal h2, #receipt-modal dt { color: #333 !important; }
  #receipt-modal dd, #receipt-modal .receipt-row span { color: #111 !important; }
  /* Сумма уехала из перечня в отдельный блок, поэтому прежнее правило для dd
     её уже не накрывало: на бумаге она печаталась белым по белому. */
  #receipt-modal .receipt-sum { border-color: #ddd; background: none; }
  #receipt-modal .receipt-sum__label { color: #555 !important; }
  #receipt-modal .receipt-sum__value { color: #000 !important; }
  #receipt-modal .badge { border: 1px solid #bbb; background: none; color: #333 !important; }
  #receipt-modal .id-chip { border-color: #ccc; background: none; }
  #receipt-modal p { color: #555 !important; }
}

/* ------------------------------------------------------------------ вкладки */
.tab-bar {
  display: flex; gap: .25rem; overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--line); padding: .4rem;
}
.tab-bar::-webkit-scrollbar { display: none; }
.tab-btn {
  display: inline-flex; align-items: center;
  padding: .5rem .9rem; font-size: .82rem; font-weight: 600; color: var(--ink-dim);
  border-radius: .55rem; cursor: pointer; white-space: nowrap;
  transition: color .18s var(--ease), background .18s var(--ease), box-shadow .18s var(--ease);
}
.tab-btn:hover { color: var(--fg-on-tint); background: rgb(var(--tint) / .06); }
.tab-btn svg { width: .95rem; height: .95rem; }
.tab-active {
  color: var(--fg-on-tint);
  background: linear-gradient(180deg, rgba(99, 102, 241, .28), rgba(99, 102, 241, .12));
  box-shadow: inset 0 0 0 1px rgba(129, 140, 248, .4), 0 0 20px -10px rgba(99, 102, 241, .9);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* Проявление панели — только при переключении вкладки (класс ставит
   initTabs). На первой загрузке анимации нет: страница и так появляется
   целиком, а мигание активной панели после перезагрузки читалось как сбой. */
.tab-panel.active.is-switched { animation: fade .32s var(--ease); }
@keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* =============================================================================
   НАЗВАНИЕ БРЕНДА

   Один компонент для шапки и подвала. Графического знака нет — только
   слово, разбитое на две части: первая белая, вторая градиентная
   (классы brand-word__a / brand-word__b рендерит Jinja-макрос, деля
   APP_NAME по второй заглавной букве).
   ========================================================================== */
.brand-link {
  display: inline-flex; align-items: center;
  text-decoration: none; border-radius: .4rem;
  /* Слово без знака — всего 18px высотой, это меньше минимальной области
     нажатия. Padding добавляет невидимую зону клика, не сдвигая соседей:
     строка шапки центрирует элементы по вертикали. */
  padding: .55rem .25rem;
}
.brand { display: inline-flex; align-items: center; }

/* Слово бренда.
   Именно inline, а не inline-flex: флекс-элементы разъезжаются по
   строкам, и имя доступности считается как «Game Dock» — со словом
   читается, но имя ссылки перестаёт совпадать с подписью на экране.

   18px, а не 16px: на фоне страницы слово терялось, а рядом с монограммой
   «GD» в панели выглядело мелким. Теперь кегль держит тот же оптический
   вес, что и монограмма (1.15rem), и панель читается как одно имя. */
.brand-word {
  display: inline;
  font-size: 1.125rem; font-weight: 800; letter-spacing: -.02em;
  line-height: 1; white-space: nowrap;
  transition: filter .22s var(--ease), text-shadow .22s var(--ease);
}
.brand-word__a, .brand-word__b { display: inline; }
.brand-word__a { color: var(--ink); }
/* Градиентный хвост: индиго → пурпурный, обрезан по тексту */
.brand-word__b {
  background-image: linear-gradient(100deg, #818cf8, #c084fc);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
/* На светлом фоне бледная часть градиента сливалась с белым: хвост слова
   «Dock» пропадал, оставалось «Game». Усиливаем оба стоп-цвета. */
html[data-theme="light"] .brand-word__b {
  background-image: linear-gradient(100deg, #4f46e5, #9333ea);
}

.brand-link:hover .brand-word {
  filter: brightness(1.12);
  text-shadow: 0 0 18px rgba(129, 140, 248, .35);
}
.brand-link:focus-visible {
  outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 3px;
}

/* Заголовок и лид первого экрана.

   Тень отделяет светлый текст от фотографии стоек — в тёмной теме это
   единственное, что держит буквы поверх снимка. В светлой теме текст
   становится тёмным (text-white → #0b1120, text-slate-200 → #1e293b),
   и та же тень превращается в серый ореол вокруг букв: тёмное пятно
   под тёмными же буквами. Плюс там уже есть свой скрим, --bg-scrim,
   который высветляет снимок, — разделять текст больше нечем.

   Поэтому тень живёт здесь, а не в утилите Tailwind в разметке: так её
   видно в обоих состояниях сразу и видно, что она не универсальная. */
.hero-title {
  /* Заголовок набирается из консоли, поэтому моноширинный: именно моно
     читается как терминал, а гротеск в капсе остаётся «рекламой баннера».
     Плюс у моноширинного все буквы одной ширины — при печати по символам
     текст не «прыгает» вправо-влево, как с обычным пропорциональным. */
  font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, Consolas, monospace;
  text-transform: uppercase;
  font-weight: 800;
  letter-spacing: -.03em;
  /* В моноширинном пробел шириной с букву, и в капсе слова разъезжаются
     («ИГРОВОЙ  СЕРВЕР»). Подбираем пробел обратно. На «за&nbsp;60&nbsp;секунд»
     это не действует: word-spacing применяется только к разделителям слов,
     а неразрывный пробел к ним не относится — эти два места остаются
     полной ширины, и строка выходит шире расчёта на 0.24em. */
  word-spacing: -.12em;
  line-height: 1.12;
  /* Кегль. Верхняя граница — 3.5rem (56px). В колонке max-w-4xl (896px) вторая
     строка занимает на нём ~760px, курсор — ещё ~36px: запас около 100px.
     В прежней колонке max-w-3xl (768px) запаса не оставалось — курсор
     переносился на третью строку. Средний член 4.6vw нужен затем же: при
     4.2vw верхняя граница почти нигде не достигалась, и на типовом экране
     заголовок оставался прежним. */
  /* Кегль на телефоне. Было clamp(1.6rem, 4.6vw, 3.5rem) — на 412px это
     25,6px раскладки, то есть 19px на экране: заголовок первого экрана
     читался мелко, он должен вести страницу, а не теряться рядом с
     подзаголовком.

     Размер ограничен самой длинной строкой фразы — «до Москвы и
     Петербурга» и «запускается за 60 секунд», по 22 символа. Замерено:
     моноширинный знак с трекингом -0.03em занимает 0.572em, плюс курсор
     около 0.55em. Отсюда предел кегля — ширина колонки, делённая на 13.13:
     30px на экране 320px, 35px на 375px, 39px на 412px, 46px на 480px.

     8.8vw даёт на этих экранах 30/33/34/36/42px — то есть запас до предела
     есть на каждом, а не впритык. Нижняя граница нужна самому узкому экрану,
     где vw уходит ниже предела.

     Верхняя граница 3.5rem не тронута: на десктопе 8.8vw переваливает за неё
     и кегль остаётся прежним. */
  font-size: clamp(1.85rem, 8.8vw, 3.5rem);
  filter: drop-shadow(0 2px 18px rgb(2 6 23 / .55));
  /* Свечение фиолетовым — тот же тон, что у кнопок и бейджей. */
  text-shadow: 0 0 26px rgb(99 102 241 / .35);
}

/* Приглашение консоли. Скрыто до старта печати: в исходной разметке его
   нет, и без JS заголовок выглядит как обычная строка, а не как обрывок
   терминала. */
.hero-title__prompt {
  color: rgb(129 140 248 / .9);
  margin-right: .18em;
  opacity: 0;
  transition: opacity .3s var(--ease);
}
.hero-title.is-typing .hero-title__prompt,
.hero-title.is-done .hero-title__prompt { opacity: 1; }

/* Курсор. Не символ в разметке, а псевдоэлемент: его можно мигать без
   перерисовки и он не попадает в текст для скринридера. */
.hero-title__caret {
  display: inline-block;
  width: .58em; height: 1.02em;
  margin-left: .06em;
  vertical-align: -.14em;
  background: rgb(129 140 248 / .95);
  box-shadow: 0 0 14px rgb(129 140 248 / .8);
  opacity: 0;
}
.hero-title.is-typing .hero-title__caret { opacity: 1; animation: hero-caret .85s steps(1) infinite; }
.hero-title.is-done .hero-title__caret { animation: hero-caret 1.4s steps(1) infinite; }

@keyframes hero-caret { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

/* Метка строки: display:inline-block нужен, чтобы печатающаяся часть не
   дёргала высоту строки между строками заголовка. */
.hero-title__line { display: inline-block; min-height: 1em; }
.hero-lead { filter: drop-shadow(0 1px 2px rgb(2 6 23 / .18)); }
html[data-theme="light"] .hero-title,
html[data-theme="light"] .hero-lead { filter: none; }
html[data-theme="light"] .hero-title { text-shadow: none; }

@media (max-width: 640px) {
  /* Капс в моноширинном шире обычного регистра примерно на треть, и на
     телефоне заголовок уезжал за край — прижимаем трекинг. */
  .hero-title { letter-spacing: -.045em; }
  .hero-title__prompt { margin-right: .1em; }
}

@media (prefers-reduced-motion: reduce) {
  /* Печати нет: текст виден целиком сразу, курсор не мигает. */
  .hero-title__caret { display: none; }
}

/* --------------------------------------------------------- навигация/подвал */
.nav-link {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .4rem .7rem; border-radius: .45rem; font-size: .875rem; font-weight: 500;
  color: var(--ink-dim); text-decoration: none;
  transition: color .18s var(--ease), background .18s var(--ease);
}
.nav-link svg { width: .95rem; height: .95rem; opacity: .8; }
.nav-link:hover { color: var(--ink); background: rgb(var(--tint) / .06); }
.nav-link-active { color: var(--fg-on-tint); background: rgb(var(--tint) / .08); }
.nav-link-active svg { opacity: 1; color: #a5b4fc; }

/* --------------------------------------------------------- меню профиля
   Аватар-кнопка в шапке + выпадающая карточка. Заменяет три отдельных
   блока (почта / админ / выход) и освобождает место на средних экранах. */
.user-trigger {
  display: inline-flex; align-items: center; gap: .55rem;
  padding: .25rem .5rem .25rem .25rem; border-radius: 999px;
  background: rgb(var(--tint) / .04); border: 1px solid var(--line);
  color: var(--ink); cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.user-trigger:hover, .user-trigger[aria-expanded="true"] {
  border-color: rgba(129, 140, 248, .45); background: rgba(99, 102, 241, .12);
}
.user-avatar {
  display: grid; place-items: center; width: 1.75rem; height: 1.75rem; flex: none;
  overflow: hidden;                      /* круглая рамка обрезает и картинку */
  border-radius: 999px; font-size: .8rem; font-weight: 700; color: #fff;
  background: linear-gradient(140deg, #6366f1, #8b5cf6);
  box-shadow: 0 0 0 1px rgb(var(--tint) / .15), 0 0 14px -4px rgba(99, 102, 241, .9);
}
/* Аватарка заполняет кружок целиком: картинка 4000 px иначе уменьшилась
   бы до полосы, а рисованный знак с его viewBox сел бы не по центру.
   Селектор — по метке, а не по «img или svg»: в этом же кружке живут
   обычные значки lucide (карточки переходов в профиле), и правило вида
   «всё, что внутри» растянуло их на весь круг с обрезкой по краю. */
.user-avatar > .user-avatar__pic { width: 100%; height: 100%; display: block; }
.user-avatar--lg { width: 2.25rem; height: 2.25rem; font-size: 1rem; }
/* Крупный круг в карточке профиля: там аватарка и есть объект, а не
   украшение, и 28 px не дают ничего рассмотреть. */
.user-avatar--avatar-lg { width: 4.5rem; height: 4.5rem; font-size: 1.6rem; }

.user-dropdown {
  position: absolute; top: calc(100% + .5rem); right: 0; z-index: 50;
  width: 15.5rem; padding: .35rem;
  border-radius: .8rem;
  background: var(--panel-solid);
  border: 1px solid var(--line-firm);
  box-shadow: 0 26px 60px -24px rgba(0, 0, 0, .95), var(--glow-accent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  transform-origin: top right;
  animation: dropIn .18s var(--ease);
}
.user-dropdown[hidden] { display: none; }
.user-dropdown__head { display: flex; align-items: center; gap: .7rem; padding: .6rem .65rem; }
.user-dropdown__sep { height: 1px; margin: .3rem .35rem; background: var(--line); }
.user-dropdown__item {
  display: flex; align-items: center; gap: .6rem;
  width: 100%; padding: .5rem .65rem; border-radius: .5rem;
  font-size: .82rem; font-weight: 500; color: var(--ink-dim);
  background: transparent; border: none; cursor: pointer; text-decoration: none; text-align: left;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.user-dropdown__item:hover, .user-dropdown__item:focus-visible {
  background: rgb(var(--tint) / .08); color: var(--fg-on-tint);
}
.user-dropdown__item svg { width: 1rem; height: 1rem; flex: none; }
.user-role {
  display: inline-flex; align-items: center;
  padding: .05rem .35rem; border-radius: .3rem;
  background: rgba(99, 102, 241, .18); border: 1px solid rgba(129, 140, 248, .3);
  color: #a5b4fc; font-size: .65rem; font-weight: 600;
}

.foot-link {
  display: inline-flex; align-items: center; gap: .45rem;
  color: var(--ink-dim); text-decoration: none;
  transition: color .16s var(--ease), transform .16s var(--ease);
}
.foot-link svg { width: .85rem; height: .85rem; color: #4b5563; transition: color .16s var(--ease); }
.foot-link:hover { color: var(--fg-on-tint); transform: translateX(2px); }
.foot-link:hover svg { color: #a5b4fc; }

/* Ссылка на игру с её портом справа: колонка узкая, поэтому порт прижат к
   правому краю строки и не растягивает подпись. Моноширинный — чтобы цифры
   выстраивались в столбик, когда названия разной длины. */
.foot-link--ported { display: flex; width: 100%; gap: .5rem; }
.foot-link__port {
  margin-left: auto; flex: none;
  font-family: ui-monospace, 'JetBrains Mono', monospace;
  font-size: .72rem; font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

/* --------------------------------------------------- ПОДВАЛ: знак игры
   Плитка у всех игр одного размера, а размер знака внутри неё — свой: это
   оптическая, а не геометрическая подгонка.

   Раньше размер задавался высотой (--mark-h), и ряд выходил неровным: у
   квадратных знаков плитка занимала 1.35rem целиком, а широкие надписи —
   DayZ, Project Zomboid, Terraria — вписывались в ту же ширину и
   превращались в блёклые полоски в 0.4-0.7rem высотой. Рядом с ними
   квадратный знак читался втрое тяжелее, хотя оба должны были быть
   «иконкой одинакового размера».

   Теперь плитка одна (--foot-mark), квадратный знак занимает её целиком с
   небольшим запасом, а надпись — всю ширину: у надписи вся масса в ширине,
   терять её нельзя. Высота у надписи не задаётся — маска и пропорции
   (--ink-ratio у --word, собственные у <img>) считают её сами. */
.foot-mark {
  display: inline-grid; place-items: center; flex: none;
  width: var(--foot-mark, 1.45rem); height: var(--foot-mark, 1.45rem);
  border-radius: 4px; overflow: hidden;
}
.foot-mark > .game-mark {
  width: 1.2rem; height: 1.2rem;
  object-fit: contain; object-position: center;
}
/* Знак с собственной плиткой (GMod, Minecraft) занимает её целиком —
   у него свой фон, отступать от края незачем. Сама плитка при этом уже
   квадратная, как и знаки без неё: иначе эти две игры выглядели бы
   крупнее остальных на размер всей плитки. */
.foot-mark--tile { width: 1.2rem; height: 1.2rem; }
.foot-mark--tile > .game-mark { width: 100%; height: 100%; object-fit: cover; }
/* Силуэт в маске (CS2, CS 1.6) — квадрат, как и плитка, только с запасом. */
.foot-mark > .game-mark--ink { width: 1.2rem !important; height: 1.2rem !important; }
/* Надпись (DayZ, Project Zomboid, Terraria) — во всю ширину плитки.
   Высота не задаётся: у --word её считает aspect-ratio из --ink-ratio,
   у <img> — собственные пропорции файла. max-width и aspect-ratio у
   .game-mark--ink--word сбрасываются: там они тянут ширину в 2-3 раза
   больше высоты, а здесь ширину задаёт сама плитка. */
.foot-mark > .game-mark--ink--word {
  width: var(--foot-mark, 1.45rem) !important;
  height: auto !important; max-width: none;
  aspect-ratio: var(--ink-ratio);
}
.foot-mark > .game-mark--wide {
  width: var(--foot-mark, 1.45rem) !important;
  height: auto !important; max-width: none;
}

/* --------------------------------------------------- ПОДВАЛ: мини-степпер
   Три шага от регистрации до запущенного сервера. Шапка футера узкая —
   поэтому шаги стоят в ряд, а соединитель рисуется между бейджами:
   тонкая линия с градиентом от бейджа к бейджу плюс шеврон на конце,
   чтобы направление читалось без подписи. На узких экранах ряд не
   помещается, и цепочка встаёт вертикально. */
.foot-steps { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem .85rem; }
.foot-steps__item {
  position: relative; display: flex; align-items: center; gap: .45rem;
  /* место под соединитель справа, кроме последнего шага */
  padding-right: .85rem;
}
.foot-steps__item:last-child { padding-right: 0; }

/* Соединитель: линия с градиентом (гаснет к следующему бейджу) + шеврон.
   Раньше был пунктир 3px с маской — на тёмном фоне он выглядел как
   случайные точки и направление steps было неочевидно. */
.foot-steps__item:not(:last-child)::after {
  content: ""; position: absolute;
  left: calc(100% - .55rem); top: 50%;
  width: .55rem; height: 2px; transform: translateY(-50%);
  border-radius: 2px;
  background-image: linear-gradient(90deg,
              rgba(129, 140, 248, .7), rgba(129, 140, 248, .18));
  box-shadow: 0 0 6px -1px rgba(129, 140, 248, .45);
}
/* Шеврон на конце линии: рамка из двух сторон квадрата, повёрнутого на 45° */
.foot-steps__item:not(:last-child)::before {
  content: ""; position: absolute;
  left: 100%; top: 50%;
  width: .26rem; height: .26rem;
  border-top: 1.5px solid rgba(165, 180, 252, .85);
  border-right: 1.5px solid rgba(165, 180, 252, .85);
  transform: translateY(-50%) rotate(45deg);
}
.foot-steps__badge {
  display: grid; place-items: center; flex: none;
  width: 1.15rem; height: 1.15rem; border-radius: 50%;
  background: rgba(99, 102, 241, .18);
  border: 1px solid rgba(129, 140, 248, .35);
  color: #c7d2fe;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .58rem; font-weight: 700; line-height: 1;
}
.foot-steps__label {
  color: var(--ink-dim); font-size: .72rem; line-height: 1.3;
  white-space: nowrap;
}

@media (max-width: 479px) {
  /* Вертикальная цепочка: горизонтальная переносит подписи и рвёт соединитель.
     Смещение 1.975rem = отступ под бейдж (1.4rem) + его радиус (.575rem):
     иначе линия и шеврон уезжают влево от самих бейджей. */
  .foot-steps { flex-direction: column; align-items: flex-start; gap: .6rem; }
  .foot-steps__item { padding-right: 0; padding-left: 1.4rem; }
  .foot-steps__item:not(:last-child)::after {
    left: calc(1.975rem - 1px); top: 1.15rem; bottom: -.6rem;
    width: 2px; height: auto; transform: none;
    background-image: linear-gradient(180deg,
              rgba(129, 140, 248, .7), rgba(129, 140, 248, .18));
  }
  /* Шеврон поворачивается вниз — по направлению цепочки */
  .foot-steps__item:not(:last-child)::before {
    left: calc(1.975rem - .13rem); top: auto; bottom: -.42rem;
    transform: rotate(135deg);
  }
}

/* ------------------------------------------------------- ПОДВАЛ: соцсети */
/* Отступ сверху задаётся здесь, а не классом mt-* в шаблоне: utility-классы
   Tailwind равны по специфичности с .foot-social и подключаются позже,
   поэтому объявление в разметке молча перебивало бы это правило. */
.foot-social {
  display: flex; flex-wrap: wrap; gap: .5rem;
  margin-top: 2rem;              /* заметная пауза после цепочки шагов */
  padding-top: 1.4rem;           /* и тонкая линия: разрыв читается как намеренный */
  border-top: 1px solid var(--line-soft);
}
.foot-social__btn {
  display: grid; place-items: center;
  /* 1.7rem — по соседям: бейджи шагов 1.15rem, знаки ссылок .85rem.
     Плитка 2.25rem была вдвое крупнее всей колонки и выглядела
     вставленной из другого интерфейса. Радиус .45rem держит пропорцию
     скругления к стороне, как было у 2.25/.6. */
  width: 1.7rem; height: 1.7rem; border-radius: .45rem;
  color: #fff;                 /* глиф знака задан currentColor */
  background: rgb(var(--tint) / .05);
  border: 1px solid transparent;
  text-decoration: none;
  transition: background .18s var(--ease), color .18s var(--ease),
              border-color .18s var(--ease), transform .18s var(--ease);
}
/* Знак занимает всю кнопку: его фирменная плитка и есть кнопка. Раньше знак
   был 1.25rem внутри кнопки 2.25rem, и скруглённая кнопка облепляла
   квадратную плитку — отсюда «наклейка в коробочке». border-radius: inherit
   скругляет и саму плитку (корень <svg> клипует содержимое по радиусу).
   Глиф внутри плитки уже подогнан в social.py под FIT_TARGET, поэтому
   увеличение кнопки не требует пересчёта. */
.foot-social__btn svg {
  width: 100%; height: 100%; border-radius: inherit;
  transition: filter .18s var(--ease);
}
/* Плитка закрывает собой фон кнопки, поэтому «подсветка» фона и цвет рамки
   на активной кнопке не видны — вместо них знак слегка высветляется. */
.foot-social__btn:hover, .foot-social__btn:focus-visible {
  background: rgba(99, 102, 241, .2);
  color: #fff;
  border-color: rgba(129, 140, 248, .35);
  transform: translateY(-1px);
}
.foot-social__btn:hover svg { filter: brightness(1.1); }
/* Обводка фокуса задаётся явно: у плитки свой цвет, и рамка кнопки на фоне
   плитки теряется — без неё табуляция даёт невидимый фокус. */
.foot-social__btn:focus-visible {
  outline: 2px solid #a5b4fc; outline-offset: 2px;
}
.foot-social__btn:active { transform: translateY(0); }

/* Адрес не задан — знак виден, но не кликается: без курсора, без фокуса и
   без обводки при табуляции (это не интерактивный элемент).
   Глиф остаётся белым: он лежит на фирменной плитке, и тёмная заливка
   здесь читалась не как «выключено», а как дыра в знаке. Неактивность
   выражается прозрачностью всей плитки вместе с глифом. */
.foot-social__btn.is-off {
  color: #fff;
  cursor: default;
}
.foot-social__btn.is-off:hover { transform: none; }
.foot-social__btn.is-off:hover svg { filter: none; }
.foot-social__btn.is-off svg { opacity: .42; }
/* Правило ниже красило глиф в серый (#8b95a6) — это писалось под старые
   одноцветные знаки на своём фоне; на цветной плитке давало дыру. */

/* ------------------------------------------- ПОДВАЛ: документы и оплата */
.foot-legal {
  color: var(--ink-dim); text-decoration: none; font-size: .72rem;
  transition: color .16s var(--ease);
}
.foot-legal:hover { color: #c7d2fe; }

/* Плашки способов оплаты намеренно приглушены: это справочная информация,
   а не призыв к действию. Приглушенность даёт рамка и прозрачный фон,
   а не opacity на всей плашке: при opacity .5 текст падал до ~3.4:1 и
   не проходил WCAG AA (проверено axe-core). Приглушённый серый #78829a
   сам по себе даёт 5.1:1 — поэтому гаснет только значок (.5 → 1),
   а подпись остаётся читаемой. */
/* Способы оплаты. nowrap + горизонтальная прокрутка вместо переноса:
   раньше «ЮMoney» уезжал на вторую строку и плашка обвисала ниже
   копирайта. На узком экране ряд прокручивается, но остаётся одной строкой
   и не ломает вертикальный ритм подвала. */
.foot-pay-list {
  display: flex; flex-wrap: nowrap; align-items: center; gap: .4rem;
  flex: none; max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.foot-pay-list::-webkit-scrollbar { display: none; }
.foot-pay-list > li { flex: none; }
.foot-pay {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .22rem .5rem; border-radius: .4rem;
  border: 1px solid rgb(var(--tint) / .07);
  background: rgb(var(--tint) / .025);
  color: var(--ink-faint);
  font-size: .68rem; font-weight: 500; line-height: 1.2;
  transition: color .18s var(--ease), border-color .18s var(--ease),
              background .18s var(--ease);
  white-space: nowrap;
}
.foot-pay__icon {
  width: .8rem; height: .8rem; flex: none;
  opacity: .5; transition: opacity .18s var(--ease);
}
.foot-pay:hover {
  color: var(--ink-dim);
  border-color: rgb(var(--tint) / .18);
  background: rgb(var(--tint) / .06);
}
.foot-pay:hover .foot-pay__icon { opacity: 1; }
/* Пометка «демо»: плашка отвечает, но и не выдаёт себя за рабочую */
.foot-pay__note {
  padding: 0 .3rem; border-radius: .25rem;
  background: rgba(251, 191, 36, .12);
  color: #fbbf24;
  font-size: .58rem; font-weight: 600;
  letter-spacing: .02em; text-transform: uppercase;
}

/* ------------------------------------------------- файловый менеджер
   Список как таблица: имя, размер, дата изменения, действия.
   Действия появляются по наведению или фокусу строки - на месте всегда
   остаётся только иконка файла, иначе пять кнопок на каждую строку
   превращают список в полосу шума. Фокус с клавиатуры раскрывает их так
   же, потому что правило :hover без мыши не работает. */
.fs-head,
.fs-row {
  /* Четыре колонки: иконка, имя, размер, дата. Пятая (действия) вынесена
     из сетки — см. .fs-row__acts ниже. */
  display: grid;
  grid-template-columns: 1.1rem minmax(0, 1fr) 3.6rem 5.2rem;
  align-items: center; gap: .5rem;
}
.fs-head {
  padding: .3rem .5rem .45rem; margin-bottom: .15rem;
  border-bottom: 1px solid var(--line-soft);
  font-size: .62rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-faint);
}
.fs-head span:nth-child(3), .fs-head span:nth-child(4) { text-align: right; }
.fs-row {
  position: relative;
  width: 100%; padding: .4rem .5rem; border-radius: .5rem;
  cursor: pointer; text-align: left;
  transition: background .16s var(--ease);
}
.fs-row:hover, .fs-row:focus-within { background: rgb(var(--tint) / .06); }
.fs-row__icon { width: 1rem; height: 1rem; flex: none; }
.fs-row__name {
  min-width: 0; font-size: .78rem; color: var(--ink-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fs-row.is-dir .fs-row__name { color: var(--ink); }
.fs-row__size, .fs-row__date {
  text-align: right; font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .64rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
/* Кнопки действий вынесены из потока колонок: до наведения они скрыты через
   opacity, но место под них grid всё равно резервирует — у файла их три, и в
   панели 320px колонка имени схлопывалась в ноль. Теперь действия лежат
   поверх строки справа и появляются по наведению, а имя занимает всю
   свободную ширину. Подложка нужна, чтобы под кнопками не просвечивал текст. */
.fs-row__acts {
  position: absolute; right: .4rem; top: 50%;
  transform: translateY(-50%);
  display: flex; gap: .15rem;
  padding-left: .9rem;
}
/* Подложка — только под наведением. Пока кнопок не видно, она лишь закрывала
   бы дату у всех строк списка. Цвет совпадает с фоном строки при наведении. */
.fs-row:hover .fs-row__acts, .fs-row:focus-within .fs-row__acts {
  background: linear-gradient(90deg, transparent, var(--row-hover) 45%);
}
/* Наведение освобождает дату: под кнопками обрезанный текст читается хуже,
   чем его отсутствие, а на самой дате при наведении смотреть некуда. */
.fs-row__date { transition: opacity .16s var(--ease); }
.fs-row:hover .fs-row__date, .fs-row:focus-within .fs-row__date { opacity: 0; }
/* Скрыты визуально, но не выключены из дерева доступности: фокус с
   клавиатуры должен по-прежнему доходить до кнопок строки. */
.fs-row__acts > * { opacity: 0; transition: opacity .16s var(--ease); }
.fs-row:hover .fs-row__acts > *,
.fs-row:focus-within .fs-row__acts > * { opacity: 1; }
.fs-row__acts > .is-locked { opacity: .35; }
.fs-row__acts > *:focus-visible { opacity: 1; }
.fs-row__acts .btn-icon:disabled { opacity: .3; cursor: not-allowed; }
@media (max-width: 420px) {
  /* на телефоне скрываем дату: колонок и без того впритык, а размер важнее */
  .fs-head, .fs-row { grid-template-columns: 1.1rem minmax(0, 1fr) 3.4rem; }
  .fs-head span:nth-child(4), .fs-row__date { display: none; }
}
/* ------------------------------------------------------------ боковое меню
   Панель слева на десктопе. Ширина фиксирована и совпадает с --sb-w,
   которым сдвигается содержимое, иначе панель наезжала бы на текст.
   Ниже 1024px панель скрыта целиком, а шапка с бургером работает сама. */
.sidebar { display: none; }

@media (min-width: 1024px) {
  :root {
    --sb-w: 15rem;
    /* Длительности раскрытия панели. Раскрытие длиннее схлопывания: человек
       наводит курсор и смотрит, как панель раскрывается — тут торопиться
       некуда, а короткое движение читалось бы как рывок. Схлопывание, наоборот,
       короткое: оно начинается сразу, как только курсор ушёл, и длинный хвост
       анимации тянул бы за собой уходящую панель.

       Кривая своя, а не общая --ease: та заточена под появление (резкий
       выход, быстрое начало) и на ширине панели читается как рывок с
       замедлением. Здесь нужно мягкое начало — панель «отъезжает» от
       края экрана, и она должна начинать движение вместе с курсором. */
    --sb-ease: cubic-bezier(.25, .8, .3, 1);
    --sb-dur-in: .38s;
    --sb-dur-out: .24s;
    /* Текст меню появляется вместе с раскрытием панели, а не после него.

       Задержка намеренно НЕ равна полной длине раскрытия: подпись в nowrap
       не помещается в ещё узкую панель и вылезает наружу поверх страницы.
       Поэтому она ждёт момента, когда панель раскрылась примерно на 90%, а
       дальше набор идёт быстро — к концу раскрытия текст уже готов.

       Это число читает и JS (static/app.js), который набирает буквы по одной.
       Отсюда оно и живёт переменной, а не продублировано числом в обоих
       местах: иначе подписи проявлялись бы и набирались в разное время. */
    --sb-text-delay: 90ms;
    --sb-text-dur: .1s;
  }

  /* Свёрнутое состояние выбирает JS и ставит атрибут на <html>, а не класс на
     панели: значение нужно до первой отрисовки (скрипт в <head>), иначе на
     узком экране панель мелькнёт развёрнутой. Ширина в свёрнутом виде — под
     одну иконку. Наведение раскрывает панель до 15rem, и тогда --sb-w для
     содержимого становится таким же: страница уезжает вправо вместе с
     панелью, а не остаётся под ней (правило ниже, на :has). */
  html[data-sidebar="collapsed"] { --sb-w: 4.75rem; }

  /* Гость: панели нет вовсе, лендинг занимает всю ширину.
     Правило стоит после html[data-sidebar="collapsed"] — у обоих одна
     специфичность (тип + атрибут), и без этого порядка состояние
     «свёрнуто», сохранённое в localStorage, перебило бы гостя и оставило
     бы пустое поле слева. */
  html[data-auth="0"] { --sb-w: 0; }

  .sidebar {
    display: flex; flex-direction: column; gap: .35rem;
    position: fixed; inset: 0 auto 0 0; z-index: 45;
    width: var(--sb-w); padding: 1rem .75rem;
    background: var(--panel);
    backdrop-filter: blur(20px) saturate(160%);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    border-right: 1px solid var(--line);
    /* без overflow-y: подсказки пунктов рисуются справа от панели и обрезались бы */
    transition: width var(--sb-dur-out) var(--sb-ease);
  }

  .sidebar__head {
    display: flex; align-items: center;
    /* высота как у шапки: при переходе логотип не «прыгает» вверх */
    min-height: 3.75rem; margin-bottom: .5rem;
    padding: 0 .5rem;
  }
  /* Логотип и монограмма занимают одну коробку и меняются прозрачностью.
     Раньше каждая включалась и выключалась через display — надпись
     «OnUse» появлялась скачком, а не перетекала из букв «GD». */
  .sidebar__logo {
    position: relative; flex: 1; min-width: 0; height: 2.25rem;
    display: flex; align-items: center;
  }
  /* Монограмма — буквы без плитки. Раньше здесь был фиолетовый квадрат со
     скруглением, и в свёрнутом виде панель начиналась лишним пятном, а сами
     буквы читались хуже, чем слово «OnUse» рядом. Теперь цвета те же,
     что у слова: первая буква — цвет текста (светлая в тёмной теме,
     тёмная в светлой), вторая — фиолетовый градиент хвоста.

     По центру, а не от левого края коробки: в свёрнутом виде под монограммой
     стоят знаки пунктов, они отцентрированы по своей колонке, и буквы,
     прижатые к левому отступу панели, с ними не вставали в одну линию.
     Центрируем и по горизонтали, и по вертикали — отсюда сдвиг на -50% в
     transform, который заодно несёт анимацию появления. */
  .sidebar__monogram {
    position: absolute; left: 50%; top: 50%;
    display: inline-flex; align-items: baseline;
    font-size: 1.15rem; font-weight: 800; letter-spacing: -.02em;
    line-height: 1; white-space: nowrap;
    opacity: 0; transform: translate(-50%, -50%) scale(.55); filter: blur(2px);
    transition: opacity .28s var(--sb-ease), transform .34s var(--sb-ease),
                filter .34s var(--sb-ease);
  }
  .sidebar__monogram-a { color: var(--ink); }
  .sidebar__monogram-b {
    background-image: linear-gradient(100deg, #818cf8, #c084fc);
    -webkit-background-clip: text; background-clip: text;
    color: transparent; -webkit-text-fill-color: transparent;
  }
  .sidebar__brand {
    display: block; min-width: 0;
    opacity: 0; transform: translateX(-.5rem); filter: blur(3px);
    transition: opacity .34s var(--sb-ease), transform .34s var(--sb-ease),
                filter .34s var(--sb-ease);
  }
  html[data-sidebar="collapsed"] .sidebar__monogram {
    opacity: 1; transform: translate(-50%, -50%) scale(1); filter: none;
  }
  html[data-sidebar="collapsed"] .sidebar__brand { opacity: 0; }
  html[data-sidebar="collapsed"] .sidebar__foot { align-items: stretch; }

  /* Ключевых кадров sbType здесь больше нет: буквы набирает Web Animations
     API прямо из JS, мимо таблицы стилей. Плюс в этом выигрыш — кадры
     задаются на самой анимации, и пересобирать CSS при смене темпа не нужно. */

  /* ---- наведение раскрывает панель целиком ----
     Панель растёт и сдвигает за собой страницу: --sb-w для содержимого
     следует за ней, поэтому текст не остаётся лежать под раскрытой панелью.
     Раньше --sb-w на наведении намеренно оставался узким, панель росла
     поверх текста и отделялась от него тенью — выглядело это как слой,
     брошенный поверх страницы.

     Правило через :has(), потому что --sb-w читают main и footer, а панель
     — их сосед: задать переменную на самой .sidebar нельзя, она не доходит
     до соседей по дереву. Через :has() значение меняется на <html> и
     наследуется всеми потомками.

     Уходить панель будет по тем же правилам — как только курсор покинул её
     целиком, а не тот пункт, под которым стоял. Панель остаётся на месте
     (position: fixed), поэтому сдвиг страницы не уводит курсор с панели и
     «мигания» вроде «раскрылась → ушла → раскрылась» не возникает.

     Побочный эффект, о котором стоит знать: при раскрытии текст
     переверстывается — колонки становятся уже на ширину панели. Это
     ровно то «двигает страницу», ради которого правило и добавлено, но
     на очень узком окне текст может перенестись неудачно.

     Тени у раскрытой панели больше нет — по просьбе. Её роль была в том,
     чтобы отделить слой, лежащий поверх текста; теперь панель не лежит
     поверх текста, а сдвигает его, и падающая вправо тень только мусорила
     полосу между панелью и страницей. Разделение теперь делает сама
     граница .sidebar (border-right) и сдвиг содержимого. */

  html[data-sb-open] { --sb-push: 10.25rem; }
  /* Содержимое едет за панелью в те же сроки: если бы сдвиг шёл по общей
     .24s, а панель раскрывалась по .38s, текст убегал бы вперёд панели и
     наезжал на неё с другой стороны. */
  html[data-sb-open] .page-shift { transition-duration: var(--sb-dur-in); }

  html[data-sidebar="collapsed"] .sidebar:hover {
    width: 15rem;
    transition-duration: var(--sb-dur-in);
  }

  /* Подписи схлопываются быстрее самой панели. Иначе буквы «OnUse» и
     названия пунктов продолжали бы доезжать уже в узкой панели, где им
     негде поместиться, и текст обрезался на середине перехода.

     Селектор с :not(:hover) — на раскрытой панели правило не действует и
     подписи едут в свои .28–.34s. По одной специфичности (0,4,1) против
     базовых (0,1,1) порядок в файле значения не имеет. */
  html[data-sidebar="collapsed"] .sidebar:not(:hover) .sidebar__monogram,
  html[data-sidebar="collapsed"] .sidebar:not(:hover) .sidebar__brand {
    transition-duration: var(--sb-dur-out);
  }
  /* Логотип и подписи пунктов возвращаются на место тем же перекрёстным
     затуханием, что и при переключении: буквы «GD» сжимаются, «OnUse»
     вырастает на них же. */
  html[data-sidebar="collapsed"] .sidebar:hover .sidebar__monogram {
    opacity: 0; transform: translate(-50%, -50%) scale(.55); filter: blur(2px);
  }
  html[data-sidebar="collapsed"] .sidebar:hover .sidebar__brand {
    opacity: 1; transform: none; filter: none;
    /* Ждёт раскрытия панели, затем появляется быстро — как подписи пунктов. */
    transition: opacity var(--sb-text-dur) var(--sb-ease) var(--sb-text-delay),
                transform var(--sb-text-dur) var(--sb-ease) var(--sb-text-delay),
                filter var(--sb-text-dur) var(--sb-ease) var(--sb-text-delay);
  }
  html[data-sidebar="collapsed"] .sidebar:hover .sidebar__foot { align-items: initial; }
  html[data-sidebar="collapsed"] .sidebar:hover [data-label] {
    justify-content: flex-start; padding: .55rem .6rem;
  }
  html[data-sidebar="collapsed"] .sidebar:hover [data-label] .side-link__label {
    position: static; width: auto; height: auto; padding: 0; margin: 0;
    overflow: visible; clip: auto; white-space: nowrap; border: 0;
    opacity: 1;
    /* Подпись ждёт, пока панель раскроется, и встаёт быстро. Задержка задана
       шириной панели, а не числом: если длительность раскрытия поменяется,
       текст подстроится сам. */
    transition: opacity var(--sb-text-dur) var(--sb-ease) var(--sb-text-delay);
  }
  /* Подсказка по наведению нужна только у свёрнутой панели: пока панель
     раскрыта, название пункта и так написано рядом со значком. */
  html[data-sidebar="collapsed"] .sidebar:hover [data-label]:hover::after,
  html[data-sidebar="collapsed"] .sidebar:hover [data-label]:focus-visible::after {
    content: none;
  }
  /* Имя и почта появляются по тому же расписанию, что и подписи пунктов.
     Здесь нужен именно animation, а не transition: подпись блока переключается
     через display, а display не анимируется — переход бы просто не сработал,
     и текст щёлкнул бы мгновенно в начале раскрытия. */
  html[data-sidebar="collapsed"] .sidebar:hover .sidebar__user-text {
    display: block;
    animation: sbTextIn var(--sb-text-dur) var(--sb-ease) var(--sb-text-delay) both;
  }
  @keyframes sbTextIn {
    from { opacity: 0; transform: translateX(-.35rem); }
    to   { opacity: 1; transform: none; }
  }
  html[data-sidebar="collapsed"] .sidebar:hover .sidebar__user {
    justify-content: flex-start; padding: .2rem .5rem .5rem;
  }
  html[data-sidebar="collapsed"] .sidebar:hover .btn-primary { font-size: inherit; gap: .45rem; }
  html[data-sidebar="collapsed"] .sidebar:hover .btn-primary svg { width: 1rem; height: 1rem; }

  .sidebar__nav { display: flex; flex-direction: column; gap: .15rem; flex: 1; min-height: 0; }
  .sidebar__foot {
    display: flex; flex-direction: column; gap: .35rem;
    padding-top: .7rem; margin-top: .5rem;
    border-top: 1px solid var(--line);
  }
  /* Блок пользователя — ссылка на страницу аккаунта (раньше это был просто
     div: нажать на почту было нечем). Поэтому здесь то, что раньше давал
     .side-link, — снятие подчёркивания, свои состояния наведения и рамка
     под текущий раздел, чтобы было видно, что страница уже открыта. */
  .sidebar__user {
    display: flex; align-items: center; gap: .6rem; padding: .2rem .5rem .5rem;
    border-radius: .55rem; text-decoration: none;
    transition: background .18s var(--ease);
  }
  .sidebar__user:hover { background: rgb(var(--tint) / .06); }
  .sidebar__user:focus-visible { outline: 2px solid rgba(129, 140, 248, .75); outline-offset: 2px; }
  .sidebar__user.is-active { background: rgba(99, 102, 241, .16); }
  html[data-theme="light"] .sidebar__user.is-active { background: rgba(99, 102, 241, .14); }
  .sidebar__user:hover .sidebar__user-chevron { color: var(--ink-dim); }
  html[data-sidebar="collapsed"] .sidebar__user-chevron { display: none; }

  .side-link {
    position: relative;
    display: flex; align-items: center; gap: .65rem;
    padding: .55rem .6rem; border-radius: .55rem;
    font-size: .875rem; font-weight: 500; color: var(--ink-dim);
    text-decoration: none; cursor: pointer;
    transition: color .18s var(--ease), background .18s var(--ease);
  }
  .side-link svg { width: 1rem; height: 1rem; flex: none; opacity: .75; }
  .side-link:hover { color: var(--ink); background: rgb(var(--tint) / .06); }
  .side-link.is-active { color: #fff; background: rgba(99, 102, 241, .16); }
  /* Активный пункт подписан по-своему в каждой теме: в тёмной подложка
     индиговая и белая подпись контрастна, в светлой та же подложка светлеет,
     и белая надпись на ней не читается. */
  html[data-theme="light"] .side-link.is-active { color: #312e81; background: rgba(99, 102, 241, .14); }
  .side-link.is-active svg { opacity: 1; color: var(--brand-400); }
  .side-link--danger { color: #fca5a5; }
  .side-link--danger:hover { background: rgba(239, 68, 68, .14); color: var(--fg-on-tint); }
  .side-link__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Подсказка свёрнутой панели берётся из data-label, а её текст меняет скрипт
     вместе с подписью: иначе рядом со значком читалось бы старое «Сменить
     тему», хотя кнопка уже переключила. */
  /* Поворот знака на смене темы здесь больше не задаётся: он живёт в
     общем правиле [data-theme-icon] в начале файла, где уходят и
     прозрачность, и поворот. Правило вида [data-theme-toggle] svg
     перебивало то по специфичности (0,1,1 против 0,1,0), и в переход
     попадал только rotate: знак крутился, но в другой не перетекал, а
     просто исчезал. */

  /* ---- свёрнуто: только иконки, подпись живёт в атрибуте и в подсказке ----
     Правила ниже, в блоке «наведение раскрывает только один пункт»:
     свёрнутое состояние и подсказка неразделимы, иначе подпись мелькнула бы
     при переключении. */
  html[data-sidebar="collapsed"] .sidebar__foot { align-items: stretch; }

  /* ---- свёрнуто: наведение раскрывает только один пункт ----
     Раньше наведение раскрывало панель целиком, и на узком экране это
     перекрывало полосу контента и уводило курсор с пункта. Теперь панель
     остаётся узкой, а название вылезает только у того пункта, который под
     курсором: одна «плашечка» вместо раскрытия всего меню. */
  html[data-sidebar="collapsed"] .sidebar [data-label] {
    position: relative; justify-content: center; padding: .55rem 0;
  }
  html[data-sidebar="collapsed"] .sidebar [data-label] .side-link__label {
    /* не display:none: подпись убралась бы из дерева доступности,
       и экранный диктор не назвал бы пункт */
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    /* opacity нужен переходу: без него подпись меняла бы видимость щелчком
       в начале раскрытия панели, когда та ещё узкая, и nowrap-текст вылезал
       бы наружу поверх страницы. На раскрытой панели это правило не
       действует — подпись видна как обычно. */
    opacity: 0;
    transition: opacity var(--sb-dur-out) var(--sb-ease);
  }
  html[data-sidebar="collapsed"] .sidebar [data-label]:hover::after,
  html[data-sidebar="collapsed"] .sidebar [data-label]:focus-visible::after {
    content: attr(data-label);
    position: absolute; left: calc(100% + .7rem); top: 50%; transform: translateY(-50%);
    z-index: 3; padding: .3rem .55rem; border-radius: .4rem; white-space: nowrap;
    font-size: .75rem; font-weight: 500; color: var(--ink);
    background: var(--bg-raised); border: 1px solid var(--line-firm);
    box-shadow: 0 14px 34px -14px rgba(0, 0, 0, .95);
    pointer-events: none;
    /* появление мгновенное: задержка отслеживания задерживала бы панель */
    animation: sbTip .12s var(--ease);
  }
  @keyframes sbTip {
    from { opacity: 0; transform: translateY(-50%) translateX(-4px); }
    to   { opacity: 1; transform: translateY(-50%) translateX(0); }
  }

  html[data-sidebar="collapsed"] .sidebar__user-text { display: none; }
  html[data-sidebar="collapsed"] .sidebar__user {
    justify-content: center; padding: .2rem 0 .5rem; position: relative;
  }
  /* Кнопка «Начать» у гостя — тоже пункт меню: без подписи и знака в
     свёрнутом виде она была пустой фиолетовой полосой. */
  html[data-sidebar="collapsed"] .sidebar .btn-primary { font-size: 0; gap: 0; }
  html[data-sidebar="collapsed"] .sidebar .btn-primary svg { width: 1rem; height: 1rem; }

  html[data-auth="0"] .sidebar { display: none; }

  /* содержимое и подвал уезжают вправо ровно на ширину панели */
  .site-header { display: none; }
  .page-shift {
    padding-left: var(--sb-w);
    /* Сдвиг — трансформацией, а не отступом. Отступ пересчитывает раскладку
       всего дерева на каждом кадре анимации: под ним вся страница, и каждый
       кадр раскрытия панели вставал на полную перевёрстку. Трансформация
       двигает уже готовую картинку и уходит композитору, раскладку не трогая
       вовсе.

       Величина сдвига — разница между свёрнутой и раскрытой панелями, а не
       сама ширина: постоянный отступ задаёт --sb-w, и он живёт своей жизнью
       (меняется только при пересборке по ширине окна). Плюс 15rem поверх
       сдвигал бы страницу вдвое дальше нужного.

       Побочный эффект, о котором стоит знать: текст теперь не переверстывается
       при раскрытии — колонки остаются своей ширины и просто уезжают вправо.
       Для панели это ровно то и нужно: раньше текст переносился на каждом
       наведении, и в узкой колонке перенос выглядел случайным.

       Трансформация делает <main> содержащим блоком для position:fixed —
       внутри <main> таких элементов нет (тосты и виджет поддержки стоят
       после него в base.html), так что на них это не сказывается. */
    transform: translateX(var(--sb-push, 0px));
    transition: transform var(--sb-dur-out) var(--sb-ease);
  }
}
/* ------------------------------------------------------------ мобильное меню */
.nav-toggle { display: none; }
.mobile-nav { display: none; }

/* 1023.98px, а не 1023px: при дробном масштабе окна (dpr 1.25/1.5) реальная
   ширина вьюпорта бывает 1023.5 — она не попадает ни в max-width:1023px, ни
   в min-width:1024px, и бургер пропадает вместе с панелью. Смещение на 0.02px
   закрывает эту полосу в один пиксель. */
@media (max-width: 1023.98px) {
  .nav-toggle { display: inline-grid; }
  .mobile-nav {
    display: block; position: absolute; left: 0; right: 0; top: 100%;
    padding: .6rem .9rem .9rem;
    background: var(--panel);
    backdrop-filter: blur(20px) saturate(160%);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--panel-shadow);
    transform-origin: top; transition: opacity .22s var(--ease), transform .22s var(--ease);
  }
  .mobile-nav[hidden] { display: none; }
  .mobile-nav.is-open { animation: dropIn .24s var(--ease); }
  .mobile-nav a {
    display: flex; align-items: center; gap: .7rem;
    padding: .75rem .8rem; border-radius: .6rem;
    font-size: .95rem; font-weight: 500; color: var(--ink-dim); text-decoration: none;
  }
  .mobile-nav a svg { width: 1.05rem; height: 1.05rem; color: var(--ink-faint); }
  .mobile-nav a:hover, .mobile-nav a:active { background: rgb(var(--tint) / .08); color: var(--ink); }
  .mobile-nav a.is-active { background: rgba(99, 102, 241, .16); color: var(--ink); }
  .mobile-nav a.is-active svg { color: var(--fg-accent); }
}
@keyframes dropIn {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}

/* ==========================================================================
   10. ТОСТЫ, МОДАЛКИ, УТИЛИТЫ
   ========================================================================== */
.toast {
  display: flex; align-items: flex-start; gap: .7rem; padding: .85rem 1rem;
  border-radius: .9rem; font-size: .84rem; font-weight: 500;
  background: var(--panel-solid); border: 1px solid var(--line-firm);
  box-shadow: 0 20px 45px -18px #000, var(--glow-accent); backdrop-filter: blur(14px);
  animation: slideIn .32s var(--ease); pointer-events: auto;
}
@keyframes slideIn { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: none; } }
.toast-ok   { border-color: rgba(52, 211, 153, .45); }
.toast-err  { border-color: rgba(248, 113, 113, .45); }
.toast-info { border-color: rgba(129, 140, 248, .45); }

.glow-line { height: 1px; background: var(--line); }

/* Итоговая сумма: подпрыгивает при пересчёте */
.price-pop { animation: pop .34s var(--ease); }
@keyframes pop {
  0%   { transform: scale(.94); opacity: .6; }
  60%  { transform: scale(1.03); }
  100% { transform: scale(1); opacity: 1; }
}
/* Строка слагаемых подсвечивается, когда сумма изменилась */
.sum-row { transition: color .2s var(--ease), background .2s var(--ease); border-radius: .3rem; }
.sum-row.is-changed { color: #fff; background: rgba(99, 102, 241, .12); }

/* Плавающая итоговая карточка */
.summary {
  position: sticky; top: 5.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgb(var(--tint) / .05), rgb(var(--tint) / .015));
  background-color: var(--bg-raised);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, .95), var(--glow-accent);
  transition: box-shadow .3s var(--ease), border-color .3s var(--ease);
}
.summary.is-bumped { box-shadow: 0 30px 60px -30px rgba(0,0,0,.95), 0 0 34px -4px color-mix(in srgb, var(--accent) 60%, transparent); }

/* ================================================================ ВЫДЕЛЕНИЕ
   Мышь не выделяет текст почти нигде: синяя полоса поверх расчёта, мастера
   или ленты возникала при каждом движении ползунка и клике по плитке, а
   толку от неё не было нигде.

   Раньше это было шесть отдельных правил — по одному на сводку, мастер,
   подвал, верхний экран, карусель игр и вступление конструктора. Теперь
   одно общее: локальные зоны со временем разъезжались (новую секцию
   забывали), а перечислять их ради одного свойства смысла не было.

   pointer-events НЕ трогаем — он гасил бы наведение, фокус и сами нажатия.
   Выделение и клик — разные вещи: правило ниже не мешает ни нажатию на
   ссылку, ни вводу текста в поле, ни перетаскиванию ленты.

   ИСКЛЮЧЕНИЯ, первый сорт — активные элементы: их подписи человек вправе
   взять в буфер, а на заблокированной кнопке выделять нечего.

     a, button:not(:disabled), input, textarea, select, [contenteditable]

   ИСКЛЮЧЕНИЯ, второй сорт — то, что человек пришёл копировать, а не
   читать. Это не активные элементы, но без них страница теряет смысл:

     pre             — блоки команд и конфигов в гайде (/guide): кнопок
                       «скопировать» там нет, это единственный способ унести
                       команду оттуда;
     .console-line   — строки лога сервера: их копируют, чтобы показать
                       ошибку или отдать в поддержку;
     .wiz-coupon__code — код купона в мастере, его надо сохранить.

   Список узкий и осознанный. Адрес и код сервера копируются кнопками
   (data-copy) — их трогать не нужно. */
body,
body * { user-select: none; -webkit-user-select: none; }
body a,
body button:not(:disabled),
body input,
body textarea,
body select,
body [contenteditable] {
  user-select: text;
  -webkit-user-select: text;
}
/* Вложенные элементы — обязательная часть правила. Исключение на сам <pre>
   мало: текст лежит в <code> внутри него, а на <code> продолжало действовать
   общее правило, и блок выглядел выделяемым по стилю, пока выделить его
   было нельзя. Проверено протягиванием: user-select в text, а буфер пуст. */
body pre,
body pre *,
body .console-line,
body .wiz-coupon__code {
  user-select: text;
  -webkit-user-select: text;
}

/* Плавающая кнопка-подсказка «вверх» при длинной странице */
.to-top {
  position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 30;
  opacity: 0; pointer-events: none; translate: 0 .5rem;
  transition: opacity .25s var(--ease), translate .25s var(--ease);
}
.to-top.is-visible { opacity: 1; pointer-events: auto; translate: 0 0; }

/* ============================================================ ПОДДЕРЖКА (AI)
   Виджет в правом нижнем углу. Кнопка встаёт над «наверх», чтобы не
   перекрывать её: обе живут в одной точке экрана. */
.support-fab {
  position: fixed; right: 1.25rem; bottom: 4.25rem; z-index: 45;
  display: grid; place-items: center; width: 3.25rem; height: 3.25rem;
  border-radius: 1rem; color: #fff;
  background: linear-gradient(140deg, #6366f1, #8b5cf6);
  border: 1px solid rgba(129, 140, 248, .4);
  box-shadow: 0 10px 30px -12px rgba(99, 102, 241, .9);
  transition: transform .22s var(--ease), box-shadow .22s var(--ease), opacity .22s var(--ease);
}
.support-fab:hover { transform: translateY(-2px); box-shadow: 0 14px 34px -12px rgba(99, 102, 241, 1); }
.support-fab svg { width: 1.25rem; height: 1.25rem; }
body.support-open .support-fab { opacity: 0; pointer-events: none; transform: scale(.8); }
/* Точка «есть непрочитанное»: ненавязчивая, но заметная */
.support-fab__dot {
  position: absolute; top: -.3rem; right: -.3rem;
  width: .7rem; height: .7rem; border-radius: 50%;
  background: #f59e0b; border: 2px solid var(--bg-card);
  display: none;
}
.support-fab.has-new .support-fab__dot { display: block; }

.support-panel {
  position: fixed; right: 1.25rem; bottom: 8rem; z-index: 46;
  display: flex; flex-direction: column;
  width: min(24rem, calc(100vw - 2rem)); height: min(32rem, calc(100vh - 10rem));
  border: 1px solid var(--line-firm); border-radius: 1.1rem;
  background: var(--bg-raised, #12151f);
  box-shadow: 0 30px 70px -28px rgba(0, 0, 0, .95);
  overflow: hidden;
  /* Состояние окна живёт в .is-open, а не в атрибуте hidden: display: none
     переключается мгновенно, и закрытие выглядело бы обрывом на полпути.
     Панель остаётся в потоке и играет transition — поэтому до первой
     отрисовки (до загрузки support.js) она невидима и не ловит клики.
     Точка отсчёта — угол кнопки: окно вырастает из неё, а не из центра. */
  transform-origin: bottom right;
  opacity: 0; visibility: hidden; transform: translateY(14px) scale(.95);
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility .2s;
}
/* Раскрытие дольше и мягче схлопывания: открытие должно читаться как
   «выросло», а закрытие — как отзывчивая реакция, а не как вялое утекание.
   visibility переключается в конце интервала (дискретная анимация), поэтому
   всё это время окно не перехватывает клики и не мешает скроллу. */
.support-panel.is-open {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .24s var(--ease), transform .34s cubic-bezier(.22, 1, .36, 1),
              visibility .34s;
}
@media (prefers-reduced-motion: reduce) {
  .support-panel, .support-panel.is-open { transition: none; }
}
.support-head {
  display: flex; align-items: center; gap: .7rem;
  padding: .85rem 1rem; border-bottom: 1px solid var(--line);
  background: linear-gradient(140deg, rgba(99, 102, 241, .16), transparent 70%);
}
.support-avatar {
  display: grid; place-items: center; flex: none;
  width: 2.1rem; height: 2.1rem; border-radius: .7rem; color: #fff;
  background: linear-gradient(140deg, #6366f1, #8b5cf6);
}
.support-avatar svg { width: 1.1rem; height: 1.1rem; }
.support-title { font-size: .84rem; font-weight: 700; color: var(--ink); line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Строка статуса не переносится: иначе «оператор подключается» ломает
   шапку на две строки и сдвигает всю панель. Модель/режим уезжают в
   конец и обрезаются, если места мало. */
.support-status {
  display: flex; align-items: center; gap: .35rem; min-width: 0;
  font-size: .7rem; color: var(--ink-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.support-status > span { overflow: hidden; text-overflow: ellipsis; }
/* Уточнение строки статуса. Раньше здесь стояла утилита opacity-60: на
   светлой панели она выдавливала подпись примерно до 2:1, и «база знаний»
   вместе с числом тем почти не читалось. Токен даёт ~4.8:1 в обеих темах. */
.support-meta { color: var(--ink-faint); }
.support-status .dot { width: 6px; height: 6px; flex: none; }
.support-status .dot-online { background: #34d399; box-shadow: 0 0 6px rgba(52, 211, 153, .8); }
.support-status .dot-waiting { background: #fbbf24; box-shadow: 0 0 6px rgba(251, 191, 36, .8); }

.support-log { flex: 1; overflow-y: auto; padding: .9rem 1rem; display: flex; flex-direction: column; gap: .6rem; }
.support-log::-webkit-scrollbar { width: 6px; }
.support-log::-webkit-scrollbar-thumb { background: rgb(var(--tint) / .12); border-radius: 999px; }

.support-msg { max-width: 86%; padding: .55rem .75rem; border-radius: .8rem; font-size: .82rem; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
.support-msg--user { align-self: flex-end; color: #fff; background: linear-gradient(140deg, #6366f1, #7c3aed); border-bottom-right-radius: .25rem; }
.support-msg--ai { align-self: flex-start; color: var(--ink); background: rgb(var(--tint) / .05); border: 1px solid var(--line); border-bottom-left-radius: .25rem; }
.support-msg--admin { align-self: flex-start; color: #d1fae5; background: rgba(16, 185, 129, .12); border: 1px solid rgba(52, 211, 153, .3); border-bottom-left-radius: .25rem; }
.support-msg--system { align-self: center; max-width: 95%; padding: .3rem .6rem; border-radius: .5rem; font-size: .7rem; color: var(--ink-faint); background: rgb(var(--tint) / .03); text-align: center; }
.support-msg__time { display: block; margin-top: .25rem; font-size: .62rem; opacity: .6; }
.support-msg__src { display: inline-block; margin-top: .3rem; font-size: .6rem; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-faint); }
/* В админской переписке подпись автора идёт перед текстом и должна быть
   отдельной строкой — иначе она приклеивается к первому слову ответа. */
.support-log--admin .support-msg__src { display: block; margin: 0 0 .25rem; }
/* На фиолетовой плашке пользователя приглушённый серый нечитаем —
   для тёмных пузырей подпись светлее. */
.support-log--admin .support-msg--user .support-msg__src { color: rgb(var(--tint) / .75); }

.support-banner {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 1rem .6rem; padding: .5rem .65rem;
  border-radius: .6rem; font-size: .74rem;
  background: rgba(251, 191, 36, .1); border: 1px solid rgba(251, 191, 36, .3); color: #fcd34d;
}
.support-banner[hidden] { display: none; }

.support-foot { border-top: 1px solid var(--line); padding: .7rem .8rem; display: grid; gap: .55rem; }
.support-input {
  width: 100%; resize: none; min-height: 2.6rem; max-height: 7rem;
  padding: .55rem .7rem; border-radius: .6rem;
  border: 1px solid var(--line); background: rgb(var(--tint) / .04);
  color: var(--ink); font-size: .82rem; line-height: 1.4;
}
.support-input:focus { outline: none; border-color: rgba(129, 140, 248, .55); }
.support-actions { display: flex; align-items: center; gap: .5rem; }
.support-actions .btn-primary { padding: .5rem .8rem; font-size: .8rem; }
.support-actions .support-send { margin-left: auto; }
/* Кнопки быстрых вопросов: помогают, пока пользователь формулирует мысль */
.support-chips { display: flex; flex-wrap: wrap; gap: .35rem; }
.support-chip {
  padding: .25rem .5rem; border-radius: 999px; font-size: .7rem;
  border: 1px solid var(--line); background: rgb(var(--tint) / .03); color: var(--ink-dim);
  transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.support-chip:hover { color: var(--ink); border-color: rgba(129, 140, 248, .45); background: rgba(99, 102, 241, .16); }
.support-chips--hidden { display: none; }

@media (max-width: 640px) {
  .support-panel { right: .6rem; left: .6rem; bottom: 7.5rem; width: auto; height: min(30rem, calc(100vh - 9rem)); }
  .support-fab { right: .9rem; bottom: 4rem; }
}

/* --------------------------------------------- ПОДДЕРЖКА: админ-дашборд тикетов */
.thread-card {
  display: block; width: 100%; text-align: left;
  padding: .75rem .85rem; border-radius: .8rem; cursor: pointer;
  border: 1px solid var(--line); background: rgb(var(--tint) / .022);
  transition: border-color .16s var(--ease), background .16s var(--ease), transform .16s var(--ease);
}
.thread-card:hover { border-color: var(--line-firm); background: rgb(var(--tint) / .05); }
.thread-card.is-active {
  border-color: rgba(129, 140, 248, .55); background: rgba(99, 102, 241, .14);
}
.thread-status {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .12rem .4rem; border-radius: 999px;
  font-size: .66rem; font-weight: 600;
}
.thread-status--ai { color: #a5b4fc; background: rgba(99, 102, 241, .16); }
.thread-status--waiting { color: #fcd34d; background: rgba(251, 191, 36, .14); }
.thread-status--resolved { color: #6ee7b7; background: rgba(16, 185, 129, .14); }

/* Плейсхолдер загрузки, пока грузятся спарклайны */
.spark.is-empty { background: linear-gradient(90deg, rgb(var(--tint) / .04), rgb(var(--tint) / .07), rgb(var(--tint) / .04)); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
  .dot-online::after { display: none; }
}

/* --------------------------------------------------------- мастер шагов
   Конструктор идёт по шагам: виден только текущий вопрос, а сделанные
   сворачиваются в строки с пометкой ответа. Поля не копируются в шаги —
   перемещается сама панель, поэтому у каждого поля ровно один id и
   pricing.js работает с теми же элементами, что и до пошаговости.

   Панели скрыты атрибутом hidden, а не display:none в CSS: без JS разметка
   обязана остаться читаемой, и hidden на панелях ставит сам скрипт. */
.wiz-pane[hidden] { display: none; }
.wiz-pane { animation: wiz-in .32s var(--ease) both; }
@keyframes wiz-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.wiz-q {
  font-size: 1.3rem; font-weight: 600; letter-spacing: -.02em;
  color: var(--ink); line-height: 1.3;
}
.wiz-sub { margin-top: .35rem; font-size: .8rem; color: var(--ink-faint); }

/* Ход шага точками. Точка — не просто кружок: у пройденных шагов она
   залита и подписана, у будущих — пустая и некликабельная. */
/* Точки шагов стоят по центру блока и расходятся к краям от центра.
   Раньше ряд был «точки слева, счётчик справа» и точки прижимались к
   левому краю; счётчика «шаг N из 6» больше нет — цифры на самих точках
   говорят то же самое, и вставать вплотную к шестой точке он не может. */
.wiz-bar {
  display: flex; align-items: center; justify-content: center; gap: .4rem;
  margin-bottom: 1.35rem;
}

/* Обнуление автоматического минимума у карточек мастера. Сетка двухколоночная
   только на lg, а ниже колонка неявная, и её размер — auto, то есть он
   подстраивается под самое широкое содержимое. Карточка мастера с длинным
   списком игр растягивала колонку за пределы экрана: на 412px секция
   считалась 583px шириной при контейнере 549px, и весь документ уезжал вбок.
   Та же причина, что была с .bento, и лечится так же. */
#wizard, .summary { min-width: 0; }
/* Шесть шагов с названиями в строку на телефоне не помещаются: последний,
   «Итог», уезжал за правый край и обрезался наполовину. Переносить их нельзя
   — между шагами тянутся соединительные чёрточки, и в два ряда они ломаются.

   Поэтому на узком экране полоса шагов прокручивается вбок: обрезанный
   край видно, и это привычнее, чем потерянный шаг. Скроллбар спрятан —
   полосу видно и без него. */
@media (max-width: 639px) {
  .wiz-bar { overflow-x: auto; scrollbar-width: none; justify-content: flex-start; }
  .wiz-bar::-webkit-scrollbar { display: none; }
}
.wiz-dots { display: flex; align-items: center; gap: .4rem; margin: 0; padding: 0; list-style: none; }
.wiz-dots li {
  display: flex; align-items: center; gap: .4rem;
  font-size: .72rem; color: var(--ink-faint);
}
.wiz-dots li + li::before {
  content: ""; width: 14px; height: 1px; background: var(--line); margin-right: .1rem;
}
.wiz-dot {
  display: grid; place-items: center;
  width: 1.35rem; height: 1.35rem; border-radius: 999px;
  font-size: .68rem; font-weight: 600;
  border: 1px solid var(--line); color: var(--ink-faint);
  transition: color .2s var(--ease), background .2s var(--ease),
              border-color .2s var(--ease);
}
/* Пройденный шаг и текущий раньше делили одно правило — кружки выглядели
   одинаково, и на шкале нельзя было понять, где ты сейчас. Разведены по
   плотности заливки: будущее — контур, пройденное — мягкая заливка с
   галочкой фиолетовым, текущее — сплошная заливка с белой цифрой и
   свечением. Получается три различимых состояния вместо двух похожих. */
.wiz-dots li.is-done .wiz-dot {
  background: rgb(var(--brand) / .16);
  border-color: rgb(var(--brand) / .5);
  color: rgb(var(--brand));
}
.wiz-dots li.is-current .wiz-dot {
  background: rgb(var(--brand));
  border-color: rgb(var(--brand));
  color: #fff;
  box-shadow: 0 0 0 3px rgb(var(--brand) / .18);
}
.wiz-dots li.is-current { color: var(--ink); font-weight: 600; }

/* Пройденный шаг кликабелен: возвращает на себя. Без курсора и реакции
   на наведение клик по нарисованному кружку — лотерея: человек не знает,
   что точка живая, и пробует её один раз, не дождавшись ответа.

   Кнопкой сделана вся строка шага, а не только кружок (1.35rem): в такой
   круг на узком экране легко промахнуться и решить, что шкала не жмётся. */
.wiz-dots li.is-done .wiz-dot-go {
  display: flex; align-items: center; gap: .4rem;
  margin: 0; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: left;
  cursor: pointer; -webkit-appearance: none; appearance: none;
  transition: color .2s var(--ease);
}
.wiz-dots li.is-done:hover .wiz-dot-go { color: var(--ink); }
.wiz-dots li.is-done:hover .wiz-dot {
  background: rgb(var(--brand) / .32);
  border-color: rgb(var(--brand) / .8);
  transform: scale(1.08);
}
.wiz-dot-go:focus-visible {
  outline: 2px solid rgb(var(--brand) / .75); outline-offset: 3px;
  border-radius: .4rem;
}
/* Точка остаётся кругом при наведении, а растягивается круглым же
   прямоугольником при фокусе с клавиатуры: так рамка показывает весь
   нажатый элемент, а не только кружок. */
/* ---- промокод на последнем шаге ------------------------------------
   Плашка с кодом скидки. Галочки перед заголовком нет: круг с галочкой
   рядом с кружками стрелок читался как ещё одна кнопка, а он ничего
   не делал. Заголовок шага и так показывает, что всё собрано. */
.wiz-coupon {
  margin-top: 1.5rem; padding: 1rem 1.1rem;
  border-radius: .85rem;
  border: 1px dashed rgb(16 185 129 / .45);
  background: rgb(16 185 129 / .07);
  animation: wiz-in .34s var(--ease) both;
}
.wiz-coupon[hidden] { display: none; }
.wiz-coupon__head {
  display: flex; align-items: baseline; justify-content: space-between; gap: .75rem;
}
.wiz-coupon__label { font-size: .78rem; font-weight: 600; color: rgb(52 211 153); }
.wiz-coupon__percent {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 1.25rem; font-weight: 700; color: rgb(52 211 153);
}
.wiz-coupon__text { margin-top: .35rem; font-size: .76rem; color: var(--ink-faint); }

/* Праздничный акцент на купоне.
   Скидку человек обычно не замечает: блок стоит в колонке вопросов, текст
   в нём служебный, а рядом уже есть главное — итоговая сумма. Между тем
   это единственное место, где за аренду предлагают заплатить меньше, и
   оно должно выглядеть как подарок, а не как сноска с мелким шрифтом.

   Первый вариант эффекта был диагональной полосой света поперёк плашки.
   От него отказались: полоса шла по тексту и читалась как блик, а не как
   праздник — особенно на длинном пояснении под процентом. Сейчас здесь
   три вещи, и ни одна не проходит по содержимому:

     1. Мягкое радиальное свечение из угла — блок «дышит», как подсвеченная
        витрина. Оно под текстом, а не поверх него, поэтому читаемость
        не страдает.
     2. Тонкое кольцо по контуру, в котором кругом ходит один-единственный
        блик. Кольцо — не сплошная радуга, иначе плашка пестрее фона и
        перетягивает на себя всё внимание.
     3. Тёплое свечение самого процента: «−16%» и есть подарок, ему и
        положено светиться ярче подписи и пояснения.

   Всё это на псевдоэлементах: сама плашка уже появляется анимацией
   wiz-in, и вторая анимация на том же элементе её бы перекрыла. */
.wiz-coupon {
  position: relative; overflow: hidden; isolation: isolate;
  /* Цвет акцента: обычный купон зелёный, пробный — фиолетовый, как шаг,
     на котором человек жмёт «Запустить бесплатно». */
  --cta: 52 211 153;
}
.wiz-coupon--trial { --cta: var(--brand); }

.wiz-coupon::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  inset: -40% -10% -60% -10%;
  background: radial-gradient(46% 58% at 22% 12%,
    rgb(var(--cta) / .30), rgb(var(--cta) / .10) 42%, transparent 68%);
  animation: coupon-breathe 5.4s ease-in-out infinite;
}
@keyframes coupon-breathe {
  0%, 100% { opacity: .55; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.1); }
}

/* Пульс самой плашки и её мягкое свечение.

   Здесь важна одна деталь: список анимаций у .wiz-coupon объявлен ЗДЕСЬ и
   больше нигде. Пока он был продублирован ниже по файлу, правило с
   coupon-pop перекрывало его целиком — и пульсация просто не запускалась,
   хотя в коде была. Отдельные правила на каждый эффект тут и не нужны:
   достаточно одного объявления со списком.

   Пульс сделан через scale, а не через transform: scale не задевает ни
   сдвиг, ни позиционирование, поэтому кнопка «Копировать» внутри блока
   не смещается и остаётся ровно под своим кодом. */
.wiz-coupon {
  border-color: rgb(var(--cta) / .5);
  animation: wiz-in .34s var(--ease) both,
             coupon-pop .5s var(--ease) both,
             coupon-pulse 3.2s ease-in-out infinite;
}
@keyframes coupon-pulse {
  0%, 100% {
    scale: 1;
    border-color: rgb(var(--cta) / .38);
    box-shadow: 0 0 .9rem -0.4rem rgb(var(--cta) / .25);
  }
  50% {
    scale: 1.012;
    border-color: rgb(var(--cta) / .85);
    box-shadow: 0 0 1.8rem -0.35rem rgb(var(--cta) / .55);
  }
}

.wiz-coupon__percent {
  animation: coupon-glow 3.4s ease-in-out infinite;
}
@keyframes coupon-glow {
  0%, 100% {
    text-shadow: 0 0 0 rgb(var(--cta) / 0);
    transform: scale(1);
  }
  50% {
    text-shadow: 0 0 1.1rem rgb(var(--cta) / .75);
    transform: scale(1.08);
  }
}
/* Появление с лёгким увеличением: блок словно расправляется. Отдельное
   правило, а не вторая строка в объявлении выше, — иначе оно перекрыло бы
   список анимаций целиком вместе с пульсом. */
@keyframes coupon-pop {
  0% { opacity: 0; scale: .94; }
  60% { scale: 1.02; }
  100% { opacity: 1; scale: 1; }
}
/* Уважение к настройке движения: мягкое свечение остаётся — оно и есть
   акцент, — а движение уходит. */
@media (prefers-reduced-motion: reduce) {
  .wiz-coupon::before { animation: none; opacity: .8; }
  .wiz-coupon__percent { animation: none; text-shadow: 0 0 .8rem rgb(var(--cta) / .5); }
  .wiz-coupon {
    animation: wiz-in .34s var(--ease) both;
    border-color: rgb(var(--cta) / .6);
    box-shadow: 0 0 1.6rem -0.4rem rgb(var(--cta) / .4);
  }
}

/* Пробный купон — про скидку, которой ещё нет. Зелёный у него означал бы
   «уже учтено в сумме», а сумма сейчас 0 ₽ и останется ей до продления.
   Фиолетовый здесь совпадает с кнопкой оплаты и с шагом, на котором
   человек нажимает «Запустить бесплатно», — блок читается как часть
   этого шага, а не как уведомление об экономии. */
.wiz-coupon--trial {
  border-style: solid;
  border-color: rgb(var(--brand) / .38);
  background: rgb(var(--brand) / .08);
}
.wiz-coupon--trial .wiz-coupon__label,
.wiz-coupon--trial .wiz-coupon__percent { color: rgb(var(--brand) / .95); }
.wiz-coupon__row { display: flex; align-items: center; gap: .6rem; margin-top: .8rem; }
.wiz-coupon__code {
  flex: 1; min-width: 0; padding: .5rem .7rem; border-radius: .55rem;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .95rem; letter-spacing: .06em; font-weight: 600;
  color: var(--ink); background: rgb(var(--tint) / .06);
  border: 1px solid var(--line);
  text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wiz-coupon__copy {
  display: inline-flex; align-items: center; gap: .35rem; flex: none;
  padding: .5rem .7rem; border-radius: .55rem; cursor: pointer;
  font-size: .72rem; color: var(--ink);
  background: rgb(var(--brand) / .12); border: 1px solid rgb(var(--brand) / .3);
  transition: background .18s var(--ease), border-color .18s var(--ease);
}
.wiz-coupon__copy:hover { background: rgb(var(--brand) / .24); border-color: rgb(var(--brand) / .55); }
.wiz-coupon__copy:focus-visible { outline: 2px solid rgb(var(--brand) / .75); outline-offset: 2px; }
.wiz-coupon__note { margin-top: .6rem; font-size: .68rem; color: var(--ink-faint); }

/* Ответы сделанных шагов. Без них через четыре шага человек не помнит,
   что выбрал, и ему приходилось бы возвращаться перещёлкивать. Список
   один на весь мастер, включая последний шаг: там он показывает все
   ответы с «изменить» и заменяет собой отдельный перечень на шаге
   оформления — тот был вторым списком тех же строк и читался как два
   разных набора параметров. */
.wiz-done { margin-bottom: 1.5rem; border-bottom: 1px solid var(--line); padding-bottom: 1rem; }
.wiz-done-row {
  display: flex; align-items: baseline; gap: .5rem; width: 100%;
  padding: .3rem .4rem; border-radius: .45rem; text-align: left;
  font-size: .8rem; color: var(--ink);
  transition: background .18s var(--ease);
}
.wiz-done-row:hover { background: rgb(var(--brand) / .08); }
.wiz-done-row:disabled { cursor: default; }
.wiz-done-row:disabled:hover { background: none; }
.wiz-done-key { color: var(--ink-faint); flex: none; }
.wiz-done-val { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wiz-done-edit {
  margin-left: auto; flex: none;
  font-size: .68rem; color: rgb(var(--brand) / .9);
  opacity: 0; transition: opacity .18s var(--ease);
}
.wiz-done-row:hover .wiz-done-edit { opacity: 1; }

.wiz-note {
  display: flex; align-items: center; gap: .5rem;
  font-size: .78rem; color: var(--ink-faint);
}

/* ---- стрелки шага --------------------------------------------------------
   Кистевой шеврон, а не иконка из набора: контур замкнут и намеренно
   неровен, концы разной толщины и сходятся к острому кончику. Ровная
   обводка со скруглёнными концами выглядит как элемент из икон-сета,
   а такое читается как мазок кистью.

   Заливка одна (currentColor), без рамки и подложки: кружок вокруг стрелки
   читался как ещё одна кнопка рядом с кнопкой оплаты, а он ничего не
   делал. Размером берётся крупный — мазок должен читаться в блоке, где
   остальные знаки мелкие.

   Правую стрелку зеркалит scaleX(-1): второй путь в разметке был бы его
   копией и разъехался бы с левым при правке формы. */
.wiz-arrow {
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem; padding: 0;
  cursor: pointer; color: rgb(var(--brand) / .85);
  background: none; border: 0;
  transition: color .22s var(--ease), transform .22s var(--ease);
}
.wiz-arrow__glyph { width: 1.75rem; height: 1.85rem; fill: currentColor; }
/* Путь в разметке смотрит влево — это «назад». «Вперёд» получается
   поворотом на 180°. Раньше поворот висел на «назад», и обе стрелки
   указывали в сторону, противоположную своему действию. */
.wiz-arrow--next .wiz-arrow__glyph { transform: rotate(180deg); }
.wiz-arrow--next:hover:not(:disabled) { transform: translateX(3px); }
.wiz-arrow--prev:hover:not(:disabled) { transform: translateX(-3px); }
.wiz-arrow:hover:not(:disabled) { color: rgb(var(--brand)); }
.wiz-arrow:focus-visible {
  outline: 2px solid rgb(var(--brand) / .75); outline-offset: 3px;
  border-radius: .5rem;
}
/* Без явного правила display:none атрибут hidden перебивается display:grid
   выше — «спрятанная» стрелка осталась бы на месте и ловила бы клики. */
.wiz-arrow[hidden] { display: none; }
/* На первом шаге стрелки «назад» нет, и в ряду остаётся одна кнопка.
   space-between прижимает её к левому краю, а ведёт она вперёд — у края
   стрелка указывала не туда, куда ведёт. Прижимаем «дальше» вправо.

   Селектор по соседству, а не :only-child: скрытая стрелка остаётся в
   DOM (её прячет атрибут hidden), поэтому «единственным ребёнком» она
   не перестаёт быть и правило не срабатывало. */
.wiz-arrow--prev[hidden] ~ .wiz-arrow--next { margin-left: auto; }
.wiz-arrow:disabled { opacity: .25; cursor: default; }

/* Кнопка оплаты заблокирована до последнего шага: без этого человек мог
   оформить сервер, не выбрав ни режима, ни карты, и получил бы значения
   по умолчанию, о которых не подозревал. */
#order-btn.is-locked,
#order-btn:disabled {
  opacity: .45; cursor: not-allowed; filter: saturate(.4);
}

/* Призыв к действию на кнопке оплаты.
   Кнопка живёт в правой колонке весь путь: заблокированная, посередине
   с тусклым текстом, разблокированная. Разблокировка — единственное
   изменение на последнем шаге, и без акцента она проходила незаметно:
   человек доходил до конца, видел привычную синюю плашку и не понимал,
   что наконец-то можно нажать. Свечение вокруг кнопки говорит «смотри
   сюда» без текста и без анимации, которая бы мешала читать цену.

   Свечение мягкое и пульсирует медленно: конструктор открывают десятки
   раз, и резкая пульсация утомляет. Кольцо не перехватывает клик — это
   box-shadow, а не отдельный элемент. */
#order-btn:not(.is-locked):not(:disabled) {
  animation: order-cta 2.4s ease-in-out infinite;
}
@keyframes order-cta {
  0%, 100% {
    box-shadow: 0 0 0 0 rgb(var(--brand) / .55),
                0 10px 30px -10px rgb(var(--brand) / .45),
                inset 0 1px 0 rgb(var(--tint) / .2);
  }
  50% {
    box-shadow: 0 0 0 .55rem rgb(var(--brand) / .13),
                0 14px 40px -8px rgb(var(--brand) / .65),
                inset 0 1px 0 rgb(var(--tint) / .2);
  }
}
/* Ссылка уводит с призывного состояния: человек уже нажал, и пульсирующее
   кольцо на кнопке под наведением только мешает читать, что происходит. */
#order-btn:not(.is-locked):not(:disabled):hover { animation-play-state: paused; }

/* Уважение к настройке движения: свечение остаётся, пульсация уходит —
   человек, отключивший анимацию, всё равно должен увидеть акцент. */
@media (prefers-reduced-motion: reduce) {
  #order-btn:not(.is-locked):not(:disabled) {
    animation: none;
    box-shadow: 0 0 0 .3rem rgb(var(--brand) / .22),
                0 10px 30px -10px rgb(var(--brand) / .55),
                inset 0 1px 0 rgb(var(--tint) / .2);
  }
}

/* Ряд навигации внизу блока. Разделительная линия сверху отделяет его от
   содержимого шага: стрелки — проход по мастеру, а не часть вопроса. */
.wiz-nav {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  margin-top: 1.75rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
}


/* ==========================================================================
   Кластер машин — страница /fleet

   Отдельный блок, а не переиспользование карточек витрины: здесь показываются
   величины, которых на витрине нет вовсе (аптайм, нагрузка, срок хватит
   денег), и они требуют другого ритма — плотной таблицы чисел, где взгляд
   идёт по строкам вниз, а не по крупным блокам.

   Полосы загрузки идут без перехода цвета: у машины нагрузка может прыгать
   на каждом обновлении (раз в 15 секунд), и анимация превращалась бы в
   мельтешение. Смена состояния машины подсвечивает карточку — это медленное и
   читаемое событие. */
.fleet-tile {
  display: flex; flex-direction: column; gap: .25rem;
  padding: .85rem .95rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
}
.fleet-tile__label {
  font-size: .7rem; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-faint);
}
/* Цифра в плитке — главный элемент, а не подпись: она должна читаться на
   первом взгляде, поэтому крупнее подписи и с табличными цифрами, иначе
   соседние числа «плывут» и глаз не может их сравнить. */
.fleet-tile__value {
  font-size: 1.45rem; font-weight: 800; line-height: 1.1;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.fleet-tile__of { font-size: .8rem; font-weight: 600; color: var(--ink-faint); }
.fleet-tile__hint { font-size: .7rem; color: var(--ink-faint); line-height: 1.35; }
/* Плитка, где цифра недостоверна (срок деньгам посчитан приблизительно),
   помечена жёлтым: не ошибка, но и не точное число. */
.fleet-tile--warn { border-color: rgba(250, 204, 21, .32); }
.fleet-tile--warn .fleet-tile__value { color: #fcd34d; }

.fleet-addform {
  margin-top: 1.25rem; padding: 1.25rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
}
.fleet-field { display: flex; flex-direction: column; gap: .35rem; }
.fleet-field > span { font-size: .78rem; color: var(--ink-faint); }
.fleet-field > input {
  background: var(--bg-card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 9px;
  padding: .55rem .7rem; font-size: .9rem;
}
.fleet-field > input:focus { outline: none; border-color: rgb(var(--brand) / .6); }

.fleet-token {
  margin-top: 1.25rem; padding: 1.25rem;
  background: var(--panel); border: 1px solid rgba(250, 204, 21, .3);
  border-radius: 14px;
}
.fleet-token__value {
  padding: .45rem .7rem; border-radius: 8px;
  background: var(--bg-card); border: 1px solid var(--line);
  /* Токен — длинная строка без пробелов. Без word-break он не влезает в
     карточку и растягивал бы её за пределы экрана на телефоне. */
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .78rem; color: var(--ink);
  word-break: break-all; overflow-wrap: anywhere;
}
.fleet-code {
  padding: .85rem 1rem; border-radius: 10px;
  background: #0b1120; border: 1px solid var(--line);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: .76rem; line-height: 1.7; color: #cbd5e1;
  overflow-x: auto; white-space: pre;
}
html[data-theme="light"] .fleet-code { background: #0f172a; }

.fleet-card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  overflow: hidden;
}
/* Полоса слева — состояние машины. Читается раньше бейджа: цвет по краю виден
   при беглом взгляде на всю страницу, а бейдж приходится читать. */
.fleet-card { border-left: 3px solid var(--line); }
.fleet-card--online  { border-left-color: #34d399; }
.fleet-card--stale   { border-left-color: #facc15; }
.fleet-card--offline { border-left-color: #f87171; }
.fleet-card--new     { border-left-color: var(--line-firm); }

.fleet-card__head {
  display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between;
  gap: 1rem; padding: 1.1rem 1.25rem .9rem;
  border-bottom: 1px solid var(--line);
}
.fleet-card__body { padding: 1.1rem 1.25rem; }

.fleet-meters {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: .75rem 1.25rem;
}
/* Подпись и значение — в одной строке, полоса под ними.
 *
 * Строки заданы явно, а не «как встанет»: полоса объявлена grid-column: 1/-1
 * и при авторазмещении вытесняет значение на третью строку — ровно то, что
 * и получилось: подпись, полоса, число в три ряда. Явные grid-row делают
 * раскладку независимой от порядка элементов в разметке. */
.fleet-meter { display: grid; grid-template-columns: 1fr auto; gap: .3rem .6rem; align-items: center; }
.fleet-meter__label { grid-column: 1; grid-row: 1; font-size: .74rem; color: var(--ink-faint); }
.fleet-meter__value { grid-column: 2; grid-row: 1; font-size: .78rem; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.fleet-meter__bar {
  grid-column: 1 / -1; grid-row: 2; height: .45rem; border-radius: 99px;
  background: var(--bg-card); overflow: hidden;
}
.fleet-meter__bar > span {
  display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, rgb(var(--brand) / .85), rgb(var(--brand) / .5));
}
/* Порог близости к потолку задаётся классом в разметке, а не селектором по
   содержимому style-атрибута: «width: 8…» ловил бы и 83 %, и 8 %, и любое
   другое число, начинающееся с восьмёрки. */
.fleet-meter--warn .fleet-meter__bar > span {
  background: linear-gradient(90deg, #fbbf24, #fb923c);
}
.fleet-meter--crit .fleet-meter__bar > span {
  background: linear-gradient(90deg, #f87171, #fb923c);
}
/* Плашка load average: подпись и число, без полосы. Отдельное правило, потому
   что у неё только две ячейки из трёх — общее правило оставило бы третью
   строку пустой. */
.fleet-meter--plain { grid-template-columns: 1fr auto; }
.fleet-meter--plain .fleet-meter__label { grid-column: 1; grid-row: 1; }
.fleet-meter--plain .fleet-meter__value { grid-column: 2; grid-row: 1; }

.fleet-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: .7rem 1.25rem; margin: 1.1rem 0 0;
}
.fleet-facts > div { min-width: 0; }
.fleet-facts dt { font-size: .68rem; letter-spacing: .03em; text-transform: uppercase; color: var(--ink-faint); }
.fleet-facts dd {
  margin: .1rem 0 0; font-size: .85rem; color: var(--ink);
  /* Адреса и версии длинные и неразрывные: без обрезки они растягивали бы
     сетку за пределы карточки. */
  overflow-wrap: anywhere;
}

.fleet-money {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: .7rem 1.25rem; margin-top: 1.1rem; padding-top: 1rem;
  border-top: 1px dashed var(--line);
}
.fleet-money > div { display: flex; flex-direction: column; }
.fleet-money__label { font-size: .68rem; letter-spacing: .03em; text-transform: uppercase; color: var(--ink-faint); }
.fleet-money__value { font-size: 1rem; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }

.fleet-warn {
  display: flex; align-items: flex-start; gap: .5rem;
  margin-top: .9rem; padding: .6rem .75rem; border-radius: 9px;
  background: rgba(250, 204, 21, .09); border: 1px solid rgba(250, 204, 21, .22);
  font-size: .78rem; line-height: 1.45; color: #fde68a;
}
html[data-theme="light"] .fleet-warn { color: #92400e; background: rgba(250, 204, 21, .14); }

.fleet-servers { padding: 0 1.25rem 1.1rem; }
.fleet-servers__title { font-size: .8rem; color: var(--ink-faint); margin-bottom: .6rem; }
.fleet-servers__empty { font-size: .8rem; color: var(--ink-faint); }
.fleet-servers__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(0, 240px)); gap: .5rem; }
.fleet-server {
  display: grid; gap: .15rem; padding: .55rem .7rem; border-radius: 9px;
  background: var(--bg-card); border: 1px solid var(--line);
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.fleet-server:hover { border-color: rgb(var(--brand) / .5); background: var(--bg-raised); }
.fleet-server__name { font-size: .85rem; font-weight: 600; color: var(--ink); }
.fleet-server__meta { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; font-size: .72rem; color: var(--ink-faint); }
.fleet-server__price { font-size: .72rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

.fleet-empty {
  display: flex; flex-direction: column; align-items: center;
  padding: 3rem 1.5rem; text-align: center;
  background: var(--panel); border: 1px dashed var(--line-firm); border-radius: 14px;
  color: var(--ink-faint);
}

/* --- Провайдер и деньги машины -----------------------------------------
   Отдельные правила, а не переиспользование .fleet-money: там раскладка
   четыре в ряд, а здесь пять ячеек с разными смысловыми классами
   (плюс/минус/неизвестно), и подсветка знака должна быть видна сразу. */

.fleet-prov {
  margin-top: 1.1rem; padding: .7rem .85rem;
  background: var(--bg-card); border: 1px solid var(--line); border-radius: 10px;
}
.fleet-prov__head {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
}
.fleet-prov__title {
  display: flex; align-items: center; gap: .45rem;
  font-size: .85rem; font-weight: 700; color: var(--ink);
}
.fleet-prov__ids {
  display: flex; flex-wrap: wrap; gap: .75rem;
  margin-top: .4rem; color: var(--ink-faint);
}

.fleet-money--prov { margin-top: .8rem; padding-top: .8rem; border-top-style: solid; }
/* Знак денежной величины, который что-то сообщает, а не украшает: плюс —
   прибыль, минус — убыток. Насыщенный цвет только здесь. */
.fleet-money__value--plus  { color: #6ee7b7; }
.fleet-money__value--minus { color: #fca5a5; }
.fleet-money__value--none  { color: var(--ink-faint); }
.fleet-money__pct { font-size: .72rem; font-weight: 600; opacity: .7; margin-left: .15rem; }
/* Метка «от какой даты» под остатком: чем она мельче и тусклее, тем яснее,
   что это не часть суммы, а её возраст. */
.fleet-money__stale {
  display: block; font-size: .68rem; font-weight: 500;
  color: var(--ink-faint); margin-top: .1rem;
}

/* Свод «сходится ли кластер». Полоса из трёх–пяти ячеек над списком машин:
   это единственное место на странице, где ответ должен читаться, не
   листая карточки.
 *
 * Минимум колонки — 150px, а НЕ minmax(0, 1fr), как на остальных сетках
 * страницы. При нулевом минимуме auto-fit на 395px набил пять колонок по
 * 79px, а подписи «ДОХОД С АРЕНДАТОРОВ В СУТКИ» в 79 не влезают: сетка
 * сообщала scrollWidth 424 при clientWidth 395 и выталкивала страницу за
 * правый край на 320-экране. С настоящим минимумом там становится две
 * колонки, а на десктопе по-прежнему пять. */
.fleet-pnl {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: .75rem; margin-top: 1rem;
}
.fleet-pnl__cell {
  display: flex; flex-direction: column; gap: .2rem;
  padding: .75rem .9rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
}
.fleet-pnl__label {
  font-size: .68rem; letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink-faint);
}
.fleet-pnl__value {
  font-size: 1.1rem; font-weight: 800; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.fleet-pnl__cell--minus { border-color: rgba(248, 113, 113, .28); }
.fleet-pnl__cell--minus .fleet-pnl__value { color: #fca5a5; }
.fleet-pnl__cell--plus { border-color: rgba(52, 211, 153, .28); }
.fleet-pnl__cell--plus .fleet-pnl__value { color: #6ee7b7; }

/* Предупреждение шириной со всю страницу, а не во всю ширину карточки:
   речь о кластере в целом, и полоса на всю ширину отделяет общее от частного. */
.fleet-warn--wide { margin: 1rem 0 0; }

/* Список адресов — отдельная строка, а не элемент сетки фактов.
 *
 * Именно отдельная: см. комментарий в fleet.html. Элемент на все колонки
 * внутри repeat(auto-fit, minmax(0, 1fr)) заставляет браузер добавлять
 * дорожки, пока вклад этого элемента в них не поместится, — сетка
 * раздувается до десятков колонок по паре пикселей. Проверено: 42 колонки
 * по 1,7 px в сетке шириной 783 px. */
.fleet-ips {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem .75rem;
  margin-top: .8rem;
}
.fleet-ips__label {
  font-size: .68rem; letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink-faint);
}
.fleet-ips__value {
  font-size: .8rem; color: var(--ink);
  /* Адрес неразрывный, а на IPv6 разделителей-запятых не бывает: перенос
     нужен, иначе длинная строка растянет карточку за пределы экрана. */
  overflow-wrap: anywhere;
}
.fleet-ips__note { font-size: .7rem; color: var(--ink-faint); }

/* --- Живое обновление ---------------------------------------------------
   Индикатор канала. Точка — единственный элемент, который мигает: она и
   должна привлекать взгляд, когда канал молчит, и затихать, когда всё
   в порядке. */
.fleet-live { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.fleet-live__btn {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .45rem .7rem; border-radius: 99px;
  background: var(--bg-card); border: 1px solid var(--line);
  color: var(--ink-dim); font-size: .75rem; cursor: pointer;
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.fleet-live__btn:hover { border-color: rgb(var(--brand) / .5); color: var(--ink); }
.fleet-live__dot {
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--ink-faint); flex: none;
}
.fleet-live__stamp {
  font-size: .68rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* Состояния канала. Пульсация только у «подключения» и «опроса»:
   там положение неопределённое, и движение говорит «живо» без слов. */
.fleet-live[data-state="live"] .fleet-live__dot {
  background: #34d399; box-shadow: 0 0 0 0 rgba(52, 211, 153, .5);
  animation: fleet-pulse 2.4s var(--ease) infinite;
}
.fleet-live[data-state="connecting"] .fleet-live__dot,
.fleet-live[data-state="polling"] .fleet-live__dot {
  background: #facc15;
  animation: fleet-blink 1.2s steps(2, start) infinite;
}
.fleet-live[data-state="denied"] .fleet-live__dot { background: #f87171; }
.fleet-live[data-state="off"] .fleet-live__dot { background: var(--ink-faint); }

@keyframes fleet-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(52, 211, 153, .45); }
  70%  { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); }
  100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
}
@keyframes fleet-blink { 50% { opacity: .25; } }

/* Полоски метрик. Переход по ширине появился вместе с живым обновлением:
   раньше полоса перерисовывалась целиком каждые 15 секунд, и переход давал
   бы мельтешение — картинка и так менялась. Теперь значение подставляется в
   существующий узел раз в 30 секунд, и плавный ход читается как движение
   нагрузки, а не как дёрганье.
   Цвет порога НЕ переходит: он меняется редко, и переход на нём затянул бы
   жёлтый на 300 мс каждый раз, когда нагрузка чуть перевалила за порог. */
.fleet-meter__bar > span { transition: width .45s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .fleet-meter__bar > span { transition: none; }
  .fleet-live__dot { animation: none !important; }
}

/* Выключенное живое обновление: метрики остаются, но полосы не едут —
   иначе страница выглядит «живой», обманывая. */
.fleet-live[data-state="off"] ~ * .fleet-meter__bar > span { transition: none; }

/* --- Роль машины и готовность кластера ------------------------------------ */

/* Роль показана не подписью, а бейджем с значком: «только панель» меняет
   смысл всей карточки, и по мелкому слову в скобках это не читается. */
.fleet-role { display: inline-flex; align-items: center; gap: .25rem; }
.fleet-role--panel {
  background: rgba(56, 189, 248, .12); color: #7dd3fc;
  border: 1px solid rgba(56, 189, 248, .3);
}
.fleet-role--games {
  background: rgb(var(--brand) / .14); color: var(--brand-400);
  border: 1px solid rgb(var(--brand) / .38);
}
html[data-theme="light"] .fleet-role--panel { color: #0369a1; }
html[data-theme="light"] .fleet-role--games { color: #4338ca; }

/* Плитка «готовы принять сервер». Ноль готовых машин — это не «пусто», а
   «заводить некуда», и выглядеть должно так же тревожно, как пустой кластер.
   Класс ставится в шаблоне по значению, а не через :has() по вложенному
   узлу: искать элемент внутри блока, чтобы покрасить его значение, — способ
   получить правило, молча переставшее работать при смене разметки. */
.fleet-tile--alarm { border-color: rgba(248, 113, 113, .32); }
.fleet-tile--alarm .fleet-tile__value { color: #fca5a5; }
.fleet-tile--ready { border-color: rgba(52, 211, 153, .28); }
.fleet-tile--ready .fleet-tile__value { color: #6ee7b7; }
html[data-theme="light"] .fleet-tile--alarm .fleet-tile__value { color: #b91c1c; }
html[data-theme="light"] .fleet-tile--ready .fleet-tile__value { color: #047857; }
