/* TISlots panel -- bio-lab terminal theme.
   Dark, high-contrast, monospace for anything that is a measurement.

   УГЛЫ (2026-08-17). Скругления в панели прежние -- прямые углы только у меню:
   у клавиш разделов (.tab) и у подписей групп (.nav-group). Всё остальное --
   окна, карточки, кнопки, поля, ярлыки-пилюли -- скруглено, как было.

   ВНИМАНИЕ, если сверяешь с чем-то старым: файл успел побывать целиком на
   прямых углах, и радиусы возвращены не побайтово. Часть значений восстановлена
   точно, часть подобрана по семейству элемента (контейнер 12px, управляющий
   элемент 9px, ярлычок 6px, полоска 3px, пилюля 999px, окно 14px). Если
   попадётся элемент с непривычным скруглением -- дело в этом, правь на месте. */

:root {
  --bg: #070b0e;
  --bg-deep: #04070a;
  --raised: #0d1419;
  --raised-2: #111a21;
  --line: #1d2b33;
  --line-hot: #2b4450;
  --text: #dfe9ee;
  --muted: #7b8f9b;
  --accent: #35e08a;
  --accent-dim: #1d8f57;
  --cyan: #46c8f5;
  --amber: #f0b429;
  --danger: #ff5b5b;
  --violet: #a78bfa;
  --brand-live: #5cc73f;
  --mono: ui-monospace, "Cascadia Mono", Consolas, monospace;
  --radius: 12px;
}

* { box-sizing: border-box; }

/* Место под полосу прокрутки держится всегда, даже когда прокручивать нечего:
   иначе короткая вкладка («Правила») отдаёт полосе ширину обратно, а длинная
   («Мутации») забирает, и всё содержимое дёргается вбок при переключении. */
html { scrollbar-gutter: stable; }

/* Тёмная тема и для того, что рисует сам браузер: полей ввода, выпадающих
   списков и полос прокрутки там, где до них не дотянулась наша разметка. Сами
   полосы описаны ниже, в своём разделе. */
:root { color-scheme: dark; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
  background: var(--bg-deep);
}

/* The artwork at full brightness -- no filter, no scrim over it. Readability is
   bought on the UI side instead: panels are opaque and loose text is shadowed. */
body::before {
  content: "";
  position: fixed; inset: 0;
  z-index: 0; pointer-events: none;
  background: url("/img/bg.jpg") center top / cover no-repeat;
}

/* Faint horizontal sweep so the flat black reads as a screen, not a void. */
.scanlines {
  position: fixed; inset: 0; pointer-events: none; z-index: 1;
  background: repeating-linear-gradient(180deg, #ffffff05 0 1px, transparent 1px 3px);
  mix-blend-mode: overlay;
}

.hidden { display: none !important; }
.muted { color: var(--muted); font-size: 12px; }
.mono { font-family: var(--mono); }
.centered { display: flex; align-items: center; justify-content: center; min-height: 72vh; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* -------------------------------------------------------------- top bar -- */

/* Шапка -- одна строка: слева бренд с бургером, справа всё, что про игрока.
   Разделы сайта из неё уехали в боковое меню, поэтому делить её на колонки под
   центрированную полосу вкладок больше не нужно. */
.topbar {
  position: sticky; top: 0; z-index: 30;
  /* sticky здесь заодно и точка отсчёта для .dev-note: она стоит absolute по
     центру шапки, а любое position, кроме static, годится ей в опору. */
  display: flex; align-items: center; gap: 16px;
  padding: 12px 22px;
  /* Opaque enough to stand apart from an undimmed photo behind it. */
  background: #060a0df2;
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex; align-items: center; gap: 12px;
  text-decoration: none; color: inherit;
  justify-self: start; min-width: 0;
}

/* «Сайт в стадии разработки» -- ПО ЦЕНТРУ ШАПКИ. Центр честный, от края окна, а
   не «между брендом и пилюлями»: absolute внутри topbar с translateX(-50%), иначе
   надпись съезжала бы вслед за шириной соседей (у админа их больше, чем у игрока,
   и пометка стояла бы у разных людей в разных местах).

   pointer-events: none -- она декоративная и не должна перехватывать клики по
   тому, что окажется под ней на узком экране. Янтарным, как все предупреждения
   панели, но без заливки во всю ширину: это постоянная пометка, а не тревога. */
.dev-note {
  position: absolute; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 7px;
  padding: 5px 12px; border-radius: 999px;
  font-size: 12px; font-weight: 550; letter-spacing: .01em;
  color: var(--amber); background: #ffb02412; border: 1px solid #ffb02440;
  white-space: nowrap; cursor: default; pointer-events: none;
}
.brand-logo {
  width: 42px; height: 42px; flex: none;
  filter: drop-shadow(0 2px 8px #000a);
  transition: transform .2s ease;
}
.brand:hover .brand-logo { transform: scale(1.06); }
.brand-name {
  font-weight: 800; letter-spacing: .1em; font-size: 15px;
  text-shadow: 0 1px 4px #000;
}
/* "LIVE" picks up the green from the logo artwork. */
.brand-name span { color: var(--brand-live); }
.brand-sub {
  font-size: 10px; color: var(--muted);
  letter-spacing: .18em; text-transform: uppercase;
}

/* Правая половина шапки: сервер, кошелёк, игрок, выход. Ни рамки, ни заливки --
   всё стоит прямо на шапке, а делят это вертикальные волосяные линии. Каждая
   вещь была пилюлей в своей оправе (а потом одной общей капсулой), и оправа
   в обоих случаях весила больше того, что в ней лежало: числу «6483» рамка не
   нужна, ему нужно, чтобы его не путали с соседним числом.

   Высота отсеков одна на всех (--head-seg): по ней меряются линии, и без неё
   они вышли бы разной длины -- по высоте содержимого каждого куска.

   Порядок отсеков прежний: сервер -> деньги -> я. Слева направо это путь от
   общего к личному, и переставлять его нельзя -- к правому краю привыкли как к
   месту, где живёт аватар. */
:root { --head-seg: 30px; }
.head-right {
  display: flex; align-items: center;
  margin-left: auto; min-width: 0;
}
/* Обёртка «сервер + пилюли» не рисует ничего сама: её дети должны стоять в
   общем ряду, иначе линии пришлось бы городить в двух уровнях. */
.head-status { display: contents; }
.status-pills { display: flex; align-items: center; height: var(--head-seg); }

/* Линии. Ставит их каждый отсек, кроме первого; спрятанный сосед свою линию
   забирает с собой -- иначе первым в ряду стояла бы черта, отделяющая отсек от
   воздуха. Последняя линия -- перед выходом: он тоже отдельная вещь. */
.head-right > * + *,
.status-pills > * + *,
.user .profile + .icon-btn { border-left: 1px solid var(--line-hot); }
.server-status.hidden + .status-pills { border-left: none; }
/* Кошелька может не быть вовсе (у игрока выключена экономика) -- пустой отсек
   оставил бы в ряду черту, отделяющую пустоту от пустоты. */
.status-pills:empty { display: none; }

/* Кошелёк. Ни рамки, ни заливки -- две валюты просто стоят рядом, разделённые
   такой же линией, как и всё остальное. Числа обе белые: валюту называет монета
   рядом (зелёная и красная -- разные картинки), а красное число в шапке
   читалось как тревога, хотя это просто вторая валюта. */
.wallet { display: flex; align-items: center; height: 100%; }
.wallet-coin {
  display: flex; align-items: center; gap: 6px;
  padding: 0 12px; font-family: var(--mono); font-size: 12px; white-space: nowrap;
}
.wallet-coin + .wallet-coin { border-left: 1px solid var(--line); }
.wallet-coin b { color: var(--text); font-weight: 600; }

/* Статус сервера = точка и имя. Если серверов больше одного, поверх отсека
   лежит прозрачный <select>, так что весь отсек работает как выпадающий
   список -- об этом говорят курсор и подсветка под ним. */
.server-status {
  position: relative;
  display: flex; align-items: center; gap: 7px;
  height: var(--head-seg); padding: 0 14px 0 0;
  font-size: 11px; font-family: var(--mono); color: var(--text);
  text-transform: uppercase; letter-spacing: .06em; white-space: nowrap;
}
.server-status .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
.server-status.ok .dot { background: var(--accent); box-shadow: 0 0 9px var(--accent); }
.server-status.bad .dot { background: var(--danger); box-shadow: 0 0 9px var(--danger); }
.server-status .caret { color: var(--muted); font-size: 10px; }
.server-status:has(.server-pick:not(.hidden)) { cursor: pointer; }
.server-status:has(.server-pick:not(.hidden)):hover { background: #ffffff0a; }
.server-status .server-pick {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; border: none; padding: 0;
}
/* «Только чтение» -- такой же отсек ряда, как сервер и кошелёк: оправы нет ни у
   кого, и у сообщения о поломке её тоже нет. Заметным его делает цвет -- красная
   точка и красные буквы среди белых. */
.pill {
  display: flex; align-items: center; gap: 7px; white-space: nowrap;
  height: var(--head-seg); padding: 0 13px;
  font-size: 11px; font-family: var(--mono); color: var(--muted);
}
.pill b { color: var(--text); font-weight: 600; }
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
.pill.ok .dot { background: var(--accent); box-shadow: 0 0 9px var(--accent); }
.pill.bad, .pill.bad b { color: var(--danger); }
.pill.bad .dot { background: var(--danger); box-shadow: 0 0 9px var(--danger); }

/* Отсек игрока: аватар с ником (ведёт в профиль) и дверь наружу. Зазора между
   ними нет -- их разделяет линия, как и всё остальное в этом ряду. */
.user { display: flex; align-items: center; height: var(--head-seg); }
.user .uname { font-size: 13px; font-weight: 550; white-space: nowrap; }
.profile {
  display: flex; align-items: center; gap: 8px;
  text-decoration: none; color: inherit;
  height: 100%; padding: 0 12px;
  background: none; border: none; border-radius: 8px;
  cursor: pointer; font: inherit;
  transition: background .15s;
}
.profile:hover { background: #ffffff0a; }
/* Рамки у отсека нет, поэтому фокус с клавиатуры показывается контуром внутрь:
   иначе идущий по Tab не видит, где он. */
.profile:focus-visible {
  outline: 1px solid var(--accent-dim); outline-offset: -1px; background: #ffffff0a;
}
/* Аватар из Steam отсюда убран (2026-09-21), а СВОЮ картинку игрок ставит сам
   -- и она тут показывается; не поставил -- нарисованный знак. Правила общие:
   круг одного размера, что с картинкой, что без. */
.profile img,
.profile-mark {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  border: 1px solid var(--line); background: var(--raised-2);
  display: grid; place-items: center; color: var(--muted);
  transition: border-color .15s, color .15s;
}
.profile img { object-fit: cover; }
.profile-mark svg { width: 15px; height: 15px; }
.profile:hover img,
.profile:hover .profile-mark { border-color: var(--accent); color: var(--accent); }
/* Дверь наружу прижата к правому краю капсулы, поэтому справа скругление
   полное: квадратный угол кнопки резал бы кольцо изнутри. В шапке значок ярче,
   чем в строке каталога: там его подсвечивает наведение на строку, а тут
   наводить не на что -- кнопка стоит сама по себе. */
.user .icon-btn { color: var(--muted); }

.badge {
  display: inline-flex; align-items: center; height: 22px;
  font-size: 9px; text-transform: uppercase; letter-spacing: .12em; font-weight: 700;
  background: #46c8f51f; color: var(--cyan); border: 1px solid #46c8f544;
  padding: 0 9px; border-radius: 999px;
}

/* --------------------------------------------------------------- layout -- */

/* Страница = боковое меню слева + содержимое справа, и всё это между липкой
   шапкой и прибитым подвалом. Высоты обоих меряет скрипт (--topbar-h/--foot-h):
   они зависят от шрифта и от того, влезли ли пилюли баланса в строку. */
/* Столбец меню чуть шире прежнего: подписи разделов и названия групп в нём
   набраны крупнее, и «Боевой пропуск» на 208px уже переносился. */
:root { --topbar-h: 66px; --foot-h: 42px; --nav-w: 228px; }

.shell { display: flex; align-items: flex-start; position: relative; z-index: 2; }

/* Меню липнет к шапке и живёт своей прокруткой: разделов тринадцать, и на
   ноутбучном экране они в столбец помещаются впритык. */
.sidenav {
  position: sticky; top: var(--topbar-h); z-index: 25;
  flex: 0 0 var(--nav-w); width: var(--nav-w);
  height: calc(100vh - var(--topbar-h) - var(--foot-h));
  overflow-y: auto; scrollbar-width: none;
  padding: 14px 10px;
  background: #060a0de8; border-right: 1px solid var(--line);
  backdrop-filter: blur(8px);
}
.sidenav::-webkit-scrollbar { display: none; }

main {
  flex: 1 1 auto; min-width: 0;
  max-width: 1280px; margin: 0 auto; padding: 22px;
  position: relative;
}

.tabs { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* Свёрнутая группа: строк не видно, подпись остаётся. Через display, а не через
   высоту с переходом: раскрытие требовало бы ещё одной обёртки под overflow, а
   меню и так открывается быстрее, чем глаз доходит до нужной строки. */
.nav-sect:not(.open) .nav-items { display: none; }

/* МЕНЮ ДЕРЖИТСЯ СТРОЕМ, А НЕ ЦВЕТОМ (2026-08-17, второй заход). Первый вариант
   заливал подписи групп тем же зелёным градиентом, что и выбранный раздел: с
   четырьмя открытыми группами на экране горело пять зелёных плашек, и та
   единственная, которая говорит «вы здесь», терялась среди них.
   Теперь яркая заливка одна на всё меню -- у выбранной строки. Группы отмечены
   не цветом, а строем: полоска слева у подписи, отступ строк от этой полоски и
   тонкая черта, ведущая от подписи вниз вдоль её строк. Зелёный у подписи
   остаётся, но словом и полоской, а не плашкой. */
.nav-group {
  display: flex; align-items: center; gap: 9px;
  width: 100%; height: 30px; padding: 0 10px 0 11px;
  background: none; border: none;
  /* Полоска слева -- начало группы; строки под ней стоят с тем же отступом, и
     вместе это читается как один столбец, а не как рамка вокруг чужих строк. */
  border-left: 2px solid var(--line-hot);
  font: inherit; text-align: left; cursor: pointer;
  /* Подпись группы была 9px в разрядку -- на фоновой картинке она читалась
     как орнамент, а не как слово. */
  font-size: 11px; font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
  white-space: nowrap; color: var(--muted);
  transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.nav-group:hover { color: var(--text); border-left-color: var(--accent-dim); background: #ffffff08; }
/* Открытая группа: зелёные слово и полоска плюс еле заметный отсвет вправо. На
   плашку это не тянет -- рядом с залитой строкой раздела подпись обязана быть
   тише её, иначе выбранное перестаёт быть заметным. */
.nav-sect.open .nav-group {
  color: var(--accent); border-left-color: var(--accent);
  background: linear-gradient(90deg, #35e08a1c, transparent 72%);
}
/* Уголок -- у правого края, нарисован рамками, а не значком: у стрелки-символа
   свой запас по бокам, и на глаз она стояла не там, где кончается подпись.
   Цвет берётся у текста -- уголок часть слова, а не отдельная метка. */
.nav-caret {
  margin-left: auto; flex: none; width: 6px; height: 6px; opacity: .85;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform .16s ease;
}
.nav-sect.open .nav-caret { transform: rotate(45deg); }
/* Строки группы -- со сдвигом от её полоски и вдоль черты, продолжающей эту
   полоску вниз: так видно, где группа кончается, без единой заливки. */
.nav-items {
  display: flex; flex-direction: column; gap: 2px;
  margin: 3px 0 0; padding-left: 9px;
  border-left: 1px solid var(--line);
}
/* Группы стоят не впритык: подпись вплотную к строкам предыдущей группы
   читалась бы как их продолжение. */
.nav-sect + .nav-sect { margin-top: 12px; }

/* Бургер. На широком экране его нет вовсе -- меню и так стоит столбцом. */
.burger {
  display: none; flex-direction: column; gap: 4px; justify-content: center;
  width: 38px; height: 34px; padding: 0 8px; cursor: pointer; flex: none;
  background: var(--raised); border: 1px solid var(--line); border-radius: 9px;
}
.burger span {
  display: block; height: 2px; background: var(--text); border-radius: 2px;
  transition: transform .18s, opacity .18s;
}
body.menu-open .burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
body.menu-open .burger span:nth-child(2) { opacity: 0; }
body.menu-open .burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

/* Строка раздела ниже подписи группы (34px): группа собирает строки под собой,
   и клавиша крупнее собственного заголовка спорила бы с ним. */
.tab {
  display: flex; align-items: center; gap: 10px;
  /* Полупрозрачная подложка: строка читается как клавиша, но сквозь неё видно
     картинку под меню -- сплошная заливка на пятнадцати строках закрыла бы её
     целиком и превратила столбец в глухую панель. */
  background: #ffffff0a; border: none; color: var(--muted);
  padding: 5px 11px; border-radius: 0; cursor: pointer;
  font: inherit; font-size: 14px; font-weight: 500; letter-spacing: .015em;
  text-align: left; width: 100%;
  transition: color .14s ease, background .14s ease;
  /* Полоска слева -- под активную вкладку: без неё выделенная строка съезжала бы
     на два пикселя, и весь столбец дёргался при переключении. */
  border-left: 2px solid transparent;
}
/* Значок в своей колонке: подписи разной длины иначе начинались бы вразнобой.
   Цветом, а не прозрачностью: приглушённый значок рядом с приглушённой подписью
   давал ровно серую строку, в которой не за что зацепиться взглядом. */
.tab span {
  color: var(--accent-dim); font-size: 13px; flex: none; width: 16px;
  text-align: center; transition: color .14s ease;
}
.tab:hover { color: var(--text); background: #ffffff16; }
.tab:hover span { color: var(--accent); }
/* Выбранная строка -- зелень навылет: заливка 24% и сходит на нет вправо, так
   что картинка под меню видна и сквозь неё. Тёмная подпись на такой заливке
   пропала бы, поэтому выбранное держится не цветом фона, а светлым словом,
   зелёным значком и сплошной полоской слева -- место под неё в строке
   зарезервировано всегда, потому столбец и не дёргается при переключении. */
.tab.active {
  color: var(--text); background: linear-gradient(90deg, #35e08a3d, #35e08a12 72%);
  font-weight: 650; border-left-color: var(--accent);
  box-shadow: none;
}
.tab.active span { color: var(--accent); }

/* Вкладка-черновик (раздел «Сервер», страница ещё не опубликована). Её видит
   только админ, и по строке должно быть ясно, что игроку её нет: приглушённая
   подпись и пунктирная полоска слева -- та же, что у выбранной вкладки, только
   не сплошная. Открытую вкладку правило не трогает (:not(.active)): выделение
   «вы здесь» важнее пометки о черновике, и спорить им незачем. */
.tab-draft { opacity: .74; border-left-style: dashed; }
.tab-draft:not(.active) { border-left-color: var(--muted); }

/* Кнопки-действия в меню («Просмотр», «Обновить»). Они не разделы: выделения
   «вы здесь» у них не бывает, поэтому и значок другого цвета -- чтобы строка не
   читалась как ещё одна вкладка, которую забыли открыть. */
.tab-act { font-weight: 500; }
.tab-act span { color: var(--cyan); }
.tab-act:hover span { color: var(--cyan); }
/* Включённый просмотр виден и по кнопке, и по подписи группы: режим меняет пол-
   панели, и забыть о нём слишком легко. */
.tab-act.on { color: var(--amber); background: #f0b42914; border-left-color: var(--amber); }
.tab-act.on span, .tab-act.on:hover span { color: var(--amber); }
body.previewing .nav-sect[data-sect="admin"] .nav-group { color: var(--amber); border-left-color: var(--amber); }
body.previewing .nav-sect[data-sect="admin"] .nav-caret { color: var(--amber); }
/* В просмотре админских разделов нет -- как и у игрока. Сама группа остаётся:
   в ней живёт кнопка выхода. */
body.previewing .nav-sect[data-sect="admin"] .tab[data-tab] { display: none; }

/* «Обновить» -- правым верхним углом под шапкой. Раньше была строкой в
   админской группе меню, но обновляются с любой вкладки и по многу раз подряд:
   ради одной кнопки каждый раз открывалось меню, а на узком экране оно ещё и
   накрывало то самое содержимое, которое хотели перечитать.

   fixed, а не в потоке: вкладок пятнадцать, у каждой своя шапка раздела, и
   место под кнопку в каждой из них пришлось бы держать отдельно. Слой ниже
   шапки (30) и ниже выпадающих списков -- кнопка не должна перекрывать то,
   что открывается поверх неё. */
.head-refresh {
  position: fixed; top: calc(var(--topbar-h) + 12px); right: 16px; z-index: 26;
  display: flex; align-items: center; gap: 7px;
  padding: 7px 13px; border-radius: 999px;
  background: #0a1116ee; border: 1px solid var(--line);
  color: var(--muted); font: inherit; font-size: 13px; cursor: pointer;
  backdrop-filter: blur(8px);
  transition: border-color .15s, background .15s, color .15s;
}
.head-refresh span { color: var(--cyan); font-size: 14px; line-height: 1; }
.head-refresh b { font-weight: 500; }
.head-refresh:hover { color: var(--text); border-color: var(--line-hot); background: #16222aee; }
/* Пока данные читаются, кнопка гаснет и крутится -- нажатие уже принято, и
   второе ничего не ускорит (обработчик его и так отбрасывает). */
.head-refresh:disabled { cursor: default; opacity: .65; }
.head-refresh:disabled span { animation: head-refresh-spin 1s linear infinite; }
/* Полоса выхода из игры прибита туда же, под шапку, и во всю ширину -- кнопка
   пропускает её вперёд. */
body.logout-open .head-refresh { top: calc(var(--topbar-h) + 64px); }
@keyframes head-refresh-spin { to { transform: rotate(360deg); } }

/* ---------------------------------------------------------------- подвал -- */

/* Прибит к низу окна: состояние сервера и ссылки нужны посреди чтения любой
   вкладки, а не в конце самой длинной из них. Место под него держит отступ
   body -- иначе подвал накрывал бы последнюю строку содержимого. */
body { padding-bottom: var(--foot-h); }
/* Высоту подвала задаёт его содержимое, а не число: раньше стояло
   `height: var(--foot-h)`, и подвал с ссылками (оферта, политика, контакты,
   дискорд, реквизиты) обрезался ровно по старой высоте -- нижняя строка уезжала
   за край экрана. Теперь наоборот: measureChrome() снимает высоту готового
   подвала и кладёт её в --foot-h, а по ней уже считаются отступ страницы и низ
   бокового меню. */
.sitefoot {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 16px;
  min-height: 34px; padding: 6px 18px;
  background: #060a0df5; border-top: 1px solid var(--line);
  backdrop-filter: blur(10px);
}
.foot-brand {
  display: flex; align-items: center; gap: 8px; flex: none;
  font-size: 11px; letter-spacing: .12em; color: var(--muted);
}
.foot-brand b { color: var(--brand-live); }
.foot-logo { width: 20px; height: 20px; }
.foot-note { margin-left: auto; white-space: nowrap; font-size: 11px; }

/* Документ, открытый без входа (/?page=<id>). Читают его чаще с телефона и
   чаще целиком, поэтому колонка узкая -- строка в 120 символов не читается. */
#publicView { max-width: 900px; margin: 0 auto; }
.public-login { display: flex; justify-content: center; margin-top: 22px; }

/* Ссылки подвала: оферта, политика, контакты, дискорд, адрес сервера. Подвал --
   единственное место, которое видно с любой страницы и до входа, поэтому здесь
   живёт то, что по закону обязано быть доступно всегда, и то, за чем игрок
   лезет чаще всего (куда подключаться, где спросить).

   Разделены точками, а не рамками: это ряд ссылок, а не панель управления. */
.foot-links {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  min-width: 0; font-size: 11px;
}
.foot-links a, .foot-links button {
  background: none; border: none; padding: 0; font: inherit; font-size: 11px;
  color: var(--muted); text-decoration: none; cursor: pointer;
  white-space: nowrap; transition: color .15s;
}
.foot-links a:hover, .foot-links button:hover { color: var(--text); }
/* Стрелка у ссылки, уводящей с сайта: дискорд открывается новой вкладкой, и об
   этом честнее предупредить значком, чем удивить. У логотипа стрелки нет --
   она перевесила бы сам значок; там роль «уводит наружу» играет логотип. */
.foot-out::after { content: "↗"; margin-left: 3px; font-size: 9px; opacity: .7; }

/* Ссылка логотипом. Логотип чуть крупнее строчной буквы -- иначе в ряду слов он
   теряется, -- и приглушён до наведения, как и остальные ссылки подвала. */
.foot-links a.foot-logo-link { display: inline-flex; align-items: center; padding: 2px 0; }
.foot-logo-mark { display: block; line-height: 0; }
.foot-logo-mark svg { width: 17px; height: 17px; display: block; }
/* Своя картинка вместо логотипа: высота та же, ширина по картинке -- бывают и
   квадратные значки, и вытянутые надписи. */
.foot-logo-img { height: 17px; width: auto; max-width: 90px; display: block; opacity: .78; }
.foot-links a:hover .foot-logo-img { opacity: 1; }
/* Реквизиты продавца и год: служебные строки, поэтому моноширинные и тише
   ссылок. На узком экране уходят -- они есть на вкладке «Контакты». */
.foot-copy, .foot-legal { color: #5f7683; font-size: 10.5px; white-space: nowrap; }
/* Оговорка -- подпись ко всему подвалу, поэтому отдельной строкой во всю ширину
   и по центру: хвостом ряда ссылок она вставала слева под ними и читалась как
   ещё одна ссылка. Пустая -- строки нет вовсе (:empty), иначе подвал стал бы
   выше на пустое место. */
.foot-disc {
  flex: 1 0 100%; text-align: center;
  color: #5f7683; font-size: 10.5px; line-height: 1.35;
}
.foot-disc:empty { display: none; }

/* Реквизиты в ряду ссылок длинные: на ноутбучном экране они переносят ряд на
   второй этаж. Прячем -- целиком они есть на вкладке «Контакты». */
@media (max-width: 1200px) { .foot-legal { display: none; } }
/* Совсем узко: от подвала остаются бренд, счётчик и обязательные ссылки --
   дискорд и адрес сервера уступают место оферте с политикой, потому что без
   первых сайт работает, а без вторых -- нет. */
@media (max-width: 760px) {
  .foot-copy, .foot-links .foot-extra { display: none; }
  .foot-links { gap: 10px; }
  /* На телефоне оговорка занимает три строки из четырёх -- отдаём место
     обязательным ссылкам, а сама она остаётся на вкладке «Контакты». */
  .foot-disc { display: none; }
}

/* ЗАГОЛОВОК РАЗДЕЛА ОДНОЙ ВЫСОТЫ НА ВСЕХ ВКЛАДКАХ (2026-08-19). Высоту шапки
   задавало то, что стоит рядом с названием: у «Сейчас на сервере» рядом нет
   ничего (20px), у «Мутаций» поле поиска (21px), у «Карты» кнопки масштаба
   (25px), у «Каталога» ряд кнопок (31px), у «Фенотип-лаборатории» выпадающий
   список (35px). Название выровнено по центру шапки -- и прыгало вверх-вниз на
   каждом переключении вкладки вместе со всем, что под ним.

   Теперь шапка не ниже самого высокого своего соседа, и название стоит на одном
   месте всегда. `min-height`, а не `height`: узкий экран переносит ряд кнопок
   под название, и жёсткая высота его обрезала бы. Если когда-нибудь рядом с
   названием встанет что-то выше 36px -- прыжок вернётся, и число надо поднять
   (сейчас самый высокий сосед -- select на 35px). */
.section-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 16px; flex-wrap: wrap; min-height: 36px;
  /* Место под «Обновить»: она висит поверх страницы и на средних ширинах
     наезжала на правый край шапки раздела, закрывая собой «+ Товар» и
     «+ Обращение». Сколько именно отступить, считает measureChrome() -- ширина
     колонки зависит от окна и от меню, числом её не угадать. */
  padding-right: var(--head-gap, 0px);
}
/* These sit straight on the artwork with nothing behind them. */
.section-head h2, .sub-head, #onlineMeta, #shopMeta {
  text-shadow: 0 1px 3px #000, 0 0 12px #000c;
}

.section-head h2 {
  margin: 0; font-size: 13px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--text);
}
.section-head h2::before {
  content: ""; display: inline-block; width: 3px; height: 13px;
  background: var(--accent); margin-right: 10px; vertical-align: -2px;
  box-shadow: 0 0 8px var(--accent);
}
/* Подзаголовок разделов («Покупки», «Использованные», группы справочника).
   Раньше он был мелким и приглушённым и на фоновой картинке читался с трудом --
   игрок просто не находил, где кончаются одни карточки и начинаются другие.
   Теперь это такой же заголовок, как h2, только на ступень тише. */
.sub-head {
  font-size: 12px; color: var(--text); margin: 28px 0 10px;
  font-weight: 700; text-transform: uppercase; letter-spacing: .16em;
}
.sub-head::before {
  content: ""; display: inline-block; width: 3px; height: 12px;
  background: var(--accent-dim); margin-right: 9px; vertical-align: -2px;
  box-shadow: 0 0 8px var(--accent-dim);
}

.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 15px; }
/* Витрина и инвентарь: карточка-плитка, высокая и узкая, как в игровых
   магазинах. Широкая карточка на весь ряд заставляла листать страницу ради
   четырёх товаров, а всё содержимое (картинка, название, цена, кнопка) и так
   идёт сверху вниз -- ширина в ней не работала. */
.cards.cards-tall { grid-template-columns: repeat(auto-fill, minmax(218px, 1fr)); gap: 13px; }
@media (max-width: 520px) {
  /* На телефоне две плитки в ряд -- предел: в одну колонку витрина становится
     лентой в экран высотой на товар. */
  .cards.cards-tall { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
}

.card {
  position: relative;
  background: linear-gradient(180deg, var(--raised) 0%, var(--bg-deep) 140%);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 15px;
}
.card::before {
  content: ""; position: absolute; top: -1px; left: 14px; right: 14px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-hot), transparent);
}
.card.empty {
  border-style: dashed; display: grid; place-items: center;
  min-height: 130px; color: var(--muted);
}
/* «Пусто» -- сообщение обо всём списке, а не ещё одна плитка в ряду: в узкой
   колонке витрины оно переносилось по слогам. */
.cards .card.empty { grid-column: 1 / -1; text-align: center; }
.card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 11px; }
.card-title { font-weight: 600; }
.card-sub { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.species-tag {
  font-family: var(--mono); font-size: 11px; color: var(--cyan);
  background: #46c8f514; border: 1px solid #46c8f533;
  padding: 2px 9px; border-radius: 6px; white-space: nowrap;
}

/* ---------------------------------------------------------- species strip */

.census { margin-bottom: 20px; }
.census-head {
  display: flex; align-items: baseline; gap: 8px;
  margin-bottom: 12px; font-size: 12px;
}
.census-head b { font-size: 17px; color: var(--text); }

/* Compact tiles: the content is a word and a number, so the box should not be
   much bigger than that. */
.census-grid {
  display: grid; gap: 6px;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
}
.census-tile {
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 7px; padding: 6px 9px;
}
.census-tile:hover { border-color: var(--line-hot); }
.census-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
.census-name { font-size: 12px; }
.census-count { font-size: 13px; color: var(--accent); font-weight: 600; }
.census-tile .bar-track { height: 3px; }

/* ----------------------------------------------------------------- bars -- */

.bar { margin: 7px 0; }
.bar-label {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 11px; color: var(--muted); margin-bottom: 3px;
}
.bar-label b { font-family: var(--mono); font-weight: 500; color: var(--text); }
/* Полоса заметно толще прежних 5px: на ней держится весь ответ «сколько
   осталось», а ниткой в пять пикселей разницу между 60% и 75% приходилось
   выцеливать. Плитки переписи оставлены тонкими -- там полоса не показатель, а
   доля вида в общем составе. */
.bar-track { height: 9px; background: #000a; border-radius: 3px; overflow: hidden; border: 1px solid #ffffff0a; }
.bar-fill { height: 100%; border-radius: 3px; transition: width .35s ease; }

/* ------------------------------------------------------------- controls -- */

.btn {
  background: var(--raised-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 9px;
  padding: 7px 13px; cursor: pointer; font: inherit; font-size: 13px;
  transition: border-color .15s, background .15s;
}
.btn:hover:not(:disabled) { border-color: var(--line-hot); background: #16222a; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn-primary {
  background: linear-gradient(140deg, var(--accent), #22c58f);
  border-color: transparent; color: #04140c; font-weight: 650;
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.12); }
.btn-danger { color: var(--danger); border-color: #5a2626; }
.btn-danger:hover:not(:disabled) { background: #2a1212; }
.btn-ghost { background: none; border: none; font-size: 21px; line-height: 1; padding: 0 6px; color: var(--muted); }
.btn-sm { padding: 5px 10px; font-size: 12px; }

select, input[type="text"], input[type="number"] {
  background: #060b0e; color: var(--text);
  border: 1px solid var(--line); border-radius: 9px;
  padding: 7px 10px; font: inherit; font-size: 13px;
}
select:focus, input:focus { outline: none; border-color: var(--accent-dim); }
input[type="range"] { width: 100%; accent-color: var(--accent); }
input[type="color"] {
  width: 38px; height: 27px; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 6px; background: none;
}

.card-actions { display: flex; gap: 7px; margin-top: 13px; flex-wrap: wrap; }

/* ---------------------------------------------------------------- login -- */

.login-card {
  background: linear-gradient(180deg, var(--raised), var(--bg-deep));
  border: 1px solid var(--line); border-radius: 12px;
  padding: 38px; max-width: 430px; text-align: center;
}
.login-logo {
  width: 220px; height: auto; margin: 0 auto 16px; display: block;
  filter: drop-shadow(0 6px 20px #000a);
}
.login-card h1 { font-size: 17px; margin: 0 0 10px; letter-spacing: .14em; text-transform: uppercase; }
.login-card p { color: var(--muted); margin: 0 0 24px; font-size: 13px; }
.login-card form { display: flex; gap: 9px; justify-content: center; flex-wrap: wrap; }
/* Шесть цифр крупно и вразрядку: их диктуют себе под нос, глядя в игровой чат,
   и опечатку в мелком моноширинном поле не видно. */
.login-code {
  width: 190px; text-align: center;
  font-size: 25px !important; letter-spacing: .32em; padding: 9px 4px 9px 14px !important;
}
.login-code::placeholder { color: #2c3a44; letter-spacing: .32em; }
.login-card .btn-primary { padding: 0 22px; font-size: 15px; }
.login-error {
  margin: 14px 0 0; color: var(--danger); font-size: 13px; line-height: 1.45;
}
.login-hint { margin: 22px 0 0 !important; font-size: 12px; opacity: .75; }

/* --------------------------------------------------------------- editor -- */

/* Карточки редактора тянутся до высоты самой длинной (это мутации -- шестнадцать
   слотов): три разновысоких блока в ряд выглядели обрывками. Кнопка «Применить»
   прижата к низу, поэтому у всех трёх она оказывается на одной линии. */
.editor-grid {
  display: grid; gap: 16px; align-items: stretch;
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
}
.editor-grid > .card { display: flex; flex-direction: column; }
.editor-grid > .card > .card-actions,
.editor-grid > .card > .btn:last-child { margin-top: auto; }

.stat-row { display: grid; grid-template-columns: 96px 1fr 78px; gap: 10px; align-items: center; margin-bottom: 9px; }
.stat-row label { font-size: 12px; color: var(--muted); }
.stat-row input[type="number"] { font-family: var(--mono); font-size: 12px; }

.seg { display: inline-flex; background: #060b0e; border: 1px solid var(--line); border-radius: 8px; padding: 3px; gap: 3px; }
.seg button {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 6px;
}
.seg button.on { background: var(--accent-dim); color: #eafff4; }
/* ВКЛАДКИ ПРОФИЛЯ -- КАЖДАЯ СВОИМ ЦВЕТОМ (2026-09-21, просьба пользователя).
   Раньше вся полоса была серой, и зелёной становилась только открытая: глазу
   не за что зацепиться, вкладки различались лишь словом. Цвет здесь -- метка
   раздела, а не украшение: он же потом узнаётся в самом разделе.
   Красится только полоса профиля (#invTabs): у прочих полос (слоты, история,
   друзья внутри раздела) смысл другой -- там это переключатель одного и того
   же списка, и цвет там врал бы, что разделы разные.
   Цвет каждой вкладки задан парой переменных: сам цвет и он же прозрачной
   подложкой -- из переменной подложку не собрать, поэтому две. */
#invTabs button[data-inv="dino"]      { --seg: #35e08a; --seg-dim: #35e08a2e; }
#invTabs button[data-inv="purchases"] { --seg: #4ea8ff; --seg-dim: #4ea8ff2e; }
#invTabs button[data-inv="friends"]   { --seg: #a77bf3; --seg-dim: #a77bf32e; }
#invTabs button[data-inv="ledger"]    { --seg: #f0b429; --seg-dim: #f0b4292e; }
#invTabs button {
  color: var(--seg, var(--muted)); opacity: .66;
  transition: opacity .12s, background .12s;
}
#invTabs button:hover { opacity: 1; background: var(--seg-dim); }
#invTabs button.on {
  opacity: 1; background: var(--seg-dim); color: var(--seg);
  box-shadow: inset 0 0 0 1px var(--seg);
}
#invTabs button.on .seg-count { color: inherit; opacity: .75; }
/* Полоса вкладок по центру строки заголовка. Центр берётся от всей шапки, а не
   от места, оставшегося между заголовком и правым краем: иначе полоса гуляла бы
   вслед за длиной заголовка и появлением админского выбора игрока.
   `--head-gap` -- место под плавающую кнопку «Обновить» (padding-right шапки);
   половину его компенсируем, чтобы центр совпал с центром списка под шапкой. */
#tab-slots .section-head { position: relative; }
.seg-row {
  position: absolute; left: 50%;
  transform: translateX(calc(-50% + var(--head-gap, 0px) / 2));
  display: flex; justify-content: center;
}
/* На узком экране троим в строке не разойтись -- полоса встаёт своей строкой
   под заголовком, как обычный блок. */
@media (max-width: 720px) {
  .seg-row {
    position: static; transform: none;
    width: 100%; order: 3; margin: 2px 0 -4px;
  }
}

.field-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 9px; }
.field-row > label { font-size: 12px; color: var(--muted); }

.zone-list { display: grid; gap: 7px; }
.zone-row { display: grid; grid-template-columns: 1fr auto auto; gap: 9px; align-items: center; }
.zone-row span { font-size: 12px; }
.zone-row input[type="text"] { width: 92px; font-family: var(--mono); font-size: 12px; text-transform: uppercase; }

details.extra { margin-top: 10px; }
details.extra summary {
  cursor: pointer; font-size: 11px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .12em; padding: 6px 0;
}
details.extra .warn-note {
  font-size: 11px; color: var(--amber); margin: 4px 0 9px; line-height: 1.45;
}

/* Шестнадцать слотов в одном столбике: строка сделана низкой, иначе карточка
   уезжает за экран. Подпись -- имя поля игры, её ширина фиксирована. */
.mut-row { display: grid; grid-template-columns: 148px 1fr; gap: 8px; align-items: center; margin-bottom: 4px; }
.mut-row label { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mut-row select { padding: 4px 7px; font-size: 11px; min-width: 0; }
/* Ценник в списке мутаций выровнен неразрывными пробелами -- держится это только
   на моноширинном шрифте. Картинку в <option> вставить нельзя, поэтому валюта
   там обозначена цветным кружком. */
.mut-row select, .mut-row option,
.mut-pick select, .mut-pick option { font-family: var(--mono); }

/* Кнопки карточки окраса. Их шесть, и в столбце шириной в плитку они
   переносились как придётся: «Импорт» уезжал на свою строку, а «Применить
   окрас» вставало в один ряд со «Случайным» и терялось среди равных.
   Теперь раскладка задана: список пресетов с «Загрузить» -- строка, четыре
   редких действия -- ровная сетка, применение -- отдельной строкой. */
.preset-row { gap: 8px; }
.preset-row select { flex: 1 1 auto; min-width: 0; }
.preset-row .btn { flex: none; }
.preset-tools {
  /* auto-fit, а не жёсткие четыре колонки: кнопок стало пять («Из кода»),
     и пятая уезжала одна на вторую строку. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(62px, 1fr));
  gap: 6px; margin-top: 8px;
  /* Воздух перед чертой, за которой «Применить окрас»: без него черта липла к
     кнопкам и читалась как их подчёркивание, а не как граница блока. */
  margin-bottom: 12px;
}
.preset-tools .btn { padding: 5px 4px; font-size: 11px; text-align: center; }
/* «Применить» -- во всю оставшуюся ширину: за ним в карточку и приходят,
   а «Случайный» рядом занимает ровно столько, сколько нужно его подписи.
   Отбито от четырёх мелких кнопок чертой и отступом: применение необратимо, а
   стояло оно вплотную под «Удалить» -- промах мимо строки стоил дорого.
   Отступ задан padding'ом, а не margin'ом: у `.card-actions` в редакторе стоит
   margin-top: auto (прижимает ряд к низу карточки), и margin здесь просто не
   сработал бы. */
.skin-apply { padding-top: 14px; border-top: 1px solid var(--line); }
.skin-apply .btn-primary { flex: 1 1 auto; }
.skin-apply .btn:not(.btn-primary) { flex: none; }

/* Код внешности в редакторе. Две длинные строки, «сейчас» и «станет»: короче их
   не сделать -- это одно значение целиком, и правится оно побайтно, поэтому
   моноширинно и с переносом по любому знаку, а не многоточием. */
.skincode {
  display: grid; gap: 5px;
  margin-top: 12px; padding-top: 11px; border-top: 1px dashed var(--line);
}
.skincode-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 12px; color: var(--muted); cursor: pointer;
}
.skincode-head input[type="checkbox"] { width: 16px; height: 16px; flex: none; }
.skincode-line { display: flex; gap: 9px; align-items: baseline; font-size: 11px; }
.skincode-line code { word-break: break-all; color: var(--text); }
/* Подпись строки -- своей колонкой: без неё «сейчас» и «станет» разной длины
   сдвигали бы сами коды друг относительно друга, а сравнивают их взглядом. */
.skincode-tag {
  flex: none; width: 46px;
  font-size: 9px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
}
.skincode-tag.on { color: var(--accent); }

.swatches { display: flex; gap: 4px; margin-top: 10px; flex-wrap: wrap; }
.swatch { width: 19px; height: 19px; border-radius: 5px; border: 1px solid #0008; box-shadow: inset 0 0 0 1px #ffffff14; }

.tags { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 10px; }
.tag {
  font-size: 11px; background: #a78bfa14; border: 1px solid #a78bfa33;
  color: var(--violet); border-radius: 999px; padding: 2px 9px;
}

/* ----------------------------------------------------------------- shop -- */

/* Категории товаров. На телефоне их больше, чем влезает в строку -- прокрутка
   внутри полоски, а не перенос: сегментированный переключатель в две строки
   перестаёт читаться как один элемент. */
.shop-cats {
  display: flex; width: max-content; max-width: 100%; margin-bottom: 14px;
  overflow-x: auto; scrollbar-width: none;
}
.shop-cats::-webkit-scrollbar { display: none; }
.shop-cats button { white-space: nowrap; }

/* The Mesozoic coin, one asset scaled per context. */
.coin-icon {
  display: block; flex: none;
  width: 18px; height: 18px;
  image-rendering: auto;
  filter: drop-shadow(0 0 5px #f0b42955);
}
.coin-icon.coin-sm { width: 15px; height: 15px; }
/* Монета в кнопке выбора валюты («Купить за 300», «Активировать · 300») -- в строку
   с надписью. Блочной она уезжала под текст, и кнопка вырастала в две строки. */
.btn > .coin-icon { display: inline-block; vertical-align: -3px; margin-left: 5px; }
/* Крупная монета -- рядом с ценой лота: там цифра 21px, и обычная выглядела
   при ней случайной точкой. */
.coin-icon.coin-md { width: 22px; height: 22px; }


/* ------------------------------------------------- применение окраса -- */

.skin-form .skin-preset { font-size: 14px; font-weight: 600; }
/* Игровое имя поля -- второй строкой в подписи: в игре страница внешности на
   английском, и связать «Вариация узора» с Pattern Variation больше негде. */
.skin-orig {
  display: block; font-family: var(--mono); font-size: 10px;
  color: var(--muted); opacity: .75; letter-spacing: .04em;
}
.skin-form select { width: 100%; }
.skin-colors { display: flex; flex-wrap: wrap; gap: 6px 14px; }
.skin-color { display: flex; align-items: center; gap: 7px; font-size: 12px; }
.skin-color .swatch { width: 18px; height: 18px; border-radius: 5px; flex: none; }

/* ------------------------------------------- правка профиля игрока (админ) -- */

/* Значок «админ» в шапке -- кнопка: по ней открывается правка профиля. Выглядит
   он при этом ровно как раньше, поэтому сбрасывается всё, что кнопке дал бы
   браузер, и добавляется только отклик на наведение. */
.badge-btn { font-family: inherit; cursor: pointer; }
.badge-btn:hover { background: #46c8f533; border-color: var(--cyan); }

.admin-pick { margin-bottom: 10px; }
.admin-pick .admin-id { width: 170px; }
.admin-facts {
  display: flex; gap: 14px; flex-wrap: wrap;
  font-size: 12px; margin-bottom: 14px;
  padding-bottom: 12px; border-bottom: 1px solid var(--line);
}

/* ---- окно правки профиля -------------------------------------------------
   Было пять экранов подряд одной простынёй: чтобы снять окрас, приходилось
   прокручивать мимо баланса, подписок и праймов, а «Сохранить» уезжало за
   нижний край окна. Стало: метки-факты, вкладки по разделам и прибитый низ. */
.admin-chips {
  display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
  margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--line);
}
.admin-chip {
  display: flex; align-items: baseline; gap: 6px;
  font-size: 11px; color: var(--muted);
  background: var(--raised-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 2px 10px;
}
.admin-chip b { color: var(--text); font-weight: 600; }
/* Полоса «с аукциона» в профиле: пояснение занимает всё свободное место, кнопка
   прижата к правому краю -- она здесь одна и действует сразу. */
.admin-lock {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 12px; padding: 8px 10px;
  background: #ff5b5b0d; border: 1px solid #ff5b5b2e; border-radius: 12px;
}
.admin-lock .muted { flex: 1 1 220px; font-size: 11px; }

/* Полоса разделов прокручивается вбок, как категории витрины: пять кнопок в
   узком окне в строку не всегда влезают, а перенос ломает вид переключателя. */
.admin-tabs {
  display: flex; width: max-content; max-width: 100%; margin-bottom: 12px;
  overflow-x: auto; scrollbar-width: none;
}
.admin-tabs::-webkit-scrollbar { display: none; }
.admin-tabs button { white-space: nowrap; }
.admin-form { gap: 0; }
/* Все разделы окна одного размера. «Кошелёк» высокий, «Окрасы» -- в две строки,
   и окно на каждом переключении прыгало во весь экран и обратно: кнопки внизу
   уезжали из-под курсора, а страница за окном дёргалась. Высота теперь общая, а
   содержимое прокручивается внутри раздела -- низ окна («Сохранить») при этом
   остаётся на месте, потому что он лежит снаружи. */
.admin-pane {
  display: grid; gap: 10px; align-content: start;
  height: min(56vh, 440px); overflow-y: auto;
  /* Место под полосу прокрутки: без него содержимое дёргалось влево на тех
     разделах, где она появляется. */
  padding-right: 6px;
  scrollbar-width: thin; scrollbar-color: var(--line-hot) transparent;
}
.admin-pane::-webkit-scrollbar { width: 8px; }
.admin-pane::-webkit-scrollbar-track { background: transparent; }
.admin-pane::-webkit-scrollbar-thumb {
  background: var(--line-hot); border-radius: 4px;
  border: 2px solid transparent; background-clip: content-box;
}
.admin-pane::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; }

/* Баланс: поле итога, быстрые прибавки и «сколько было». Правят его почти
   всегда как «выдать столько-то», а в поле стоит итог -- без разницы рядом
   пришлось бы считать в уме на каждую выдачу. */
.admin-money { display: grid; gap: 6px; }
.admin-money > input { width: 100%; }
.admin-quick { gap: 5px; }
.admin-quick .btn { padding: 3px 9px; font-size: 11px; }
.admin-delta { font-size: 11px; color: var(--muted); margin-left: auto; }
.admin-delta.up { color: var(--accent); }
.admin-delta.down { color: var(--danger); }

/* Строка подписки: название, дата, состояние, кнопки -- в одну линию.
   Состояние своей колонкой, иначе «истекла» и «12 дн.» разной длины двигали бы
   кнопки, и попасть в «Снять» можно было только прицелившись. */
.admin-sub-row .admin-note {
  flex: none; min-width: 62px; font-size: 11px; text-align: center;
  border: 1px solid var(--line); border-radius: 999px; padding: 2px 8px;
}
.admin-note.on { color: var(--accent); border-color: #35e08a3d; background: #35e08a12; }
.admin-note.bad { color: var(--danger); border-color: #ff5b5b3d; background: #ff5b5b0f; }
.admin-note.off { color: var(--muted); }

/* Низ окна прибит к его краю: разделы длинные, и «Сохранить» внизу пятого
   экрана означало прокрутку туда-обратно на каждую правку. */
.admin-foot {
  position: sticky; bottom: -20px; z-index: 2;
  margin-top: 14px; padding: 12px 0;
  background: linear-gradient(180deg, transparent, var(--bg-deep) 22%);
}
.admin-rows { display: grid; gap: 6px; }
/* Название слева, всё управление -- справа от него: строки подписок и праймов
   разной длины, и без общей колонки под название они бы плясали. */
.admin-row { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.admin-row-name { flex: 1 1 150px; min-width: 0; font-size: 13px; }
.admin-row input[type="date"] { width: 150px; }
.admin-row .admin-count { width: 74px; }
.admin-row .admin-note { font-size: 11px; min-width: 52px; }
.admin-skins { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px 12px; }
.admin-check { display: flex; align-items: center; gap: 8px; font-size: 13px; min-width: 0; }
.admin-check input[type="checkbox"] { flex: none; width: 16px; height: 16px; }
.admin-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Кнопки в строке игрока -- в ряд, а не столбиком. */
.row-actions { display: flex; gap: 6px; flex: none; }

/* ---- список игроков (админ) ------------------------------------------------
   Раньше это была таблица из четырёх колонок, в которой всё выглядело
   одинаково: имя, семнадцать цифр id и полная дата съедали строку, и нужного
   человека приходилось искать глазами. Теперь строка -- карточка: имя крупно,
   id под ним, остальное метками справа. */
.player-list { display: grid; gap: 7px; }
.player-row {
  display: flex; align-items: center; gap: 13px; flex-wrap: wrap;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 9px 13px;
}
.player-row:hover { border-color: var(--line-hot); }
/* Тот, кто сейчас в игре, отбит слева зелёной кромкой: список длинный, и это
   первое, что в нём ищут. */
.player-row.on { border-left: 2px solid var(--accent); }
.player-face {
  flex: none; width: 36px; height: 36px; border-radius: 8px;
  display: grid; place-items: center;
  background: #05090c; border: 1px solid var(--line);
  color: var(--muted); font-size: 15px; font-family: var(--mono);
}
.player-who { flex: 1 1 200px; min-width: 0; }
.player-name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 14px; }
.player-live {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11px; color: var(--accent);
  background: #35e08a12; border: 1px solid #35e08a33;
  border-radius: 999px; padding: 1px 9px;
}
.player-live .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 8px var(--accent);
}
/* Id копируется нажатием: его переносят в конфиги и бан-листы, а выделять
   мышью семнадцать цифр из строки неудобно. */
.player-id {
  background: none; border: none; padding: 0; cursor: pointer;
  font-family: var(--mono); font-size: 11px; color: var(--muted);
}
.player-id:hover { color: var(--cyan); }
.player-facts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.player-chip {
  font-size: 11px; color: var(--text);
  background: var(--raised-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 2px 10px; white-space: nowrap;
}
.player-chip b { font-family: var(--mono); }
#playerSearch { min-width: 180px; }

/* ---- доступ к панели (владелец) --------------------------------------------
   Строки те же, что в списке игроков, -- различает их только метка роли. Цвет у
   метки не украшение: владельцев в списке двое-трое на десяток админов, и
   искать их глазами по слову «владелец» пришлось бы каждый раз. */
.role-chip {
  font-size: 11px; border-radius: 999px; padding: 2px 10px; white-space: nowrap;
  border: 1px solid var(--line); background: var(--raised-2); color: var(--text);
}
.role-chip.owner { color: var(--amber); border-color: #f0b42955; background: #f0b42912; }
.role-chip.admin { color: var(--cyan); border-color: #46c8f555; background: #46c8f512; }
/* Откуда права: config.json или Game.ini сервера. Приглушённая -- это подпись к
   строке, а не сама роль. */
.role-src { font-size: 11px; color: var(--muted); white-space: nowrap; }
.player-row.owner { border-left: 2px solid var(--amber); }
/* Своя строка отбита: снять права с себя панель не даёт, и понятно, почему
   именно у неё нет кнопок. */
.player-row.self { background: #111a21; }
/* Админ, которому оставили только просмотр: это не ошибка, но и не обычное
   состояние -- метка янтарная, чтобы такую строку было видно в списке. */
.player-chip.warn-chip { color: var(--amber); border-color: #f0b42955; }

/* Окно правки прав: галочка на раздел, под названием -- что она открывает.
   Пояснение здесь обязательно: «События» без расшифровки владелец отметит
   наугад. */
.rights-form { display: grid; gap: 10px; margin: 4px 0 10px; }
/* Раздел -- рамка, работы внутри -- строки. Иначе девятнадцать одинаковых
   галочек подряд читались бы одним списком, в котором не видно, где кончается
   магазин и начинается содержимое сайта. */
.rights-group { border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.rights-row {
  display: flex; gap: 10px; align-items: flex-start; cursor: pointer;
  background: var(--raised); padding: 9px 11px;
}
.rights-row:hover { background: var(--raised-2); }
.rights-row input[type="checkbox"] { flex: none; width: 16px; height: 16px; margin-top: 2px; }
/* Заголовок раздела: галочка на всю группу. Отбит фоном и чертой -- это не
   ещё одна работа в списке, а выключатель для всех. */
.rights-head { background: var(--raised-2); border-bottom: 1px solid var(--line); }
.rights-head .rights-title { font-size: 14px; color: var(--cyan); }
.rights-leaf { padding-left: 26px; }
.rights-leaf + .rights-leaf { border-top: 1px solid #1d2b3355; }
.rights-text { min-width: 0; }
.rights-title { font-size: 13px; }
.rights-text .muted { font-size: 12px; }

@media (max-width: 720px) {
  .player-facts, .row-actions { flex: 1 1 100%; }
}

@media (max-width: 620px) {
  .admin-skins { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------- профиль -- */

.profile-card { margin-bottom: 18px; }
.profile-who { display: flex; align-items: center; gap: 14px; }
.profile-face {
  width: 54px; height: 54px; flex: none;
  border-radius: 14px; overflow: hidden;
  display: grid; place-items: center;
  background: #05090c; border: 1px solid var(--line-hot);
  box-shadow: 0 0 0 3px #04070a, 0 6px 18px #0009;
  color: var(--muted); font-size: 20px; font-family: var(--mono);
}
/* В лице профиля либо своя картинка игрока, либо первая буква имени: аватара
   из Steam тут нет (2026-09-21), правило .profile-id ушло вместе со строкой
   Steam ID. У друзей своё лицо -- .friend-face, его это не касается. */
.profile-face img { width: 100%; height: 100%; object-fit: cover; }
/* Лицо -- оно же кнопка смены аватара (2026-09-21): отдельной кнопки под именем
   нет. Чтобы это не было тайной, квадрат светится под курсором, а снизу
   выезжает подпись «Сменить». Подпись лежит поверх картинки и появляется только
   при наведении и с клавиатуры: постоянная надпись на лице -- это уже не лицо. */
.profile-face.face-pick { cursor: pointer; position: relative; }
.profile-face.face-pick:hover,
.profile-face.face-pick:focus-visible {
  border-color: var(--accent-dim); outline: none;
}
.face-pick-hint {
  position: absolute; left: 0; right: 0; bottom: 0;
  /* Светлый текст на плотной подложке, а не зелёный: подпись ложится ПОВЕРХ
     чужой картинки, и зелёное по зелёному не прочитать. */
  background: #04070aee; color: #e8f1ee;
  font-size: 9px; letter-spacing: .04em; text-align: center;
  padding: 2px 0; opacity: 0; transition: opacity .12s;
}
.profile-face.face-pick:hover .face-pick-hint,
.profile-face.face-pick:focus-visible .face-pick-hint { opacity: 1; }
/* Буква съезжает вверх ровно настолько, чтобы подпись её не перечеркнула. */
.profile-face.face-pick:hover .profile-letter,
.profile-face.face-pick:focus-visible .profile-letter { transform: translateY(-5px); }
.profile-letter { transition: transform .12s; }
/* ЗНАК ПОДПИСЧИКА (2026-09-21, просьба пользователя). Сначала подписка была
   плиткой в «Покупках», потом полосой со списком в карточке профиля -- и то и
   другое отменено: полоса занимала полкарточки ради сведений, которые читают
   раз в месяц. Осталась метка перед ником и такая же рамка у аватара.
   ЦВЕТ И НАДПИСЬ ЗДЕСЬ НЕ ЗАДАНЫ: они берутся из самой подписки (`chat_badge`,
   `chat_color` -- те же поля, что красят ник в чате) и ставятся стилем прямо
   на узел. В CSS остаётся только форма; ненастроенная метка выглядит так же
   тихо, как значок в чате. */
.vip-tag {
  font-size: 10px; font-weight: 700; letter-spacing: .1em;
  color: var(--text); border: 1px solid var(--line-hot);
  background: #ffffff0a; border-radius: 999px; padding: 1px 7px;
  line-height: 1.5; flex: none; white-space: nowrap;
  max-width: 12em; overflow: hidden; text-overflow: ellipsis;
}
/* Значок-кнопка: по нажатию игрок выбирает свой цвет метки. Внешне он тот же,
   меняется только поведение -- курсор и подсветка под курсором. */
/* ВАЖНО: не `font: inherit` -- этой строкой значок-кнопка подхватывал кегль
   имени (17 px) и вырастал вдвое против значка-надписи. Кнопке нужно взять у
   страницы только гарнитуру, всё остальное задано выше, в `.vip-tag`. */
.vip-tag.vip-pick {
  cursor: pointer; font-family: inherit;
  font-size: 10px; font-weight: 700; letter-spacing: .1em;
}
.vip-tag.vip-pick:hover { filter: brightness(1.25); }
.vip-tag.vip-pick:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; }
/* ОКНО ВЫБОРА ЦВЕТА МЕТКИ. Сверху образец -- то же лицо, значок и ник, что в
   карточке профиля, только мельче: цвет выбирают, глядя на результат, а не на
   код. Ниже одно поле -- палитра браузера. Готовые кружки и поле с кодом
   отсюда убраны (просьба пользователя): палитра и так даёт любой оттенок, а
   два лишних ряда делали из выбора цвета целую форму. */
.vip-sample {
  display: flex; align-items: center; gap: 12px;
  background: #05090c; border: 1px solid var(--line); border-radius: 12px;
  padding: 11px 13px; margin: 4px 0 14px;
}
.vip-sample-ring {
  width: 46px; height: 46px; flex: none; border-radius: 13px; overflow: hidden;
  border: 1px solid var(--line-hot); background: #04070a;
  display: grid; place-items: center;
  font-family: var(--mono); font-size: 18px; color: var(--muted);
}
.vip-sample-ring img { width: 100%; height: 100%; object-fit: cover; }
.vip-sample-who { min-width: 0; }
.vip-sample-name { display: flex; align-items: center; gap: 8px; font-size: 15px; }
.vip-sample-note { font-size: 10px; letter-spacing: .04em; margin-top: 3px; }
.vip-color-row { display: flex; align-items: center; gap: 9px; }
/* Единственный орган управления в окне -- поэтому крупнее обычного поля. */
.vip-color-row input[type="color"] {
  width: 84px; height: 36px; padding: 3px; border: 1px solid var(--line);
  border-radius: 10px; background: #05090c; cursor: pointer;
}
.vip-color-row input[type="color"]:hover { border-color: var(--line-hot); }
.vip-note { font-size: 11px; margin: 13px 0 0; }
/* Имя со знаком -- одной строкой; знак не растягивается и не переносится. */
.profile-name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Срок подписки под ником: мелко и приглушённо -- это справка, а не заголовок. */
.profile-sub-when { font-size: 11px; margin-top: 3px; }
/* Кнопка ежедневных монет под именем: своей ширины, не во всю карточку. */
.profile-ident .daily-claim { width: auto; margin-top: 7px; }
.profile-ident { min-width: 0; }
.profile-ident .card-title { font-size: 17px; }
/* Категории покупок: полоса-фильтр под именем игрока. Прокручивается вбок, как
   и такая же полоса в магазине -- категорий может быть много. */
.profile-cats { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.profile-cats::-webkit-scrollbar { display: none; }
.profile-cats button { white-space: nowrap; }

/* Слоты -- в правом верхнем углу карточки, вровень с именем. Полоска под
   числом: «5 / 25» отвечает на «сколько занято», но не на «много это или мало»,
   а решение докупить слот принимается именно по второму. */
.profile-slots {
  margin-left: auto; align-self: flex-start;
  display: grid; gap: 3px; justify-items: end; text-align: right;
  min-width: 116px;
}
.profile-slots b { font-size: 17px; }
.profile-slots .bar-track { width: 100%; }
/* Из чего сложилась вместимость -- строкой под полоской: купленное навсегда и
   подписочное исчезают по-разному, и это стоит знать до того, как подписка
   кончится. */
.profile-slots-note { font-size: 10px; line-height: 1.3; }

/* Переключатель разделов -- нижняя полоса карточки профиля, а не строка под
   ней: он относится к тому, что лежит ниже, но принадлежит карточке -- так у
   списков появляется шапка, а у карточки низ. Полоса растянута до краёв
   карточки и лежит на своём фоне, как и всё, что в ней отбито чертой. */
.profile-card .inv-head {
  margin: 15px -15px -15px; padding: 10px 15px;
  background: #05090c80; border-top: 1px solid var(--line);
}
/* Паспорт живёт на своей вкладке в обычной карточке. */
#dinoPanel .card { padding-top: 13px; }

/* Счётчики игрока -- плитками, по числу на плитку. Раньше это была строка
   «пройдено: 19.3 км  часов в игре: 23.2  всего заработано: 12873» одним
   размером и цветом: четыре числа подряд сливались в одно длинное слово, и
   найти в нём нужное можно было только прочитав всё. */
/* Плитки счётчиков МЕЛЬЧЕ прежнего (2026-09-21, просьба пользователя): это
   справка, на которую смотрят мельком, а занимали они полосу в полкарточки.
   Уже и ниже -- зато их больше помещается в ряд, и рядом с ними встала
   реферальная программа. */
.profile-stats {
  display: grid; gap: 7px; margin-top: 12px;
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
}
.stat-tile {
  min-width: 0; padding: 6px 10px;
  background: #05090cb3; border: 1px solid var(--line); border-radius: 10px;
  transition: border-color .15s;
}
.stat-tile:hover { border-color: var(--line-hot); }
.stat-label {
  font-size: 8px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stat-value {
  display: flex; align-items: center; gap: 5px;
  font-size: 15px; line-height: 1.15; color: var(--text);
}
/* Плитка-кнопка (реферальная программа): та же плитка, но на неё жмут --
   отсюда курсор, подсветка рамки акцентом и выравнивание текста по левому
   краю (кнопка по умолчанию центрирует). Подпись у неё длинная и переносится
   -- ряд всё равно держит одну высоту. */
.stat-tile.stat-link {
  cursor: pointer; text-align: left; font: inherit; display: block;
}
.stat-tile.stat-link .stat-label { white-space: normal; }
.stat-tile.stat-link:hover,
.stat-tile.stat-link:focus-visible {
  border-color: var(--accent-dim); outline: none;
}
.stat-tile.stat-link:hover .stat-value { color: var(--accent); }
/* Единица измерения -- при числе, но тише его: «19.3 км» одним кеглем читается
   как одно слово, и число в нём теряется. */
.stat-value i { font-style: normal; font-size: 11px; color: var(--muted); }
.stat-tile.accent .stat-value { color: var(--amber); }

/* Шапка инвентаря: слева переключатель «Покупки | История операций», справа
   фильтр той вкладки, что открыта. Заголовков-подписей здесь больше нет --
   вкладка называет раздел сама. */
.inv-head { margin: 20px 0 12px; align-items: center; }
.inv-head .row { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
.inv-head .row::-webkit-scrollbar { display: none; }
/* Пустое место вместо значка валюты у строк без денег: без него столбец с
   причиной у активаций уезжал бы влево. */
.coin-gap { display: inline-block; width: 15px; flex: none; }

/* Карточка покупки -- такая же плитка, как в витрине: тот же товар, только уже
   свой. Картинка сверху, состояние в бейдже, кнопка прижата к низу -- ряд
   получается ровным, чем бы карточки ни отличались внутри. */
.purchase {
  display: flex; flex-direction: column; min-height: 250px;
  padding-top: 0; overflow: hidden;
}
/* Инвентарь мельче витрины (2026-08-17). В витрине плитка продаёт -- картинка в
   ней главное; в инвентаре по плиткам ищут уже купленное, и крупная картинка
   заставляла листать страницу ради десятка своих же покупок. Уменьшен только
   инвентарь: у #shopGrid размеры прежние. */
#purchaseGrid.cards-tall {
  grid-template-columns: repeat(auto-fill, minmax(164px, 1fr)); gap: 10px;
  /* ВСЕ РЯДЫ ОДНОЙ ВЫСОТЫ (2026-09-21, замечание пользователя). Высоту ряда
     задавало его содержимое: в первом ряду попадался прайм -- у него две
     кнопки и строка «Для вида», -- и он растягивал ряд, а следующий ряд из
     простых плиток выходил заметно ниже. Сетка из плиток разной высоты
     читается как недоделанная, хотя каждая плитка сама по себе верна.
     `1fr` у неявных рядов равняет их все по самому высокому; содержимому это
     не мешает -- у такого ряда минимум всё равно по содержимому. */
  grid-auto-rows: 1fr;
}
#purchaseGrid .card { padding: 10px; }
/* Плитка инвентаря стала выше (там же): при одинаковых рядах прежние 145 px
   выглядели полоской, а место под картинку есть. Это не возврат к витринному
   размеру (250 px) -- в инвентаре по плиткам ищут своё, и десяток покупок
   по-прежнему должен помещаться в экран. */
#purchaseGrid .purchase { min-height: 235px; padding-top: 0; }
/* Картинка -- полосой 16:7, а не витринным 16:9: в инвентаре она узнаёт покупку,
   а не продаёт её. Была 16:5 -- при подросшей плитке такая полоска смотрелась
   обрезком, а лишние два деления высоты как раз показывают, что на картинке.
   СООТНОШЕНИЕ, А НЕ РАСТЯЖЕНИЕ (2026-09-21, замечание пользователя): сначала
   картинке отдали остаток высоты (`flex: 1 1 auto`), чтобы не было пустоты под
   названием, -- и окно вышло РАЗНЫМ: у прайма, где есть ещё строка «Для вида»
   и вторая кнопка, картинка ужималась сильнее, чем у простой плитки рядом.
   Плитки одного размера с картинками разного -- хуже, чем немного воздуха над
   кнопкой, поэтому растяжение убрано: колонки одной ширины, соотношение одно,
   значит и окно с картинкой одинаковое во всех плитках. */
#purchaseGrid .purchase-media { margin: 0 -10px 7px; aspect-ratio: 4 / 3; }
#purchaseGrid .purchase-media .shop-icon { font-size: 22px; }
#purchaseGrid .purchase .card-title { font-size: 12px; }
#purchaseGrid .purchase .card-head { margin-bottom: 5px; gap: 6px; }
#purchaseGrid .species-tag { font-size: 9px; padding: 1px 6px; }
/* НА ПЛИТКЕ -- ТОЛЬКО ОПОЗНАНИЕ (2026-08-17). Плитка отвечает на один вопрос:
   «что это за покупка» -- картинка, название, для какого вида. Всё остальное,
   что на ней стояло -- состав, пояснение, подсказка-препятствие, дата покупки,
   -- переехало в окно подтверждения: читают это в момент решения, а не когда
   плитка попалась на глаза, и там для текста есть место.
   Узлы остаются в разметке: те же самые классы работают в витрине и на
   аукционе, а здесь их прячет одно правило -- вернуть надпись можно, убрав
   строку, а не переписывая сборку карточек. */
#purchaseGrid .fact-chips,
#purchaseGrid .purchase-note,
#purchaseGrid .card-hint,
#purchaseGrid .purchase .card-sub { display: none; }
#purchaseGrid .purchase-for { margin-top: 6px; font-size: 10px; }
#purchaseGrid .purchase-for b { font-size: 11px; }
#purchaseGrid .purchase .btn { margin-top: 7px; }
#purchaseGrid .purchase > .card-actions { padding-top: 7px; gap: 6px; }
/* Кнопки в узкой плитке -- мельче: на плитке их бывает две («Мутации» и
   «Активировать»), и в прежнем кегле они переносились на две строки, отыгрывая
   высотой всё, что плитка выиграла шириной. */
#purchaseGrid .purchase > .card-actions .btn { font-size: 11px; padding: 4px 8px; }
@media (max-width: 520px) {
  #purchaseGrid.cards-tall { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 9px; }
}
/* Шапка-картинка выходит за поля карточки: поля остаются у остального
   содержимого, которое дописывают сверху вызывающие. */
.purchase-media {
  position: relative; margin: 0 -15px 12px; aspect-ratio: 16 / 9;
  display: grid; place-items: center; overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 0%, #35e08a12, transparent 70%),
    linear-gradient(180deg, var(--raised-2), var(--raised));
  border-bottom: 1px solid var(--line);
}
.purchase-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.purchase-media .shop-icon { font-size: 34px; opacity: .8; margin: 0; }
.purchase .card-head { margin-bottom: 8px; align-items: center; }
/* Название -- в две строки максимум и покрупнее подписей под ним: в плитке
   шириной в две ладони заголовок и пояснение одного кегля читались как сплошной
   абзац. */
.purchase .card-title {
  font-size: 14px; font-weight: 650; line-height: 1.25; letter-spacing: .01em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.purchase .species-tag { font-size: 10px; padding: 2px 7px; }
/* Дата покупки -- последней строкой и тише всего: её читают раз в жизни.
   order здесь не украшение: дописывают карточку сверху вниз вызывающие
   («для какого вида», подсказка, выбранные мутации), а дата уходит в неё из
   общего покроя раньше их всех -- и оказывалась посреди текста. */
.purchase .card-sub { order: 2; margin-top: 11px; font-size: 10px; opacity: .8; }
.purchase > .card-actions, .purchase > .btn:last-child { order: 3; }
.purchase-note { font-size: 12px; margin-top: 8px; line-height: 1.45; }

/* Состав покупки -- метками, а не строкой через точку. «условий: 10 из 10 ·
   готовность · рост до 75% · желудок до 50% · мутаций: до 8» одной строкой
   читается как одно длинное предложение: чтобы найти в нём рост, приходится
   прочитать всё остальное. */
.fact-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 9px; }
/* Список выбранных мутаций пуст, пока выбор не сделан: пустая строка с отступом
   раздвигала карточку ни за чем. */
.fact-chips:empty { display: none; }
.fact-chips span {
  font-size: 11px; line-height: 1.35; color: var(--text);
  background: #ffffff0a; border: 1px solid var(--line);
  border-radius: 6px; padding: 2px 8px;
}
/* Для кого куплено: строка-ответ на «а тем ли динозавром я зайду». Название
   вида в ней -- главное, поэтому оно светлее подписи. */
.purchase-for {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  margin-top: 9px; font-size: 11px; color: var(--muted);
}
.purchase-for b { color: var(--cyan); font-size: 12px; font-weight: 600; }
/* Подсказка «что сделать, чтобы кнопка сработала». Раньше это был такой же
   серый текст, как и всё прочее на карточке, и терялся ровно там, где он
   единственный что-то объясняет. */
.card-hint {
  margin-top: 10px; padding: 7px 10px;
  font-size: 11px; line-height: 1.4; color: var(--muted);
  background: #ffffff06; border-left: 2px solid var(--line-hot);
  border-radius: 12px;
}
.card-hint.warn {
  color: var(--amber); background: #f0b4290d; border-left-color: var(--amber);
}
/* Метка типа на картинке -- как в витрине: покупку узнают по той же подписи, по
   какой выбирали товар, и в шапке карточки она больше не спорит с названием. */
.purchase-media .shop-badge { top: 8px; left: 8px; }
/* Истёкшая подписка: покупка была, силы больше нет. */
.purchase.dim { opacity: .6; }
.purchase.dim .species-tag { color: var(--muted); background: #ffffff08; border-color: var(--line); }
.purchase .btn { margin-top: 12px; }
/* Кнопки -- к низу плитки: карточки в ряду разной высоты, и без этого «Применить»
   у соседей оказывалась на разных уровнях. */
.purchase > .card-actions,
.purchase > .btn:last-child { margin-top: auto; }
.purchase > .card-actions { padding-top: 11px; border-top: 1px solid var(--line); }
.purchase > .card-actions .btn { flex: 1 1 auto; margin-top: 0; }

/* ------------------------------------------------------------ правила -- */

/* РАЗМЕТКА И ПАНЕЛЬ КНОПОК (2026-08-20). Правила и новости были голым текстом:
   выделить пункт было нечем, и его выделяли КАПСОМ. Теперь у поля есть панель --
   как в текстовом редакторе, -- но кнопки не «делают текст жирным», а вставляют
   ту же разметку, которую можно набрать руками. В файле остаётся обычный текст;
   на страницу он уезжает разобранным в узлы, а не через innerHTML.

   Панель приклеена к верху поля: это одно целое, а не кнопки, живущие рядом. */
.md-field { display: grid; gap: 0; min-width: 0; }
.md-bar {
  display: flex; align-items: center; gap: 2px; flex-wrap: wrap;
  padding: 4px 6px;
  background: var(--raised-2); border: 1px solid var(--line);
  border-bottom: none; border-radius: 9px 9px 0 0;
}
.md-field > textarea { border-radius: 0 0 9px 9px; }
.md-btn {
  min-width: 26px; height: 24px; padding: 0 6px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; line-height: 1; color: var(--muted);
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  cursor: pointer; transition: color .15s ease, background .15s ease;
}
.md-btn:hover { color: var(--text); background: #ffffff0d; }
.md-btn:active { background: #ffffff16; }
/* Кнопка показывает собой то, что делает: жирная -- жирным, курсив -- курсивом. */
.md-bold { font-weight: 800; }
.md-italic { font-style: italic; }
.md-strike { text-decoration: line-through; }
.md-mark { color: var(--accent); }
.md-sep { width: 1px; height: 16px; margin: 0 4px; background: var(--line); }

/* Палитра цвета. Кнопка -- буква с цветной полосой снизу, как в любом текстовом
   редакторе: по ней видно и что она красит, и чем красила в прошлый раз... точнее,
   чем красит вообще -- полоса радужная, потому что цвет выбирают в самой палитре,
   а не запоминают за кнопкой. Шесть цветов темы: подобранный на глаз розовый на
   чёрном фоне читался бы хуже любого из них. */
/* Выравнивание абзаца: значок из трёх полосок, как в любом редакторе. Шрифтовых
   глифов под это нет, а подпись словами заняла бы всю панель. */
.md-al { flex-direction: column; justify-content: center; gap: 2px; }
.md-al i { display: block; height: 2px; border-radius: 1px; background: currentColor; }
.md-al i:nth-child(1), .md-al i:nth-child(3) { width: 13px; }
.md-al i:nth-child(2) { width: 8px; }
.md-al-l { align-items: flex-start; }
.md-al-c { align-items: center; }
.md-al-r { align-items: flex-end; }
.md-al-j i:nth-child(2) { width: 13px; }
/* В заголовке выравнивание -- переключатель: выбран ровно один, и он подсвечен.
   В тексте выравнивание метится в самой строке, и подсвечивать там нечего. */
.md-al.on { color: var(--accent); background: #35e08a14; }

/* Панель заголовка: то же, что над текстом, но поле под ней в одну строку. */
.md-bar-title { border-radius: 9px 9px 0 0; }
.md-field > input { border-radius: 0 0 9px 9px; }
.md-bar-title .rules-size-pick { width: 132px; height: 24px; padding: 0 6px; font-size: 11px; }

.md-color { position: relative; display: inline-flex; }
.md-color-btn {
  flex-direction: column; gap: 2px; font-weight: 700;
  padding-bottom: 3px;
}
.md-color-btn::after {
  content: ""; display: block; width: 14px; height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), var(--cyan), var(--amber), var(--danger));
}
.md-palette {
  position: absolute; top: calc(100% + 5px); left: 0; z-index: 5;
  display: grid; grid-template-columns: repeat(4, 22px); gap: 6px;
  padding: 8px; border-radius: 9px;
  background: var(--raised); border: 1px solid var(--line-hot);
  box-shadow: 0 10px 26px #000a;
}
.md-swatch {
  width: 22px; height: 22px; padding: 0; border-radius: 6px;
  border: 1px solid #ffffff1f; cursor: pointer;
  transition: transform .12s ease;
}
.md-swatch:hover { transform: scale(1.12); }
/* «Без цвета» -- не цвет, поэтому и выглядит иначе: пустая клетка с крестиком. */
.md-swatch-none {
  background: transparent; color: var(--muted);
  border-style: dashed; border-color: var(--line-hot);
  font-size: 13px; line-height: 1;
}
.md-swatch-none:hover { color: var(--text); }
/* «Просмотр» -- не форматирование, а проверка: он справа и подписан словом. */
.md-eye { margin-left: auto; padding: 0 9px; font-size: 11px; }
.md-eye.on { color: var(--accent); background: #35e08a14; }
.md-preview {
  margin-top: 6px; padding: 10px 12px;
  border: 1px dashed var(--line-hot); border-radius: 9px; background: var(--bg-deep);
  font-size: 13px; line-height: 1.5;
}

/* Как разметка выглядит на самой странице. Подзаголовок -- внутри раздела, он
   мельче заголовка карточки: иначе спорит с ним за главное. */
.rich-sub {
  margin: 12px 0 6px; font-weight: 700; font-size: 13px;
  letter-spacing: .02em; color: var(--text);
}
.rich-sub:first-child { margin-top: 0; }
.rich-list { margin: 0 0 9px; padding-left: 20px; }
.rich-list li { margin: 0 0 3px; }
.rich-list li:last-child { margin-bottom: 0; }
/* Примечание -- полосой слева, как описание мутации: это оговорка к правилу, а
   не само правило. */
.rich-quote {
  margin: 0 0 9px; padding: 2px 0 2px 11px;
  border-left: 2px solid var(--line-hot); color: var(--muted);
}
.rich-mark { background: #35e08a1f; color: var(--accent); border-radius: 4px; padding: 0 4px; }
.rich-link { color: var(--cyan); }
/* Цвет текста -- классом, а не стилем в разметке: в правилах лежит имя из
   палитры, и подставить туда что-либо своё нельзя. */
.rich-c-green { color: var(--accent); }
.rich-c-cyan { color: var(--cyan); }
.rich-c-amber { color: var(--amber); }
.rich-c-red { color: var(--danger); }
.rich-c-violet { color: var(--violet); }
.rich-c-grey { color: var(--muted); }
/* Выравнивание абзаца. Левый край отдельного класса не имеет: это обычный абзац,
   и метки в тексте у него тоже нет. */
.rich-al-c { text-align: center; }
.rich-al-r { text-align: right; }
.rich-al-j { text-align: justify; }
/* У списка выравнивание красит сами пункты, а маркеры остаются на месте --
   иначе список по центру выглядит сломанным. */
ul.rich-al-c, ol.rich-al-c, ul.rich-al-r, ol.rich-al-r { list-style-position: inside; padding-left: 0; }

/* Картинка вступления -- шапкой карточки, во всю её ширину и без обрезки по
   бокам: на правилах это чаще баннер сервера, чем иллюстрация к абзацу. Правило
   то же, что у новости, но карточка правил со своими отступами. */
.rules-shot {
  margin: -15px -15px 14px; border-bottom: 1px solid var(--line);
  background: var(--bg-deep); overflow: hidden; border-radius: 12px 12px 0 0;
}
.rules-shot img { display: block; width: 100%; max-height: 320px; object-fit: cover; }

.rules-intro p, .rules-text { margin: 0 0 9px; }
.rules-intro p:last-child, .rules-text:last-child { margin-bottom: 0; }
/* ПРАВИЛА -- ОДНО ОКНО (2026-08-20). Было: карточка вступления, под ней карточка
   на каждый раздел -- десяток одинаковых коробочек с промежутками, и правила
   выглядели не документом, а лентой. Теперь одна карточка: картинка шапкой,
   вступление, а разделы -- строками внутри, разделёнными волосяной линией.

   РАЗДЕЛ СВЁРНУТ: сорок разделов подряд -- простыня в несколько экранов, по
   которой не найти нужный пункт. Заголовки видны все, текст раскрывается
   щелчком; раздел -- родной <details>, как группа в справочнике мутаций. */
.rules-doc { padding-bottom: 4px; }
.rules-empty { padding: 6px 0 10px; }
/* Отбивка между вступлением и первым разделом -- та же линия, что и между
   разделами: одно окно не должно распадаться на два. */
.rules-list { margin-top: 12px; }
.rules-doc > .rules-intro + .rules-list { border-top: 1px solid var(--line); }
.rules-section { border-top: 1px solid var(--line); }
.rules-section:first-child { border-top: none; }
.rules-section > .card-head {
  align-items: baseline; gap: 11px; margin: 0;
  padding: 9px 8px; border-radius: 9px;
  cursor: pointer; list-style: none;
}
.rules-section > .card-head:hover { background: #ffffff08; }
.rules-section[open] > .card-head { margin-bottom: 2px; }
/* Текст раздела встаёт под заголовком, с тем же отступом, что и он. */
.rules-section > :not(.card-head) { padding: 0 8px; }
.rules-section[open] { padding-bottom: 10px; }
.rules-section > .card-head::-webkit-details-marker { display: none; }
.rules-section > .card-head:hover .card-title { color: var(--accent); }
.rules-section .card-title { flex: 1 1 auto; min-width: 0; transition: color .15s ease; }
.rules-caret {
  flex: none; font-style: normal; align-self: center;
  font-family: var(--mono); font-size: 15px; line-height: 1;
  color: var(--accent-dim); transition: transform .15s ease, color .15s ease;
}
.rules-section[open] .rules-caret { transform: rotate(90deg); color: var(--accent); }
/* Номер раздела -- не украшение: по нему на правило и ссылаются. */
.rules-num {
  flex: none; min-width: 26px; height: 26px; display: grid; place-items: center;
  border: 1px solid var(--line-hot); border-radius: 6px;
  color: var(--accent); font-size: 12px; background: var(--bg-deep);
}
/* Размер шрифта вступления и заголовков разделов задаёт админ (2026-08-20): у
   одного сервера правила -- три строки, у другого простыня на сорок разделов.
   Классы -- четыре штуки, «обычный» отдельного класса не имеет: это обычный
   размер карточки, и лишний класс на него ничего бы не менял. */
.rules-size-s { font-size: 12px; }
.rules-size-l { font-size: 17px; }
.rules-size-xl { font-size: 21px; }
/* У заголовка раздела крупные размеры просят и межстрочный поменьше: строка в
   21 точку с обычным интервалом висит слишком далеко от номера раздела. */
.card-title.rules-size-l, .card-title.rules-size-xl { line-height: 1.25; }

.rules-editor { display: grid; gap: 12px; margin: 10px 0; }
/* Размер живёт в панели над полем -- и у вступления (строкой под ним), и у
   заголовков (в самой панели). */
.rules-size-pick { flex: 0 0 auto; width: 150px; }
.rules-size-row { display: flex; gap: 8px; align-items: center; margin-top: 6px; }
.rules-edit-row {
  display: grid; gap: 6px; padding: 11px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--bg-deep);
}
.rules-edit-row textarea { resize: vertical; font-family: inherit; }

/* ------------------------------------------------------------ новости -- */
/* Новость -- обычная карточка в столбец, а не плитка: её читают, а не сравнивают
   с соседней. Ширину держим читаемой: строка во весь широкий экран теряется
   глазом на обратном ходе. */
.news-item { max-width: 760px; margin-bottom: 12px; }
.news-item.news-draft { border-style: dashed; opacity: .85; }
.news-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.news-title { margin: 0; font-size: 16px; }
/* Размер заголовка новости выбирает админ. Правила размеров живут выше, а у
   .news-title свой размер -- при равной весомости побеждает то, что ниже,
   поэтому переопределяем здесь и от базовых 16 точек, а не от 14 в правилах. */
.news-title.rules-size-s { font-size: 13px; }
.news-title.rules-size-l { font-size: 20px; }
.news-title.rules-size-xl { font-size: 25px; }
.news-when { font-size: 11px; margin: 3px 0 9px; }
/* Картинка новости -- шапкой карточки, во всю её ширину и без обрезки по бокам:
   на объявлениях это чаще скриншот, чем фон. */
.news-shot {
  margin: -14px -14px 12px; border-bottom: 1px solid var(--line);
  background: var(--bg-deep); overflow: hidden;
}
.news-shot img { display: block; width: 100%; max-height: 300px; object-fit: cover; }
.news-item .card-actions { margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line); }
/* Черновик и другие «внимание»-метки: тот же значок, но янтарный. */
.badge-warn { background: #f0b4291f; color: var(--warn, #f0b429); border-color: #f0b42955; }

/* ------------------------------------------------------------ галерея -- */
/* Склад картинок для витрины. Плитки одного размера: их сравнивают глазами, и
   разнокалиберная сетка для этого бесполезна. */
.gal-drop {
  border: 1px dashed var(--line); border-radius: 12px; padding: 12px;
  transition: border-color .15s, background .15s;
}
/* Перетаскивание -- главный способ загрузки, и рамка обязана отзываться: без
   этого непонятно, попал ли файл в панель или в браузер. */
.gal-drop.over { border-color: var(--accent); background: #46c8f50f; }
.gal-grid {
  display: grid; gap: 12px; margin-top: 12px;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
}
.gal-item {
  display: flex; flex-direction: column; gap: 8px; padding: 9px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--raised);
}
.gal-item.current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.gal-shot {
  display: block; width: 100%; aspect-ratio: 1 / 1; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  background: var(--bg-deep);
}
.gal-shot:hover { border-color: var(--accent); }
.gal-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gal-cap { display: grid; gap: 2px; }
/* Подпись в одну строку с усечением: имена файлов бывают длиной в абзац, и от
   них сетка разъезжалась бы. */
.gal-title {
  font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gal-meta { font-size: 10px; }
.gal-acts { display: flex; gap: 6px; margin-top: auto; }
.gal-acts .btn { flex: 1 1 auto; }
/* В окне выбора плитки мельче: там их видно сразу десяток, и подписи с кнопками
   у них нет. */
.gal-pick { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
/* Кнопка «Из галереи» под полем ссылки: она и есть обычный путь заполнения. */
.img-pick { margin-top: 7px; }

/* Цена до скидки подписчика: тише и мельче самой цены -- она справочная. */
.price-full { color: var(--muted); font-size: 11px; margin-right: 5px; opacity: .8; }

/* ------------------------------------------------------------ аукцион -- */

/* Кнопка «убрать закрытые» стоит НАД сеткой лотов и прижата вправо: это уборка,
   а не действие с конкретным лотом. */
.auction-sweep { justify-content: flex-end; margin-bottom: 10px; }
.auction-lot { display: flex; flex-direction: column; }
.auction-lot .card-actions { margin-top: auto; padding-top: 11px; border-top: 1px solid var(--line); }

/* Шапка лота: вид крупно, кличка и рост подписью, состояние торгов -- меткой
   справа. Раньше состояние было обычной серой меткой вида и терялось среди
   остальных, а кличка с ростом шли одной строкой через точку. */
/* Вид и состояние -- первой строкой, кличка, рост и пол -- второй, во всю
   ширину. Сетка, а не флекс: во флексе строка клички жила в левой колонке и в
   узкой карточке переносилась по словам. */
.lot-head {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 5px 8px; margin-bottom: 0;
}
.lot-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lot-sub {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 5px;
  min-width: 0; font-size: 12px; color: var(--muted);
}
.lot-chip {
  flex: none; font-size: 11px; line-height: 1.5; padding: 0 7px;
  border: 1px solid var(--line); border-radius: 999px; color: var(--text);
  background: #ffffff08;
}
.lot-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text); opacity: .85;
}
.lot-growth { color: var(--text); opacity: .75; font-size: 12px; }
.lot-status {
  flex: none; font-size: 11px; line-height: 1.6; padding: 1px 9px;
  border-radius: 999px; border: 1px solid var(--line); color: var(--muted);
  white-space: nowrap;
}
.lot-status.live { color: var(--accent); background: #34d39914; border-color: #34d39933; }
.lot-status.leading { color: var(--amber); background: #f0b42914; border-color: #f0b42933; }
.lot-status.won { color: var(--accent); background: #34d39922; border-color: #34d39955; }
.lot-status.sold { color: var(--muted); background: #ffffff08; }
.lot-status.off { color: var(--muted); border-style: dashed; }

/* Чей лот и приписка продавца -- одной мелкой строкой: спорить по весу с ценой
   им незачем. Приписка обрезается, целиком она в подсказке. */
.lot-who {
  display: flex; align-items: baseline; gap: 8px; margin-top: 8px; font-size: 11.5px;
}
.lot-note {
  min-width: 0; color: var(--text); opacity: .8;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Деньги и время -- плашкой в две колонки: цена крупно и чья ставка под ней,
   справа столбиком таймер и блиц. */
.lot-money {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
  gap: 8px; margin-top: 8px; padding: 7px 9px;
  border: 1px solid var(--line); border-radius: 10px; background: #00000033;
}
.lot-price-col { display: grid; gap: 1px; min-width: 0; }
.lot-price-col .lot-bid-note {
  font-size: 11px; overflow: hidden; text-overflow: ellipsis;
}
.lot-side { display: grid; gap: 4px; justify-items: end; }
.auction-price { display: flex; align-items: center; gap: 7px; font-size: 17px; }
.auction-price .muted { font-size: 12px; }
.lot-bid { font-size: 21px; line-height: 1.1; }
.lot-bid-note { white-space: nowrap; }
.lot-buyout, .lot-clock {
  font-size: 12px; color: var(--muted); white-space: nowrap;
  border: 1px solid var(--line); border-radius: 999px; padding: 1px 9px;
}
.lot-buyout b { color: var(--text); }
.lot-clock { margin-left: 0; }

/* Почему на своём лоте нет кнопки ставки. Строка мелкая и приглушённая: это
   объяснение, а не действие, и спорить с ценой над ней ей незачем. */
.lot-own-note { margin-top: 6px; font-size: 11.5px; line-height: 1.4; }

/* Характеристики дино на карточке лота. Покупатель выбирает по ним, поэтому они
   стоят сразу под названием вида, а не за кнопкой «Подробно»: там остаётся то,
   что нужно реже -- витальные, hex каждой зоны, SkinCode. */
.lot-specs { margin-top: 7px; display: flex; flex-direction: column; gap: 6px; }
.lot-specs:empty { display: none; }
.lot-tags { display: flex; gap: 5px; flex-wrap: wrap; align-items: center; }
.lot-when { color: var(--muted); }
.lot-colors { display: flex; gap: 4px; flex-wrap: wrap; }
/* Квадратики окраса тут мельче, чем в паспорте: их до десяти, а карточка узкая. */
.lot-colors .swatch { width: 15px; height: 15px; border-radius: 5px; }

/* Окрас одной полосой: десять отдельных квадратиков читались как сыпь, а
   вопрос у покупателя один -- «какой он с виду». Зоны и hex -- в подсказке. */
.lot-skin {
  display: flex; height: 12px; border-radius: 6px; overflow: hidden;
  border: 1px solid var(--line);
}
.lot-skin-seg { flex: 1 1 0; min-width: 0; }

.lot-muts-head { display: flex; align-items: baseline; gap: 7px; }
.lot-muts-head .sub-head { margin: 0; }
.lot-muts { display: flex; gap: 4px; flex-wrap: wrap; }
.lot-nomut { font-size: 12px; }
.mut-chip {
  font-size: 11px; line-height: 1.5; max-width: 100%;
  background: #05090c; border: 1px solid var(--line);
  border-left: 2px solid var(--accent-dim);
  border-radius: 6px; padding: 1px 7px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Цвет чипа = откуда мутация. Одинаковые чипы не давали отличить редкое от
   рядового, а именно за редким на торги и приходят. */
.mut-chip.elder { border-left-color: var(--amber); }
.mut-chip.mut-secret { border-left-color: #c084fc; color: #e9d5ff; }
.mut-chip.mut-carnivore { border-left-color: #ff7a59; }
.mut-chip.mut-herbivore { border-left-color: #6ee7a8; }
.mut-chip.mut-common { border-left-color: var(--line-hot); }
/* «+N ещё» -- кнопка, но выглядит как чип: это продолжение того же списка. */
.mut-more {
  cursor: pointer; color: var(--muted); border-left-color: var(--muted);
  font-family: var(--mono);
}
.mut-more:hover { color: var(--text); border-color: var(--line-hot); }
/* Перерождение открывает элдерские слоты -- метка того же цвета, что и они. */
.rebirth-tag {
  color: var(--amber); background: #f5b64614; border-color: #f5b64633;
}
/* Метка «с аукциона»: это запрет, а не украшение, поэтому красная, как отказ. */
.lock-tag {
  color: var(--danger); background: #ff5b5b14; border-color: #ff5b5b3d;
}

/* ----------------------------------------------------- боевой пропуск -- */

/* Дорожка уровней -- две ленты наград (бесплатная сверху, платная снизу) и
   шкала уровней между ними. Высоты строк заданы здесь, а не содержимым: столбец
   подписей слева -- отдельная сетка, и совпасть с лентой построчно она может,
   только если обе знают одни и те же высоты. */
:root { --bp-tile: 132px; --bp-knot: 58px; --bp-col: 148px; }

/* Плашка «сезон открыт только админам» -- янтарная, как пометка «в разработке». */
.bp-test-note {
  margin-bottom: 12px; padding: 9px 14px; border-radius: 10px; font-size: 13px;
  color: #f5b946; background: #2c241266; border: 1px dashed #f5b94699;
}

/* ---- шапка сезона */
.bp-hero {
  display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 16px;
  background:
    radial-gradient(820px 260px at 4% -40%, #a78bfa1a, transparent 62%),
    radial-gradient(680px 220px at 100% 130%, #35e08a12, transparent 60%),
    var(--raised);
  border-color: var(--line-hot);
}
.bp-hero-main { display: flex; gap: 18px; align-items: center; flex: 1 1 340px; min-width: 0; }
/* Уровень -- главное число вкладки. Кольцо вокруг него показывает долю до
   следующего уровня: полосу ниже на телефоне видно только после прокрутки, а
   кружок стоит первым. Заливка -- conic-gradient по переменной --bp-pct. */
.bp-level-badge {
  flex: none; width: 88px; height: 88px; border-radius: 50%;
  display: grid; place-content: center; padding: 4px;
  background: conic-gradient(var(--accent) var(--bp-pct, 0%), #1d2b3399 0);
  box-shadow: 0 0 22px #35e08a2e;
}
.bp-level-inner {
  width: 100%; height: 100%; border-radius: 50%;
  display: grid; place-content: center; justify-items: center;
  background: radial-gradient(circle at 50% 30%, #35e08a14, var(--bg-deep) 72%);
  border: 1px solid var(--line);
}
.bp-level-badge b { font-size: 26px; line-height: 1; color: var(--accent); font-family: var(--mono); }
.bp-level-badge span {
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}
.bp-hero-text { min-width: 0; flex: 1 1 auto; }
.bp-hero-text .card-title { font-size: 19px; margin-bottom: 6px; }
.bp-hero-meta { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 10px; }
/* Метки сезона: даты и темп начисления. Пилюлями, а не строкой через точку --
   иначе «до 30.09» терялось среди прочего. */
.bp-chip {
  font-size: 11px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px;
  background: #05090c;
}
.bp-chip-end { color: var(--amber); border-color: #f0b42933; }

/* Три числа под полосой: уровень, собранные награды и сколько ждёт получения. */
.bp-sums { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.bp-sum {
  display: flex; align-items: baseline; gap: 6px;
  padding: 5px 11px; border-radius: 8px;
  border: 1px solid var(--line); background: #05090c;
}
.bp-sum b { font-size: 16px; }
.bp-sum span { font-size: 11px; color: var(--muted); }
/* «Можно забрать» -- единственное, ради чего сюда заходят, когда оно не ноль. */
.bp-sum-hot { border-color: var(--accent); background: #35e08a12; }
.bp-sum-hot b { color: var(--accent); }

/* Правая колонка шапки -- всё про платную ветку: открыта или как открыть. */
.bp-hero-side {
  flex: 0 1 268px; display: grid; gap: 8px; align-content: center;
  padding: 12px 14px; border-radius: 12px;
  border: 1px solid #a78bfa2b; background: #a78bfa0a;
}
.bp-hero-side.on { border-color: #a78bfa66; background: #a78bfa14; }
.bp-side-cap {
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--violet);
}
.bp-premium-on {
  display: inline-flex; align-items: center; gap: 7px; justify-self: start;
  font-size: 13px; color: var(--violet);
  background: #a78bfa1f; border: 1px solid #a78bfa55;
  border-radius: 999px; padding: 4px 14px;
}
/* Список подписок из этой карточки убран (2026-08-20): название и срок лежат в
   самой витрине, где рядом цена и остальное содержимое подписки. Здесь осталось
   «чем открывается» и кнопка на нужную полку магазина. */
.bp-warn { color: var(--amber); }

/* ---- дорожка */
.bp-track-card { padding-bottom: 10px; }
.bp-track-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
}
.bp-nav-btn { width: 32px; padding: 0; font-size: 16px; line-height: 1; }
.bp-track { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; }
/* Подписи лент прилипают слева: с полусотней уровней лента уезжает далеко, и
   без подписи неясно, какая из двух строк платная. */
.bp-lanes {
  display: grid; grid-template-rows: var(--bp-tile) var(--bp-knot) var(--bp-tile);
  gap: 8px; align-items: center; width: 74px;
}
.bp-lane-name {
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted); line-height: 1.3;
}
.bp-lane-name.paid { color: var(--violet); opacity: .6; }
/* Ветка открыта -- подпись горит в полную силу: по ней и видно, свои эти
   награды или чужие. */
.bp-lane-name.paid.on { opacity: 1; }

.bp-scroll {
  overflow-x: auto; scroll-behavior: smooth; padding-bottom: 8px;
  scrollbar-width: thin; scrollbar-color: var(--line-hot) transparent;
}
.bp-scroll::-webkit-scrollbar { height: 8px; }
.bp-scroll::-webkit-scrollbar-thumb { background: var(--line-hot); border-radius: 4px; }
/* Сама лента -- сетка в три строки, столбцы наращиваются уровнями. width:max-content
   нужен, чтобы рельс под узлами тянулся на всю ленту, а не на видимую её часть. */
.bp-grid {
  position: relative; width: max-content;
  display: grid; grid-auto-flow: column; grid-auto-columns: var(--bp-col);
  grid-template-rows: var(--bp-tile) var(--bp-knot) var(--bp-tile);
  gap: 8px;
}
/* Рельс и его заполненная часть -- ровно по центру строки узлов. */
.bp-rail, .bp-rail-fill {
  position: absolute; left: 0; height: 3px; border-radius: 3px; pointer-events: none;
  top: calc(var(--bp-tile) + 8px + var(--bp-knot) / 2); transform: translateY(-50%);
}
.bp-rail { right: 0; background: var(--line); }
.bp-rail-fill {
  background: linear-gradient(90deg, var(--accent-dim), var(--accent));
  box-shadow: 0 0 10px #35e08a66;
}

/* Узел уровня. */
.bp-knot {
  position: relative; z-index: 1; justify-self: center; align-self: center;
  width: var(--bp-knot); height: var(--bp-knot); border-radius: 50%;
  display: grid; place-content: center; justify-items: center;
  border: 2px solid var(--line-hot); background: var(--bg-deep); color: var(--muted);
}
.bp-knot.on {
  border-color: var(--accent); color: var(--accent);
  box-shadow: 0 0 12px #35e08a55, inset 0 0 10px #35e08a18;
}
.bp-knot.next {
  border-color: var(--cyan); color: var(--cyan); box-shadow: 0 0 14px #46c8f566;
}
.bp-knot-num { font-size: 15px; line-height: 1.1; }
.bp-knot-xp { font-size: 9px; opacity: .75; line-height: 1; }

/* Плитка награды. ВСЕ ПЛИТКИ ОДНОГО РАЗМЕРА (2026-08-17): рамка занимает строку
   сетки целиком, поэтому «получено», «Забрать» и пустая клетка выглядят
   одинаково. Раньше высота считалась по содержимому (`align-self: end/start`), и
   плитка с кнопкой была заметно выше плитки с одной подписью -- ряд получался
   рваным. Прижимается теперь не рамка, а её содержимое. */
.bp-tile {
  position: relative; overflow: hidden;
  height: 100%; align-self: stretch;
  display: grid; gap: 5px; justify-items: center; align-content: start;
  padding: 10px 8px; text-align: center;
  border: 1px solid var(--line); border-radius: 12px; background: var(--raised);
}
/* Верхняя (бесплатная) лента держится за шкалу низом, нижняя -- верхом: так обе
   ветки одного уровня зрительно принадлежат своему узлу. */
.bp-tile.free { align-content: end; }
.bp-tile.paid { align-content: start; border-color: #a78bfa33; background: #a78bfa0a; }
.bp-tile.on { border-color: var(--accent-dim); box-shadow: inset 0 0 22px #35e08a0f; }
.bp-tile.paid.on { border-color: var(--violet); box-shadow: inset 0 0 22px #a78bfa14; }
/* Готовая к получению плитка светится: в ленте из сотни уровней это
   единственное, ради чего сюда и заходят. */
.bp-tile.ready {
  border-color: var(--accent); box-shadow: 0 0 0 1px #35e08a33, 0 0 18px #35e08a26;
}
.bp-tile.taken { opacity: .55; }
/* Столбец текущего уровня -- обе его плитки: «где я» глаз ищет первым, и одного
   кружка на шкале для этого мало. */
.bp-tile.bp-now { border-color: var(--cyan); }
.bp-tile.bp-now::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 2px;
  background: var(--cyan); box-shadow: 0 0 10px var(--cyan);
}
.bp-tile.paid.bp-now::after { inset: 0 0 auto 0; }
.bp-tile-type {
  font-size: 9px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
}
.bp-tile.empty {
  border-style: dashed; background: none; align-content: center; color: var(--muted);
}
.bp-tile-img { width: 54px; height: 54px; object-fit: cover; border-radius: 12px; }
.bp-tile-icon { font-size: 28px; line-height: 1.2; color: var(--accent-dim); }
.bp-tile.paid .bp-tile-icon { color: var(--violet); }
.bp-tile-name {
  font-size: 12px; line-height: 1.25; max-width: 100%;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.bp-tile-note { font-size: 10px; }
.bp-taken { color: var(--accent); }
.bp-tile .btn { margin-top: 2px; padding: 4px 10px; font-size: 12px; }
/* Замок на платной плитке: ветку не открывали, и это должно быть видно раньше,
   чем игрок ткнёт в неактивную кнопку. */
.bp-lock {
  position: absolute; inset: 0; display: grid; place-content: center; gap: 3px;
  justify-items: center; background: #04070ac2; color: var(--violet); font-size: 20px;
}
.bp-lock span { font-size: 9px; letter-spacing: .12em; text-transform: uppercase; }

/* ---- редактор уровней (админ) */
/* Генератор лестницы: способ роста и его числа. Поля разные у разных способов,
   поэтому лишние прячутся, а не тускнеют -- иначе не понять, что именно считает. */
.bp-gen {
  display: grid; gap: 9px; padding: 11px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--bg-deep);
  margin-bottom: 10px;
}
.bp-gen-row { display: flex; gap: 9px; flex-wrap: wrap; align-items: flex-end; }
.bp-gen-field { display: grid; gap: 3px; font-size: 12px; color: var(--muted); }
.bp-gen-field input, .bp-gen-field select { width: 118px; }
.bp-gen-preview {
  font-family: var(--mono); font-size: 11px; color: var(--cyan);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bp-editor { display: grid; gap: 6px; margin: 8px 0; max-height: 46vh; overflow-y: auto; }
.bp-edit-row {
  display: grid; gap: 7px; align-items: center;
  grid-template-columns: 24px 92px minmax(0, 1fr) minmax(0, 1fr) 36px;
}
.bp-edit-head { padding: 0 2px; font-size: 11px; position: sticky; top: 0; }
.bp-edit-num { text-align: right; }

@media (max-width: 720px) {
  /* На телефоне столбец уже: три уровня в поле зрения важнее, чем длинное
     название награды целиком. */
  :root { --bp-col: 122px; --bp-tile: 118px; }
  .bp-hero-side { padding-left: 0; border-left: none; }
  .bp-lanes { width: 60px; }
  .bp-edit-row { grid-template-columns: 24px 78px minmax(0, 1fr); }
  .bp-edit-row > *:nth-child(4) { grid-column: 2 / -1; }
}

/* ---------------------------------------------------------- рефералы -- */

/* ВКЛАДКА -- НЕ СТОЛБИК ОДИНАКОВЫХ КАРТОЧЕК (2026-08-20). Было четыре карточки
   во всю ширину одна под другой: герой, правила, «кто вас пригласил», «кого вы
   привели». На широком экране это лента в два экрана высотой при трёх строках
   содержимого, и глазу не за что зацепиться -- всё одного веса.

   Стало: герой с кодом (в нём же числа полоской, без своих рамок), под ним две
   колонки -- правила и «кто вас пригласил», и внизу приведённые друзья сеткой.
   Текст героя ограничен по ширине: строка в сто с лишним символов не читается,
   а тянулась она на весь экран. */

.ref-link { display: flex; gap: 8px; align-items: center; margin: 14px 0 0; }
.ref-link input { flex: 1 1 auto; min-width: 0; }
.ref-link .btn { flex: none; }

/* Шапка вкладки. Раньше здесь были три одинаковые серые карточки, и главное --
   код, который диктуют голосом, -- лежало мелкой строкой в подвале первой.
   Теперь код и есть шапка: билетик справа, обещание слева, числа под ними. */
.ref-hero {
  position: relative; overflow: hidden;
  background:
    radial-gradient(900px 240px at 8% -30%, #35e08a1a, transparent 65%),
    radial-gradient(700px 220px at 96% 120%, #46c8f514, transparent 60%),
    var(--raised);
  border-color: var(--line-hot);
}
/* Билет прижат к правому краю, а текст ограничен по ширине: строка в сто с
   лишним символов не читается, а тянулась она на весь экран. Пустота между ними
   -- это поле героя, а не дыра: он и должен дышать. Числа из подвала героя
   уехали в свою колонку -- в герое остались обещание, код и ссылка. */
.ref-hero-head {
  display: flex; gap: 20px; flex-wrap: wrap;
  align-items: flex-start; justify-content: space-between;
}
.ref-hero-text { flex: 1 1 320px; min-width: 0; max-width: 62ch; }
.ref-kicker {
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--accent);
}
.ref-title { margin: 4px 0 8px; font-size: 22px; line-height: 1.2; }

/* Билетик с кодом: пунктирная рамка и разрядка -- он выглядит как то, что
   отрывают и отдают, а не как ещё одно поле формы. */
.ref-ticket {
  flex: 0 1 232px; display: grid; gap: 6px; justify-items: center;
  padding: 12px 16px; border-radius: 12px;
  border: 1px dashed var(--line-hot); background: var(--bg-deep);
  box-shadow: 0 0 24px #35e08a10 inset;
}
.ref-ticket-cap {
  font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--muted);
}
/* Линия отрыва над кнопкой -- та самая, по которой билет и отрывают. Тянется до
   краёв билета за отступы, поэтому и читается как перфорация, а не как рамка
   кнопки. */
.ref-ticket-btn { width: 100%; position: relative; margin-top: 3px; }
.ref-ticket-btn::before {
  content: ""; position: absolute; left: -16px; right: -16px; top: -8px;
  border-top: 1px dashed var(--line-hot); opacity: .7;
}
/* Код диктуют голосом -- он должен читаться крупно и вразрядку. */
.ref-code {
  font-size: 24px; letter-spacing: .18em; line-height: 1.2; color: var(--accent);
  text-shadow: 0 0 18px #35e08a55;
  word-break: break-all; text-align: center;
}

/* Числа -- четвёртая колонка строки (2026-08-21), столбиком внутри своей
   карточки. Раньше они лежали полоской в подвале героя: герой от этого был
   высоким, а колонок под ним выходило три из четырёх, и правая четверть экрана
   пустовала. Строки разделены волосяной линией, своих рамок у клеток нет --
   три коробочки внутри карточки читались бы как ещё три карточки. */
.ref-stats { display: grid; gap: 11px; }
.ref-stat { min-width: 0; }
.ref-stat + .ref-stat { border-top: 1px solid var(--line); padding-top: 11px; }
.ref-stat-value {
  display: flex; align-items: center; gap: 6px;
  font-size: 20px; line-height: 1.2; font-family: var(--mono); color: var(--text);
}
.ref-coin { display: inline-flex; align-items: center; gap: 6px; }
.ref-stat-cap {
  margin-top: 2px; font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}

/* Четыре равные колонки под героем -- как группы в справочнике мутаций: шаги,
   «кто вас пригласил», числа и приведённые друзья. Раньше друзья лежали
   отдельной карточкой во всю ширину под двумя колонками, а числа -- полоской в
   подвале героя, и вкладка с тремя строками содержимого тянулась на два экрана
   при пустых полях по бокам.

   Карточки одного размера: ширина -- по четверти (`minmax(0, 1fr)`, а не
   `auto`: иначе длинное имя друга раздувало бы свою колонку за счёт соседних),
   высота -- по самой длинной в строке (`align-items: stretch`). Разнобой в
   высоте читался как разная важность карточек, хотя это просто разное
   количество текста в них; в справочнике мутаций панели тоже стоят вровень. */
.ref-grid {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 15px; align-items: stretch; margin-top: 15px;
}

/* ПАНЕЛИ ТОЙ ЖЕ ПОРОДЫ, ЧТО ГРУППЫ В СПРАВОЧНИКЕ МУТАЦИЙ (2026-08-21): цветная
   риска по верху, название вразрядку с горящей чёрточкой, счётчик пилюлей в том
   же цвете. Четыре одинаково серые карточки различались только текстом, и нужную
   приходилось искать чтением заголовков; цвет узнаётся боковым зрением. Цвета
   разные не для красоты: они разводят соседние колонки, а не метят их смыслом --
   у справочника цвет значит группу рациона, здесь значить ему нечего. */
.ref-grid > .card {
  --g: var(--accent); --g-soft: #35e08a12; --g-line: #35e08a33;
  overflow: hidden;
  background: linear-gradient(180deg, #0d1419f7 0%, #04070af7 100%);
}
/* Селектор той же длины, что у общего правила выше: `.ref-steps` в один класс
   проигрывал бы `.ref-grid > .card` по весу, и все четыре колонки остались бы
   зелёными. */
.ref-grid > .ref-steps   { --g: var(--cyan);   --g-soft: #46c8f512; --g-line: #46c8f533; }
.ref-grid > .ref-who     { --g: var(--violet); --g-soft: #a78bfa12; --g-line: #a78bfa33; }
.ref-grid > .ref-numbers { --g: var(--accent); --g-soft: #35e08a12; --g-line: #35e08a33; }
.ref-grid > .ref-mates   { --g: var(--amber);  --g-soft: #f0b42912; --g-line: #f0b42933; }
/* Волосяная линия .card заменена той же риской, что у группы мутаций: от края
   до края и в цвет колонки. */
.ref-grid > .card::before {
  top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, var(--g), transparent 78%);
}
/* Название колонки -- как имя группы: вразрядку, заглавными, с чёрточкой в
   цвете. Не переносится: в четверти экрана «КТО ВАС ПРИГЛАСИЛ» ломалось бы
   пополам, а панели в ряду перестали бы совпадать по высоте заголовка. */
.ref-grid > .card .card-title {
  font-size: 12px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ref-grid > .card .card-title::before {
  content: ""; display: inline-block; width: 3px; height: 12px;
  background: var(--g); box-shadow: 0 0 8px var(--g);
  margin-right: 9px; vertical-align: -2px;
}
/* Отбивка под названием -- одна на все четыре колонки. У «Кого вы привели»
   заголовок стоит в `.card-head` со счётчиком, и отступ держит он. */
.ref-grid > .card > .card-title { margin-bottom: 10px; }

/* Три шага: правила программы раньше приходилось вычитывать из мелкого текста
   под ссылкой. Номер рисуется счётчиком -- нумерация не разъедется при правке. */
.ref-step-list { list-style: none; counter-reset: step; margin: 0; padding: 0; display: grid; gap: 10px; }
.ref-step {
  counter-increment: step;
  display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 4px 12px;
  align-items: baseline;
}
.ref-step::before {
  content: counter(step); grid-row: span 2;
  display: grid; place-content: center;
  width: 30px; height: 30px; border-radius: 50%;
  font-family: var(--mono); font-size: 13px; color: var(--g, var(--accent));
  border: 1px solid var(--g-line, var(--accent-dim));
  background: var(--g-soft, #35e08a0f);
}
.ref-step b { font-size: 13px; }

/* Приведённые друзья -- карточками в сетку, а не столбиком: десять человек
   столбиком это десять экранных строк ради двух чисел в каждой. */
.ref-mates { min-width: 0; }
/* В своей колонке места на одну карточку в ряд, но на широком экране, когда
   колонки развернулись обратно в одну, друзья снова становятся сеткой -- отсюда
   auto-fill, а не фиксированное число. */
.ref-friends {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 8px; margin-top: 10px;
}
.ref-friend {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 11px; border: 1px solid var(--line); border-radius: 12px;
  background: #05090c;
}
.ref-avatar {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-content: center;
  font-size: 15px; font-weight: 600; color: var(--bg-deep);
  background: linear-gradient(140deg, var(--accent), var(--cyan));
}
.ref-friend-text { display: grid; gap: 1px; min-width: 0; flex: 1 1 auto; }
.ref-friend-text b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ref-friend-earned { display: flex; align-items: center; gap: 5px; flex: none; }
.ref-count {
  margin-left: auto; font-family: var(--mono); font-size: 12px;
  padding: 1px 9px; border-radius: 999px;
  color: var(--g, var(--text)); background: var(--g-soft, var(--raised-2));
  border: 1px solid var(--g-line, var(--line-hot));
}
/* Кто пригласил -- одна строка, и в своей колонке она стоит одна: сетку под неё
   не разводим. */
.ref-who .ref-friend { margin-top: 8px; }

/* Четыре колонки требуют ширины: ниже 1180 точек это уже не колонки, а
   колодцы по 200 точек с переносом каждого слова. Складываем по две, потом в
   одну. */
@media (max-width: 1180px) {
  .ref-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  .ref-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 620px) {
  .ref-title { font-size: 18px; }
  .ref-ticket { flex: 1 1 100%; }
}

/* --------------------------------------------------------------- клан -- */
/* Вкладка «Клан»: карточки одна под другой, внутри -- строки «кто · когда ·
   кнопки». Строка гнётся на узком экране: имя остаётся слева, кнопки уезжают
   на вторую строку, а не выдавливают имя за край. */
.clan-card { display: grid; gap: 10px; margin-bottom: 12px; }
.clan-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.clan-name { margin: 0; font-size: 17px; }
.clan-sub { margin: 0; font-size: 15px; }
/* Кнопки строки -- всегда справа: они одинаковые у всех строк, и выровненный
   край даёт по ним попадать не глядя. */
.clan-acts { display: flex; gap: 6px; margin-left: auto; }
.clan-about { margin: 4px 0 0; font-size: 12px; line-height: 1.5; color: var(--muted);
  white-space: pre-wrap; }
/* Эмблема клана -- тот же квадрат, что лицо игрока, но внутри тег: он короткий
   и читается как знак. Длинный тег ужимается, чтобы не распирать квадрат. */
.clan-face { font-family: var(--mono); }
.clan-emblem {
  font-size: 15px; letter-spacing: .04em; color: var(--accent);
  max-width: 44px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Кнопка состава: список под ней спрятан (просьба пользователя 2026-09-21) --
   в него смотрят редко, а место он занимал всегда. */
/* Карточка клана -- сетка (.clan-card), поэтому кнопка растянулась бы во всю
   ширину: прижимаем её влево и даём свою ширину. */
.clan-tools { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
/* Открытая створка -- подсвеченной плиткой: иначе непонятно, чьё это
   раскрытие висит ниже. */
.stat-tile.stat-open { border-color: var(--accent-dim); }
.stat-tile.stat-open .stat-value { color: var(--accent); }
.clan-board { margin-top: 8px; display: grid; gap: 8px; }
.clan-list { display: grid; gap: 4px; margin-top: 8px; }
.clan-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 5px 8px; background: #ffffff08; border-radius: 8px; }
.clan-who { font-weight: 600; font-size: 14px; }
.clan-note { font-size: 12px; }
/* Время (и счётчик состава) отжимает всё, что после него, вправо: сначала имя и
   пометки, потом пустота, потом кнопки. */
.clan-when { margin-left: auto; font-size: 12px; }
.clan-dir { display: grid; gap: 4px; padding: 7px 9px; background: #ffffff08;
  border-radius: 9px; }
.clan-post { display: grid; gap: 5px; padding: 8px 10px; background: #ffffff08;
  border-left: 2px solid var(--line); border-radius: 0 9px 9px 0; }
.clan-post-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.clan-post-title { margin: 0; font-size: 14px; }
/* Текст анонса пишет игрок: переносы строк -- его, и делает их CSS, а не
   разметка (в разметку чужой текст здесь не попадает вовсе). */
.clan-post-body { margin: 0; font-size: 13px; line-height: 1.55; white-space: pre-wrap; }
/* Строка-мостик на соседнюю вкладку («2 заявки ждут ответа» -> «К заявкам»):
   разнесённые вкладки иначе не подсказывают, что на соседней что-то есть. */
.clan-hint { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 6px 9px; background: #46c8f512; border: 1px solid #46c8f533;
  border-radius: 9px; font-size: 13px; }

/* Карточка «Свой клан» -- узкий столбец по центру (просьба пользователя
   2026-09-21: «по минимуму, но красиво», затем «убери это» про шапку). В
   карточке остались три поля и кнопка: заголовка, строки про лидерство и
   квадрата эмблемы нет, о чём окно -- говорит вкладка. Подписи полей живут в
   самих полях, счётчик появляется, только когда место кончается. */
.clan-new {
  max-width: 460px; margin: 0 auto; justify-items: center;
  gap: 8px; padding: 18px; position: relative; overflow: hidden;
}
.clan-new::before {
  content: ""; position: absolute; inset: -150px 0 auto 0; height: 220px;
  background: radial-gradient(closest-side, #35e08a14, transparent 70%);
  pointer-events: none;
}
.clan-new > * { position: relative; }
.clan-new-form { width: 100%; display: grid; gap: 8px; }
/* Счётчик лежит поверх правого края поля: своей строки он не заслуживает, а
   появляется редко. */
.clan-field { position: relative; display: grid; }
.clan-field input, .clan-field textarea { width: 100%; text-align: center; }
.clan-field input { padding: 9px 12px; }
.clan-field textarea { text-align: left; resize: vertical; }
.clan-count {
  position: absolute; right: 8px; bottom: 6px;
  font-size: 10px; color: var(--muted); pointer-events: none;
}
.clan-field input ~ .clan-count { top: 50%; bottom: auto; transform: translateY(-50%); }
.clan-count-full { color: var(--amber); }
.clan-new-go { margin-top: 4px; min-width: 190px; padding: 9px 22px; font-size: 14px; }

/* -------------------------------------------------------------- карта -- */

.map-board { margin-top: 4px; }
/* Кнопки зон и карта -- две колонки. Колонка кнопок узкая: длинное имя зоны
   перенесётся на вторую строку, зато карта отодвинута от текста. */
.map-layout { display: flex; gap: 12px; align-items: flex-start; }
.map-layout .map-board { flex: 1 1 auto; min-width: 0; }
.map-zones { flex: 0 0 156px; display: flex; flex-direction: column; gap: 6px; }
.map-zones-head { font-weight: 600; }
.map-zones-hint { font-size: 12px; line-height: 1.35; }
/* Название зоны бывает длинным («West Rail Access») -- кнопка выравнивается по
   левому краю и переносит текст, а не растягивает колонку. */
.map-zone-btn { width: 100%; text-align: left; white-space: normal; }
.map-zone-btn.tp-ready { border-color: var(--line-hot); }
@media (max-width: 860px) {
  .map-layout { flex-direction: column; }
  .map-zones { flex: 1 1 auto; width: 100%; }
  /* В колонку карта не тянется сама: площадка внутри задана долей ширины, а
     доля от «по содержимому» -- это ноль, и карта схлопывалась в точку. */
  .map-layout .map-board { width: 100%; }
  /* В одну колонку кнопки зон лучше стоят рядами, а не столбцом в экран длиной. */
  .map-zones { flex-direction: row; flex-wrap: wrap; }
  .map-zones-head, .map-zones-hint { flex: 1 1 100%; }
  .map-zone-btn { width: auto; }
}
/* Квадратная площадка: точки считаются в процентах от её краёв, и растягивать её
   по ширине окна нельзя -- карта поехала бы вместе с ними. */
.map-plate {
  position: relative; width: 100%; max-width: 860px; aspect-ratio: 1 / 1;
  /* Прижата к колонке кнопок слева: пустое поле копится справа, одним куском,
     а не двумя щелями по краям, как было бы по центру. */
  margin: 0 auto 0 0; border: 1px solid var(--line-hot); border-radius: 8px;
  background: var(--bg-deep); overflow: hidden;
  cursor: grab; touch-action: none; user-select: none;
}
.map-plate.dragging { cursor: grabbing; }
/* Всё, что двигается при увеличении, лежит здесь: картинка, сетка и точки едут
   одним куском, иначе они разъехались бы между собой. Сама площадка не
   двигается -- она обрезает то, что вылезло за край. */
.map-canvas { position: absolute; inset: 0; transform-origin: 0 0; }
.map-image {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: .92; -webkit-user-drag: none;
}
.map-zoom { display: flex; align-items: center; gap: 6px; }
.map-zoom .btn { min-width: 30px; padding: 2px 8px; }
#mapZoomLevel { min-width: 34px; text-align: center; }
/* Сетка под картинкой: без неё пустая карта -- просто чёрный квадрат, и понять,
   что точки вообще где-то расставлены, нельзя. */
.map-grid {
  position: absolute; inset: 0; pointer-events: none;
  background:
    repeating-linear-gradient(90deg, #35e08a14 0 1px, transparent 1px 10%),
    repeating-linear-gradient(180deg, #35e08a14 0 1px, transparent 1px 10%);
}
/* Рамка зоны спавна поверх карты: кнопка слева и место на карте должны быть для
   игрока одним и тем же. Мышь не ловит -- иначе карту нельзя было бы таскать. */
.map-zone {
  position: absolute; border: 1px dashed #46c8f555; border-radius: 4px;
  background: #46c8f50f; pointer-events: none;
}
/* Круглая зона: та же рамка, но круг. Карта тянется по осям неодинаково,
   поэтому на вытянутой карте круг честно выглядит овалом -- он показывает
   то, что в мире, а не то, что ровнее на экране. */
.map-zone.round { border-radius: 50%; }
/* Подпись гасит увеличение обратным масштабом -- как и точки игроков. */
.map-zone-label {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%) scale(calc(1 / var(--mz, 1)));
  font-size: 11px; color: #46c8f5cc; white-space: nowrap;
}
/* Второй и следующие куски одной зоны: имя то же, но бледнее -- чтобы было
   видно, что это та же зона, и чтобы карта не превратилась в список имён. */
.map-zone-label.faint { color: #46c8f577; font-size: 10px; }
/* Рамка, которую админ тянет мышью: ярче обычных, чтобы её было видно поверх
   уже нарисованных зон. */
.map-zone.drawing { border-style: solid; border-color: var(--accent); background: #35e08a1a; }
/* Полоса обводки над картой: что обводим и чем закончить. */
.zone-draw-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 6px; padding: 6px 10px; border-radius: 8px;
  border: 1px solid var(--line-hot); background: #04070a80;
}
/* Строка правки зоны в окне настройки карты. */
.zone-edit { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.zone-edit input:not([type="checkbox"]) { min-width: 150px; }
.zone-edit-check { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
/* Квадратик цвета зоны в кнопке переноса: по нему кнопка и рамка на карте
   узнаются друг в друге. */
.zone-dot {
  width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto;
  box-shadow: 0 0 0 1px #04070a80;
}
.map-zone-btn { display: flex; align-items: center; gap: 7px; }
.zone-edit input[type="color"] {
  width: 34px; height: 24px; min-width: 34px; padding: 1px; cursor: pointer;
}
/* Свёрнутая часть настройки карты: картинка и границы. Заголовок крупнее, чем у
   прочих <details class="extra">, -- это не сноска, а половина окна. */
.map-basics > summary { font-size: 12px; color: var(--text); }
.map-basics { margin: 0 0 6px; }
/* Ползунок видимости: короткий, чтобы строка зоны не разъезжалась на две. */
.zone-edit input[type="range"] { width: 92px; min-width: 92px; }
.zone-edit .mono { min-width: 38px; text-align: right; }
/* Точка гасит увеличение обратным масштабом: иначе при четырёхкратном зуме она
   раздувается вчетверо, и приближали бы мы кляксу, а не разбирали, кто где. */
.map-dot {
  position: absolute; transform: translate(-50%, -50%) scale(calc(1 / var(--mz, 1)));
  border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 10px #35e08aaa, 0 0 0 1px #04070a;
  cursor: default;
}
.map-dot.me { background: var(--cyan); box-shadow: 0 0 12px #46c8f5cc, 0 0 0 1px #04070a; }
/* Подпись показывается по наведению: два десятка ников поверх карты
   превращают её в кашу. */
.map-tag {
  position: absolute; left: 50%; bottom: calc(100% + 5px); transform: translateX(-50%);
  white-space: nowrap; font-size: 11px; padding: 2px 6px; border-radius: 6px;
  background: #04070ae0; border: 1px solid var(--line-hot); color: var(--text);
  opacity: 0; transition: opacity .12s; pointer-events: none;
}
.map-dot:hover .map-tag { opacity: 1; }
.map-dot:hover { z-index: 2; }

/* ---------------------------------------------------------- поддержка -- */

/* Две колонки: слева ветки, справа открытая. Список узкий -- в нём читают тему
   и состояние, а не текст. Пустой список колонок не делит вовсе: показывать
   рядом две пустые коробки («обращений нет» и «выберите обращение») -- это
   ровно то, чего на первом экране быть не должно. */
/* Раздел занимает всё, что осталось от экрана под шапкой и над подвалом:
   переписка -- это окно, в котором сидят, и разговор на треть высоты с пустотой
   под ним читается как «тут ничего нет». Вычитается: шапка сайта, подвал,
   шапка раздела с кнопками и отступы колонки. */
.support-grid {
  display: grid; grid-template-columns: minmax(230px, 320px) minmax(0, 1fr);
  gap: 14px; align-items: stretch;
  /* Высоту считает fitSupport(): от места, куда встала шапка раздела, до
     подвала. Числом её не задать -- обе границы плавают.

     minmax(0, 1fr) у строки -- не украшение: без него строка сетки растёт по
     содержимому (одиннадцать сообщений в ленте), высота карточки считается от
     неё же, и «height: 100%» упирается в само себя. Тогда переписка вылезает
     под подвал, а поле ответа оказывается за краем экрана. */
  height: var(--support-h, 460px);
  grid-template-rows: minmax(0, 1fr);
}
/* Колонка с веткой: даёт карточке ужаться до высоты строки, а не растянуть её.
   min-height: 0 -- то же правило, что и у ленты внутри карточки. */
.support-thread { display: flex; min-width: 0; min-height: 0; }
/* Список сам по себе короткий: он стоит сверху, а не растягивается на всю
   высоту. Станет длинным -- поедет своей прокруткой, не утаскивая страницу. */
.support-list { align-self: start; max-height: 100%; overflow-y: auto; }
.support-grid.is-empty { grid-template-columns: minmax(0, 1fr); height: auto; }
.support-list { display: grid; gap: 7px; }

/* «Новое обращение» -- пунктирная плитка первой строкой списка, а не кнопка в
   шапке раздела: живое обращение у игрока одно, и кнопка «завести ещё» большую
   часть времени отвечала бы отказом. Пунктир говорит «здесь появится ещё одна
   строка», а не «нажми меня немедленно». */
.ticket-new {
  width: 100%; font: inherit; font-size: 13px; cursor: pointer;
  color: var(--accent); background: #35e08a0a;
  border: 1px dashed #35e08a3d; border-radius: 12px; padding: 11px 13px;
  text-align: left; letter-spacing: .01em;
  transition: background .15s, border-color .15s;
}
.ticket-new:hover { background: #35e08a16; border-color: var(--accent-dim); }
/* Заводить нельзя -- на том же месте объяснение, почему, и что делать вместо. */
.ticket-locked {
  font-size: 11.5px; line-height: 1.45; color: var(--muted);
  border: 1px dashed var(--line); border-radius: 12px; padding: 9px 13px;
}

/* Переключатель «Мои / Очередь» -- сегменты, а не две отдельные кнопки: выбор
   один из двух, и подсвечен всегда ровно один. */
.btn.is-on {
  background: linear-gradient(140deg, var(--accent), #22c58f);
  border-color: transparent; color: #04140c; font-weight: 650;
}

/* Строка списка -- кнопка: по ней жмут, и вести себя она должна как кнопка. */
.ticket-row {
  position: relative; display: grid; gap: 5px; width: 100%;
  text-align: left; font: inherit; cursor: pointer;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 10px 13px 10px 15px;
  transition: border-color .15s, background .15s;
}
.ticket-row:hover { border-color: var(--line-hot); background: var(--raised-2); }
/* Полоска состояния у левого края -- как у товара в каталоге: очередь читают
   сверху вниз, и «чей сейчас ход» должно быть видно до чтения слов. */
.ticket-row::before {
  content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px;
  border-radius: 0 3px 3px 0; background: var(--muted);
}
.ticket-row.state-open::before { background: var(--amber); }
.ticket-row.state-answered::before { background: var(--accent); }
.ticket-row.state-closed::before { background: var(--cyan); opacity: .75; }
.ticket-row.is-open { border-color: var(--accent-dim); background: var(--raised-2); }
.ticket-head { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.ticket-subject {
  font-size: 13.5px; font-weight: 550; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ticket-meta {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 11px; color: var(--muted); min-width: 0;
}
/* В СТРОКЕ СПИСКА подпись живёт ровно в одну строку: перенос уносил вниз дату,
   оставляя висеть точку-разделитель, и строки в списке разъезжались по высоте.
   Не влезло -- значит, тема раздела сокращается многоточием, а дата остаётся:
   «когда» в очереди важнее, чем «про что» (про что написано выше). */
.ticket-row .ticket-meta { flex-wrap: nowrap; overflow: hidden; }
.ticket-row .ticket-meta > span:not(.ticket-state):not(.cat-sep) {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ticket-row .ticket-meta > .mono { flex: none; }
/* Состояние словом: «ждёт ответа» и «ответили» цветом не различить, а разница в
   них ровно та, ради которой в список и смотрят. */
.ticket-state {
  font-family: var(--mono); font-size: 10px; letter-spacing: .04em;
  text-transform: uppercase; white-space: nowrap;
  padding: 2px 8px; border-radius: 6px; flex: none;
}
.state-open .ticket-state, .ticket-state.state-open {
  color: var(--amber); background: #f0b42914; border: 1px solid #f0b42933;
}
.state-answered .ticket-state, .ticket-state.state-answered {
  color: var(--accent); background: #35e08a14; border: 1px solid #35e08a33;
}
/* «Закрыто» -- голубым, а не серым: серым в панели помечено выключенное и
   пустое, а закрытое обращение -- это законченная работа, и выглядеть как
   «ничего нет» она не должна. Голубой при этом не спорит с двумя живыми
   состояниями: янтарь -- ждём нас, зелёный -- ход игрока. */
.state-closed .ticket-state, .ticket-state.state-closed {
  color: var(--cyan); background: #46c8f514; border: 1px solid #46c8f533;
}
/* Точка непрочитанного -- в строке заголовка, у самого края. */
.ticket-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--accent); box-shadow: 0 0 8px var(--accent);
}

/* Первый экран поддержки: он же справка. Человек, у которого что-то сломалось,
   пишет «не работает» и уходит ждать -- а потом три письма уходят на «какой ник,
   какой сервер, когда». Дешевле сказать это здесь, до того как он начал. */
.support-welcome { display: grid; gap: 16px; padding: 20px 22px; }
.support-welcome-head { display: flex; align-items: flex-start; gap: 14px; }
.support-welcome-head h3 { margin: 0 0 4px; font-size: 16px; }
.support-emoji {
  width: 40px; height: 40px; flex: none; display: grid; place-items: center;
  font-size: 19px; border-radius: 12px;
  color: var(--accent); background: #35e08a12; border: 1px solid #35e08a30;
}
.support-tips { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.support-tip-head {
  font-size: 11px; text-transform: uppercase; letter-spacing: .12em;
  color: var(--muted); margin-bottom: 6px;
}
.support-tip ul { margin: 0; padding-left: 18px; display: grid; gap: 3px; }
.support-tip li { font-size: 12.5px; line-height: 1.45; }
.support-welcome-actions { gap: 10px; }
/* «Очередь пуста» -- сообщение, а не карточка товара: без рамки в полэкрана. */
.support-quiet { display: grid; gap: 8px; justify-items: center; min-height: 120px; }

/* Ветка -- три этажа: шапка, лента, поле ответа. Скользит только лента; шапка
   с кнопками и поле ответа стоят на месте, как в любом окне переписки. Высота
   карточки -- по экрану, а не по числу сообщений: иначе длинный разбор уносил
   поле ответа под сгиб, и отвечать приходилось, пролистав всё обратно. */
.ticket-card {
  display: flex; flex-direction: column; gap: 12px;
  flex: 1 1 auto; min-height: 0;
}
.ticket-card-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; flex: none;
}
.ticket-card-title { min-width: 0; display: grid; gap: 5px; }
.ticket-card .ticket-subject { font-size: 15px; white-space: normal; }
/* «Чей ход» -- строкой под шапкой: ярлык состояния говорит, что с обращением, а
   человеку надо знать, что делать ему. */
.ticket-next { font-size: 12px; color: var(--muted); }
/* Управление веткой -- в шапке: внизу пишут ответ, и «Удалить» рядом с
   «Отправить» стояло бы не там, где ему место. */
.ticket-acts { display: flex; gap: 6px; flex: none; }

/* Лента ветки. Высота ограничена, а не «сколько получится»: в длинном разборе
   поле ответа уезжало бы за нижний край, и отвечать пришлось бы вслепую. */
.ticket-feed {
  display: grid; gap: 9px; align-content: start;
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  /* Докрутившись до края ленты, страница за ней не едет: разговор читают
     колесом, и «дочитал ветку -- уехала вся вкладка» тут ни к чему. */
  overscroll-behavior: contain; scroll-behavior: smooth;
  padding-right: 4px; justify-items: start;
}
.ticket-feed .ticket-msg.is-mine { justify-self: end; }
/* Сообщение -- реплика в разговоре двоих: своё прижато вправо, собеседника --
   влево, и ни одно не растягивается на всю ширину (читать строку в 140 символов
   тяжело, а по краю видно, кто говорит, ещё до подписи). */
.ticket-msg {
  border: 1px solid var(--line); border-radius: 12px;
  background: #0b1116; padding: 9px 13px; max-width: 80%; min-width: 0;
}
.ticket-msg.is-mine {
  margin-left: auto; border-color: var(--line-hot); background: #101a21;
  border-bottom-right-radius: 4px;
}
.ticket-msg:not(.is-mine) { border-bottom-left-radius: 4px; }
/* Ответ поддержки зелёный: в длинной ветке важно не читать подпись, а видеть,
   где кончился вопрос и начался ответ. Цвет -- про роль, сторона -- про то, кто
   из двоих ты сам. */
.ticket-msg.from-admin { border-left: 2px solid var(--accent-dim); background: #0c1a14; }
.ticket-msg-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: 11px; color: var(--muted); margin-bottom: 4px;
}
.ticket-msg-head b { color: var(--text); font-weight: 600; font-size: 12px; }
.ticket-time { font-size: 10.5px; white-space: nowrap; }
/* Текст писал человек: переносы строк -- его, и съедать их нельзя. */
.ticket-body { font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
/* Поле ответа и кнопка -- одной строкой: кнопка держится у правого нижнего угла
   поля, как в любом окне переписки. */
.ticket-reply {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px;
  align-items: end; flex: none;
}
.ticket-reply textarea { width: 100%; resize: vertical; min-height: 76px; }
/* В обращении пишут словами, а не кодом: моноширинный шрифт полей панели тут
   мешает -- он для чисел и путей, а не для «сегодня в 20:30 пропал дино». */
.ticket-text { font-family: inherit !important; font-size: 13px !important; line-height: 1.5; }

@media (max-width: 900px) {
  .support-grid { grid-template-columns: minmax(0, 1fr); }
  .support-tips { grid-template-columns: minmax(0, 1fr); }
  /* На телефоне карточка снова растёт по содержимому: страница там листается
     целиком, и запирать разговор в окошко на треть экрана незачем. */
  .support-grid { height: auto; grid-template-rows: none; }
  .support-thread { display: block; }
  .ticket-card { flex: none; }
  .ticket-feed { overflow: visible; }
}

/* ------------------------------------------------- админка каталога -- */

/* Список товаров -- реестр, а не витрина: строка на товар, одинаковая высота у
   всех и колонки, которые совпадают сверху вниз. Раньше строка была набором
   разноширинных кусков, а справа стояли четыре кнопки со словами -- они и
   занимали половину ширины, повторяясь в каждой строке. Теперь право на слово
   есть только у товара; всё служебное (тип, состояние, действия) сжато до
   цвета, точки и значка. */
.catalog-list { display: grid; gap: 6px; }
/* Строка каталога -- это карточка плюс пояснение под ней; перетаскивается
   целиком, поэтому зазор между товарами задаётся списком, а не карточкой. */
.catalog-entry { display: grid; }

.cat-card {
  position: relative;
  display: grid;
  grid-template-columns: 16px 30px minmax(190px, 420px) minmax(0, 1fr) minmax(112px, auto) 104px auto;
  align-items: center; gap: 14px;
  min-height: 50px; padding: 5px 12px 5px 15px;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px;
  transition: border-color .15s ease, background .15s ease;
}
.cat-card:hover { border-color: var(--line-hot); background: var(--raised-2); }
/* Полоска цвета типа у левого края. Тип написан и словом, но слово читают, а
   полоску видят: по ней список делится на группы при беглом взгляде сверху
   вниз, не читая ни строки. */
.cat-card::before {
  content: ""; position: absolute; left: 0; top: 9px; bottom: 9px; width: 3px;
  border-radius: 0 3px 3px 0; background: var(--type, var(--muted));
}
/* Выключенный и забракованный товары гаснут, но не исчезают: админ приходит
   сюда именно за ними. Пунктир -- общий признак «в витрину не попадёт»,
   красная полоска у забракованного -- «дело не в выключателе». */
.cat-card.is-off, .cat-card.is-bad { border-style: dashed; }
.cat-card.is-off .cat-title, .cat-card.is-bad .cat-title { color: var(--muted); }
.cat-card.is-off .cat-badge, .cat-card.is-off .cat-type, .cat-card.is-off .cat-price,
.cat-card.is-bad .cat-badge, .cat-card.is-bad .cat-price { opacity: .55; }
.cat-card.is-bad::before { background: var(--danger); }

/* Ручка перетаскивания. Тащат только за неё, поэтому курсор меняется именно
   здесь -- на остальной строке живут кнопки. В покое ручка почти не видна:
   переставляют товары редко, а мозолит глаза она в каждой строке. */
.cat-grip {
  cursor: grab; color: #34495455; text-align: center; user-select: none;
  font-size: 14px; line-height: 1; transition: color .15s;
}
.cat-card:hover .cat-grip { color: var(--muted); }
.cat-grip:active { cursor: grabbing; }
.catalog-entry.dragging { opacity: .45; }
.catalog-entry.dragging .cat-card { border-style: dashed; border-color: var(--accent-dim); }

/* Плитка значка. Цвет типа в заливке и рамке -- та же роль, что у полоски:
   слот, подписка и прайм различаются раньше, чем прочитано название. */
.cat-badge {
  width: 30px; height: 30px; display: grid; place-items: center;
  font-size: 15px; line-height: 1; border-radius: 9px;
  color: var(--type, var(--muted));
  background: color-mix(in srgb, var(--type, #7b8f9b) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--type, #7b8f9b) 30%, transparent);
}

.cat-main { min-width: 0; display: grid; gap: 2px; }
/* Первая строка карточки: название и ярлык типа. Ярлык жмётся к названию
   (flex: none), а сокращается при нехватке места название -- у него есть
   многоточие, а у ярлыка одно слово, которое либо влезает, либо нет. */
.cat-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
.cat-title {
  font-size: 13.5px; font-weight: 550; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Вторая строка: id (им товар зовут в файле и в командах) и категория витрины.
   Обе -- служебные, поэтому мельче названия и тише по цвету. */
.cat-meta {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  font-size: 11px; color: var(--muted);
}
.cat-meta code { font-family: var(--mono); font-size: 11px; letter-spacing: .01em; }
.cat-meta > span:not(.cat-sep) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-sep { color: var(--line-hot); }

.cat-type {
  flex: none;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .05em;
  text-transform: uppercase; white-space: nowrap;
  padding: 3px 9px; border-radius: 6px;
  color: var(--type, var(--muted));
  background: color-mix(in srgb, var(--type, #7b8f9b) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--type, #7b8f9b) 26%, transparent);
}

/* «Что делает» -- служебная строка, поэтому моноширинная и тише названия.
   Растягивается она, а не название: у названия есть свой потолок ширины, и без
   этого при пустом описании товара строка снова расползалась бы пустотой. */
.cat-facts {
  min-width: 0; font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.cat-price {
  display: flex; align-items: center; justify-content: flex-end; gap: 12px;
  font-size: 13px;
}
.cat-coin { display: inline-flex; align-items: center; gap: 5px; }

/* Состояние: точка и слово. Работает точка -- цвет виден краем глаза; слово
   стоит рядом для тех трёх состояний, которые по цвету не различить
   («выключен» и «скрыт» оба серые, и путать их нельзя). */
.cat-state { display: flex; align-items: center; gap: 7px; font-size: 11.5px; }
.cat-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.cat-state.on { color: var(--accent); }
.cat-state.on .cat-dot { box-shadow: 0 0 7px var(--accent); }
.cat-state.off, .cat-state.hid { color: var(--muted); cursor: help; }
/* Скрытый -- кольцо вместо точки: товар продаётся, но его не видно в витрине. */
.cat-state.hid .cat-dot { background: none; box-shadow: inset 0 0 0 1.5px currentColor; }
.cat-state.bad { color: var(--danger); cursor: help; }

/* Ряд кнопок-значков. Приглушены, пока строку не тронули мышью: в списке из
   двадцати товаров восемьдесят ярких кнопок перекрикивали сам список. Видны
   всегда (а не только при наведении) -- иначе на телефоне до них не добраться. */
.cat-acts { display: flex; gap: 2px; justify-content: flex-end; }
.icon-btn {
  width: 30px; height: 30px; display: grid; place-items: center;
  padding: 0; border: 1px solid transparent; border-radius: 9px;
  background: none; color: #56707d; cursor: pointer;
  transition: color .13s, background .13s, border-color .13s;
}
.icon-btn svg { width: 17px; height: 17px; display: block; }
.cat-card:hover .icon-btn { color: var(--muted); }
.icon-btn:hover, .cat-card:hover .icon-btn:hover {
  color: var(--text); background: #16222a; border-color: var(--line-hot);
}
.icon-btn:focus-visible { outline: none; color: var(--text); border-color: var(--accent-dim); }
/* Кнопка, возвращающая товар (включить выключенный, вернуть скрытый в витрину),
   светится зелёным: она -- единственное осмысленное действие в такой строке. */
.icon-btn.is-armed, .cat-card:hover .icon-btn.is-armed { color: var(--accent); }
.icon-btn.is-danger:hover, .cat-card:hover .icon-btn.is-danger:hover {
  color: var(--danger); background: #2a1212; border-color: #5a2626;
}

/* Забракованный товар лежит в файле, но не продаётся: причина -- сразу под ним,
   с отступом под колонку названия, чтобы читалось как продолжение строки. */
.cat-why {
  margin: 3px 0 0 46px; padding: 5px 11px;
  font-size: 11px; color: var(--danger);
  background: #ff5b5b0d; border-left: 2px solid #ff5b5b66; border-radius: 0 8px 8px 0;
}

/* Список подписок в окне «Подписки» -- те же карточки, но без ручки и без
   колонок под порядок: подписок три-четыре, и выравнивать там нечего. */
.cat-card.is-plain { display: flex; }
.cat-card.is-plain .cat-main { flex: 1 1 auto; }
.cat-card.is-plain .cat-price { flex: none; }

/* Ярлыки состояния, которые панель ставит и вне каталога (мутации, связи,
   пороги). Каталог рисует состояние точкой, но эти три класса живут своей
   жизнью в других списках. */
.state-on { color: var(--accent); }
.state-off { color: var(--muted); }
.state-bad { color: var(--danger); cursor: help; }
/* Нормы подписки: строка = товар + «N раз за H часов». Числа узкие, товар
   тянется -- иначе селект с длинным названием выдавливал бы поля за край. */
.quota-editor { display: grid; gap: 7px; justify-items: start; }
.quota-rows { display: grid; gap: 6px; width: 100%; }
.quota-row {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
}
.quota-row select { flex: 1 1 150px; min-width: 0; }
.quota-row input { flex: 0 0 66px; }
.quota-row .btn-danger { flex: none; }

.catalog-warning {
  border: 1px solid #f0b42955; background: #f0b4290f; color: var(--amber);
  border-radius: 12px; padding: 9px 13px; margin-bottom: 12px; font-size: 12px;
}

/* Форма товара в модальном окне. */
.item-form { display: grid; gap: 10px; }
.form-row { display: grid; grid-template-columns: 148px 1fr; gap: 12px; align-items: center; }
.form-row > label { font-size: 12px; color: var(--muted); }
.form-cell { min-width: 0; display: grid; gap: 3px; }
/* HEX -- основной ввод цвета, поэтому он шире пипетки и стоит перед ней. */
.hex-input { width: 92px; text-transform: uppercase; }
/* Зоны окраса в форме товара: две колонки, в каждой -- флажок, подпись, HEX и
   пипетка одной строкой. Ширины заданы жёстко: общее правило формы растягивает
   текстовые поля на всю ячейку, и строка разваливалась на три этажа. */
.zone-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 14px; }
.zone-pick { display: flex; align-items: center; gap: 8px; min-width: 0; }
.zone-pick .zone-label { flex: 1 1 auto; min-width: 0; }
.zone-pick .zone-label > span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zone-pick input[type="checkbox"] { flex: none; width: 16px; height: 16px; }
.zone-pick input[type="text"] { flex: none; width: 88px; }
.zone-pick input[type="number"] { flex: none; width: 68px; }
.zone-pick input[type="color"] { flex: none; width: 34px; height: 26px; padding: 2px; }
/* Подпись цветовой зоны: перевод и под ним игровое имя поля. */
.zone-label { display: grid; gap: 1px; }
.zone-raw { font-size: 10px; color: var(--line-hot); letter-spacing: .02em; }
.form-cell input[type="text"],
.form-cell input[type="number"] { width: 100%; }
.form-cell input[type="checkbox"] { justify-self: start; width: 16px; height: 16px; }
.item-form + .item-form { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }

/* Категории витрины: название, id и кнопки порядка -- одной строкой. */
.cat-list { display: grid; gap: 7px; }
.cat-row { display: grid; grid-template-columns: 1fr 150px auto auto auto; gap: 7px; align-items: center; }
.cat-row input { min-width: 0; }
.cat-row .btn { padding: 5px 9px; }
/* Прайс секретных мутаций: название и две цены. Поля узкие -- цена в монетах
   короткая, а имя мутации длинное и должно занимать всё остальное. */
.mut-price-row { display: grid; grid-template-columns: 1fr 108px 108px; gap: 8px; align-items: center; }
.mut-price-row input { min-width: 0; width: 100%; font-family: var(--mono); font-size: 12px; }
.mut-price-row .row { gap: 5px; }
/* Ячейка цены -- <label> (клик по монете ставит курсор в поле), а подписи в формах
   сверстаны столбиком: монета вставала над полем. Здесь -- в строку. */
.mut-price-row .mut-price { display: flex; flex-flow: row nowrap; align-items: center; margin: 0; }
.mut-price-row .mut-price input { flex: 1 1 auto; width: auto; }
/* Ценник настоящими монетами: пары «значок + число» с воздухом между парами. */
.mut-price { gap: 4px; align-items: center; }
.mut-price .coin-icon + .mono { margin-right: 6px; }

@media (max-width: 620px) {
  .zone-form { grid-template-columns: minmax(0, 1fr); }
}

/* Узкий экран: колонки каталога больше не выстраиваются -- строка становится
   гибкой лентой и переносит хвост (цену, состояние) под название. Кнопки
   остаются справа: их ищут у края, а не там, куда их отнесёт перенос. */
@media (max-width: 1040px) {
  .cat-card { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 8px 12px; }
  .cat-main { flex: 1 1 170px; }
  /* «Что делает» встаёт рядом с названием, пока хватает места, и переносится
     вместе с ценой -- иначе строка расползалась на три этажа. */
  .cat-facts { flex: 1 1 150px; }
  .cat-price { flex: 0 0 auto; justify-content: flex-start; }
  .cat-acts { margin-left: auto; }
  .cat-why { margin-left: 0; }
}

/* Телефон: «что делает» уходит совсем. Место есть только на одну строку
   текста, и название товара нужнее подсказки -- иначе от него остаётся
   «Пополнони...», а сама подсказка всё равно обрезается на полуслове. */
@media (max-width: 720px) {
  .cat-facts { display: none; }
  .cat-main { flex: 1 1 160px; }
}

@media (max-width: 900px) {
  .form-row { grid-template-columns: 1fr; gap: 4px; }
  .cat-row { grid-template-columns: 1fr 1fr auto auto auto; }
}

/* Витрина. Карточка -- три этажа: картинка товара, название с одной строкой
   сути, подвал с ценой и кнопкой. Картинку задаёт админ ссылкой; нет ссылки --
   на её месте крупный значок той же высоты, иначе ряд шёл бы лесенкой.
   Высота этажей фиксирована здесь, а не содержимым: товар с выбором вида выше
   соседей, и без этого витрина расползалась бы. */
.shop-item {
  display: flex; flex-direction: column; padding: 0; overflow: hidden;
  transition: border-color .15s ease, transform .15s ease;
}
.shop-item:hover { border-color: var(--line-hot); transform: translateY(-2px); }
.shop-item::before { left: 0; right: 0; }

/* Картинка -- квадрат во всю ширину плитки, а не полоска сверху: вместе с
   названием и подвалом карточка выходит вдвое выше своей ширины. */
.shop-media {
  position: relative; aspect-ratio: 1 / 1; flex: 0 0 auto;
  display: grid; place-items: center; overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 0%, #35e08a12, transparent 70%),
    linear-gradient(180deg, var(--raised-2), var(--raised));
  border-bottom: 1px solid var(--line);
}
.shop-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Низ картинки притемнён -- под ним лежит название, и светлый снимок его съедал. */
.shop-media.has-image::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 52%;
  background: linear-gradient(180deg, transparent, #04070add);
  pointer-events: none;
}
.shop-media .shop-icon { font-size: 42px; opacity: .8; }
.shop-badge {
  position: absolute; top: 9px; left: 9px; z-index: 1;
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--cyan);
  background: #04070ac7; border: 1px solid #46c8f533;
  padding: 2px 8px; border-radius: 6px;
}
.shop-flag {
  position: absolute; top: 9px; right: 9px; z-index: 1;
  font-size: 11px; color: var(--accent);
  background: #04070ac7; border: 1px solid var(--accent-dim);
  padding: 2px 8px; border-radius: 6px; white-space: nowrap;
}

/* Сколько монет даёт обмен -- крупно, поверх картинки: у обмена это и есть сам
   товар. Цена в подвале карточки говорит только вторую половину сделки. */
.shop-gain {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 10px;
  background: linear-gradient(180deg, transparent, #04070ae6);
}
.shop-gain b {
  font-family: var(--mono); font-size: 23px; line-height: 1;
  color: var(--accent); text-shadow: 0 2px 10px #000;
}
.shop-gain.red b { color: var(--danger); }
.shop-gain .coin-icon { width: 21px; height: 21px; }

.shop-body { padding: 11px 12px 0; flex: 1 1 auto; }
/* Длинное название админ написать может, а вот развалить им витрину -- нет. */
.shop-item .card-title {
  font-size: 13px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.3;
}
.shop-gist {
  margin-top: 5px; font-size: 12px; color: var(--muted); line-height: 1.45;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* На плитке витрины сути нет (2026-08-17): в две строки она всё равно
   обрывалась многоточием на полуслове, а целиком лежит в окне покупки -- том
   самом, которое открывается следующим нажатием. Плитка отвечает на «что это»:
   картинка, тип, название, цена. Узел остаётся в разметке: тот же класс
   работает в других списках, а здесь его прячет одна строка. */
#shopGrid .shop-gist { display: none; }
.shop-item .mut-picker { margin-top: 10px; }
/* Подвал плитки: цена и кнопка друг под другом. В строку они помещались, пока
   карточка была широкой; в узкой «Купить» рядом с ценником сжималось в огрызок. */
.shop-item .card-actions {
  margin: 11px 12px 12px; padding-top: 11px; border-top: 1px solid var(--line);
  flex-direction: column; align-items: stretch; gap: 8px;
}
.shop-item .card-actions .btn { width: 100%; text-align: center; }
.shop-item .prices { justify-content: center; }
.shop-item .swatches { margin-top: 10px; }
/* Картинка товара в окне покупки -- шире и целиком, здесь её не обрезаем. */
.shop-shot {
  margin-bottom: 12px; border: 1px solid var(--line); border-radius: 8px;
  overflow: hidden; background: var(--bg-deep);
}
.shop-shot img { display: block; width: 100%; max-height: 260px; object-fit: cover; }
/* Предпросмотр ссылки в форме товара: битую видно сразу, а не после сохранения. */
.img-preview {
  margin-top: 8px; width: 132px; height: 78px; border-radius: 12px;
  border: 1px solid var(--line); overflow: hidden; background: var(--bg-deep);
}
.img-preview.bad { border-color: var(--danger); }
.img-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shop-icon { font-size: 21px; line-height: 1; margin-right: 4px; }

/* ---- подписки (админка) --------------------------------------------------
   Список подписок правится целиком, как категории: у каждой свой срок, слоты и
   состав, и сверять их удобнее рядом друг с другом. */
.sub-list { display: grid; gap: 12px; }
.sub-row {
  display: grid; gap: 9px; padding: 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--raised);
}
/* Шапка строки: название во всю оставшуюся ширину, id рядом, крестик с краю.
   Подписи есть у обоих -- в заполненной форме плейсхолдер не виден.
   Класс НЕ .sub-head: тот -- заголовок раздела (капс, разрядка, верхний отступ),
   и строка формы от него получала вид подзаголовка вместо пары полей. */
.sub-row-head { display: flex; gap: 9px; align-items: flex-start; flex-wrap: wrap; }
.sub-row-head > .sub-field:first-child { flex: 1 1 200px; }
.sub-row-head > .sub-field { flex: 0 1 180px; }
.sub-row-head > .btn { flex: none; }
.sub-fields {
  display: grid; gap: 9px 12px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
}
/* align-content: start -- иначе поле в короткой ячейке растягивалось по высоте
   самой длинной колонки (у соседей подсказка в три строки) и выглядело выше и
   ниже остальных. */
.sub-field {
  display: grid; gap: 3px; align-content: start;
  font-size: 12px; color: var(--muted); min-width: 0;
}
.sub-field > input, .sub-field > select { width: 100%; }
.sub-field-cell { display: grid; gap: 3px; min-width: 0; }
.sub-field-cell input { width: 100%; }
.sub-grants { border-top: 1px dashed var(--line); padding-top: 9px; }
.sub-grants .zone-form { margin-top: 8px; }
.sub-grants .row { margin-top: 7px; }
/* Раздел формы подписки: полей полтора десятка, и без заголовков это была
   сплошная простыня числовых клеток. */
.sub-block { border-top: 1px dashed var(--line); padding-top: 9px; display: grid; gap: 8px; }
.sub-block > b { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
.sub-block .quota-editor { margin-top: 2px; }
/* Итог обычными словами: по отдельности поля не отвечают на единственный вопрос
   покупателя -- что он получит за эти деньги. */
.sub-summary {
  margin-top: 4px; padding: 9px 12px; font-size: 12px;
  border: 1px solid var(--accent-dim); border-radius: 12px;
  background: #35e08a0a; color: var(--text);
}

/* ---- слоты мутаций живого дино (вкладка «Мутации») ---------------------- */
/* Докупленный слот в окне мутаций -- отбит слева, чтобы не путать с основным. */
.mut-extra { margin-top: 12px; border-top: 1px dashed var(--line); padding-top: 10px; }
.mut-pick-extra { border-left: 2px solid var(--accent-dim); padding-left: 8px; }
.price {
  display: flex; align-items: center; gap: 6px; white-space: nowrap;
  background: #f0b4290f; border: 1px solid #f0b42933;
  border-radius: 8px; padding: 3px 10px;
}
.price b { font-family: var(--mono); color: var(--amber); }

/* Вторая валюта. Картинка та же самая, что у монеты, только перекрашенная:
   hue-rotate сохраняет блики и рельеф, а заливка силуэта сплошным цветом
   потеряла бы сам рисунок монеты. Угол подобран замером: у coin.png светлые
   пиксели лежат на 100° (зелёный), -100° уводит их на 6° -- чистый красный. */
.coin-icon.coin-red { filter: hue-rotate(-100deg) saturate(2.2); }
.price.price-red { background: #e5484d0f; border-color: #e5484d40; }
.price.price-red b { color: var(--danger); }
.price.price-off b { color: var(--muted); }
.prices { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
/* Поле ввода цены во второй валюте -- чтобы две одинаковые клетки не путались. */
.input-red { border-color: #e5484d59; color: var(--danger); }

/* Наборы пополнения. Выбранный подсвечен рамкой -- это переключатель, а не набор
   отдельных кнопок «купить». */
.donate-grid {
  display: grid; gap: 9px; margin-bottom: 4px;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
}
.donate-pack {
  position: relative; text-align: center; cursor: pointer;
  background: var(--raised); color: var(--text);
  border: 1px solid var(--line); border-radius: 12px; padding: 12px 10px;
}
.donate-pack:hover { border-color: var(--line-hot); }
.donate-pack.on { border-color: var(--danger); background: #e5484d0f; }
.donate-coins { display: flex; align-items: center; justify-content: center; gap: 5px; }
.donate-coins b { font-family: var(--mono); font-size: 17px; color: var(--danger); }
.donate-best {
  margin-top: 5px; font-size: 11px; color: var(--amber);
  text-transform: uppercase; letter-spacing: .1em;
}

/* Состав товара в окне «Подробнее» и в подтверждении покупки. */
.shop-facts { margin: 10px 0 0; padding-left: 18px; font-size: 13px; color: var(--muted); }
.shop-facts li { margin: 4px 0; }

/* Счётчик количества в окне покупки. Кнопки квадратные и одного размера с
   полем: «−» и «+» должны попадаться пальцем, а не быть подписью к вводу. */
.qty-wrap { margin-top: 12px; }
.qty-row { display: flex; align-items: center; gap: 8px; }
.qty-label { font-size: 12px; color: var(--muted); margin-right: auto; }
.qty-btn { width: 34px; height: 34px; padding: 0; font-size: 17px; line-height: 1; }
.qty-input {
  width: 72px; height: 34px; text-align: center; padding: 0 6px;
  /* Стрелки браузера рядом с нашими «−»/«+» -- два счётчика в одном поле. */
  -moz-appearance: textfield; appearance: textfield;
}
.qty-input::-webkit-outer-spin-button,
.qty-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* Уже открытые игроком секретные мутации в счёте активации: не в списке цен, а
   отдельным блоком -- иначе строка без цены читается как потерянная. */
.mut-free-list { margin-top: 10px; }

/* ВКЛАДКА «ОНЛАЙН»: ОДИН СПИСОК ОДИНАКОВЫХ КАРТОЧЕК (2026-08-20). Своя пешка
   стояла отдельной карточкой над заголовком «Игроки»: другой ширины, с полосами
   показателей, окрасом и своей кнопкой. Теперь она первая карточка того же
   списка и ничем, кроме метки «вы», не отличается.

   Размер у всех один. В сетке ряд и так тянет карточки под самую высокую, но
   ряды между собой разъезжались: у карточки с кнопками одна высота, у карточки
   со строкой состояния («нет пешки») -- другая. Наименьшая высота задана явно, а
   нижняя строка -- кнопки или состояние -- прижата к низу, поэтому карточки
   совпадают и по высоте, и по тому, где у них что лежит. */
.cards-online .card { display: flex; flex-direction: column; min-height: 116px; }
.cards-online .card-actions,
.cards-online .card-state { margin-top: auto; }
/* Ник, ID и метки: метки могут не поместиться рядом с длинным ником -- пусть
   переносятся под него, а не жмут ник до многоточия. */
.card-head-tags { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
/* Ник в Steam бывает и в тридцать символов без единого пробела: без переноса
   такой ник распирал карточку шире колонки, и «все одного размера» кончалось. */
.cards-online .card-title { overflow-wrap: anywhere; }
/* «вы» -- своя карточка в общем списке. Зелёная, как всё, что про самого игрока. */
.mine-tag {
  color: var(--accent); background: #35e08a14; border-color: #35e08a33;
  font-size: 10px; padding: 1px 8px;
}

/* База внешности (Каталог -> Узоры): вид в строке, три числа в клетках. */
.pat-table { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.pat-row {
  display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: 8px; align-items: end;
  padding: 7px 9px; border: 1px solid var(--line); border-radius: 8px;
  background: #05090c;
}
.pat-head { background: none; border-style: dashed; }
.pat-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pat-class { font-size: 10px; overflow: hidden; text-overflow: ellipsis; }
.pat-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pat-cap {
  font-size: 9px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .08em;
}
.pat-num { width: 100%; height: 28px; padding: 0 6px; text-align: center; }
.pat-seen { font-size: 10px; }

@media (max-width: 560px) {
  .pat-row { grid-template-columns: 1fr 1fr; }
  .pat-name { grid-column: 1 / -1; }
}

.ttl { font-size: 12px; color: var(--muted); white-space: nowrap; }
.ttl::before { content: "⏳ "; }
.ttl.soon { color: var(--amber); }

.card.locked { opacity: .75; }
.card.locked .muted { font-size: 13px; }

/* ----------------------------------------------------------------- misc -- */

.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 13px; border-bottom: 1px solid #ffffff0a; font-size: 13px; }
th { color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: .1em; }
tr:last-child td { border-bottom: none; }

/* Slots read as a register, not a gallery: ten rows fit where four cards did. */
/* align-content: start -- чтобы запас высоты (renderSlots держит её по самой
   длинной вкладке) оставался ПОД списком, а не растягивал сами строки. */
.slot-list { display: grid; gap: 7px; align-content: start; }
.slot-row {
  display: grid;
  grid-template-columns: 118px minmax(0, 1fr) auto auto;
  align-items: center; gap: 14px;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 9px 14px;
}

/* СТРОКИ ХРАНИЛИЩА ОДНОЙ ВЫСОТЫ. Высоту раньше задавало содержимое: занятый
   слот с кнопками выходил на восемь пикселей выше пустого, и список шёл
   ступеньками. Отступ по вертикали убавлен настолько, чтобы самая высокая
   строка (с кнопками) влезала в min-height -- иначе он не выровнял бы ничего.
   Ширину список занимает всю, как и остальные списки панели. */
#slotGrid .slot-row { min-height: 44px; padding-top: 5px; padding-bottom: 5px; }
.slot-row.empty-row,
.slot-row.locked { grid-template-columns: 1fr auto; opacity: .65; }
/* Locked but occupied: the dinosaur is still there, so it keeps its species tag. */
.slot-row.locked.has-data { grid-template-columns: 118px 1fr auto; opacity: .8; }
.slot-row.locked { border-style: dashed; }
/* Временный слот (дино с колеса, из магазина, с аукциона): пунктир и тёплая
   рамка -- он тут гость. Стоит первым в списке, а не своим номером в хвосте. */
.slot-row.slot-temp { border-style: dashed; border-color: #f0b42947; }
.slot-row:hover { border-color: var(--line-hot); }
.temp-tag { color: var(--amber); background: #f0b42914; border-color: #f0b42933; }
/* Откуда дино в слоте -- тихая метка рядом с датой: это справка, а не действие,
   и спорить за внимание с «временный» или жёлтой «Выбор мутаций» ей незачем.
   Цвет -- только оттенком текста. */
.src-tag { font-size: 11px; color: var(--muted); background: #ffffff08; border-color: var(--line); }
.src-tag.src-shop { color: #7cc8e8; }
.src-tag.src-wheel { color: #c79bf2; }
.src-tag.src-grant { color: #8fe0a8; }
.src-tag.src-auction { color: #f0b429; }
/* Прайм в строке хранилища. Незаконченный -- такая же тихая метка, как «откуда
   дино»: это счёт, мимо которого можно пройти. Готовый -- зелёный, тем же
   цветом, что «готов» в карточке «Подробно»: одно и то же состояние в двух
   местах не должно называться разными цветами. Жёлтый тут занят и означает
   другое -- «требует действия» («временный», «Выбор мутаций»). */
.prime-tag { font-size: 11px; color: var(--muted); background: #ffffff08; border-color: var(--line); }
.prime-tag.on { color: var(--accent); background: #35e08a14; border-color: var(--accent-dim); }
/* Кнопка «Мутации» у выигранного дино: тёплая, как и метка временного
   слота, -- это то, что игрок должен сделать, а не может сделать. */
/* Жёлтая кнопка (просьба пользователя 2026-09-17): «Выбор мутаций» -- то, что
   игрок ДОЛЖЕН сделать, пока не поздно (мутации выбираются один раз, до
   активации прайма и до загрузки выигранного дино в игру). Поэтому она не
   сливается с соседними «Подробно» и «Удалить», а светится, как метка
   временного слота. Заливка приглушённая: рядом стоит зелёное «Загрузить», и две
   плотные заливки в одной строке спорят за главное. */
.btn.btn-warn {
  color: var(--amber); border-color: #f0b42966; background: #f0b4291f;
}
.btn.btn-warn:hover:not(:disabled) {
  color: #ffd98a; border-color: #f0b429cc; background: #f0b42933;
}

.slot-state { font-size: 13px; color: var(--muted); }
.slot-date { white-space: nowrap; font-size: 12px; }
/* Слот из общего пула: сохранён на соседнем сервере, играть им можно и здесь. */
.slot-origin {
  font-size: 11px; color: var(--accent-dim); letter-spacing: .04em;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* Метка «откуда дино» и сервер -- в одну строку сразу за видом. */
  display: flex; align-items: center; gap: 8px;
}
.slot-server { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.slot-actions { display: flex; gap: 6px; justify-content: flex-end; }

@media (max-width: 1000px) {
  .slot-row,
  .slot-row.empty-row,
  .slot-row.locked { display: flex; flex-wrap: wrap; }
  .slot-origin { flex: 1 1 auto; }
  .slot-actions { flex: 1 1 100%; justify-content: flex-start; }
}

.used-list { display: grid; gap: 7px; }
.used-row {
  display: flex; align-items: center; gap: 13px; flex-wrap: wrap;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 9px 13px; font-size: 13px;
}
.used-row .grow { flex: 1; min-width: 0; }
/* Дата -- у правого края и одной строкой: перенос ломал бы столбец. */
.led-when { flex: none; white-space: nowrap; }

.toasts { position: fixed; right: 20px; bottom: 20px; display: grid; gap: 9px; z-index: 100; }
.toast {
  background: var(--raised-2); border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 9px; padding: 11px 15px; min-width: 240px;
  box-shadow: 0 10px 30px #000a; animation: slide .18s ease;
  font-size: 13px;
}
.toast.err { border-left-color: var(--danger); }
@keyframes slide { from { transform: translateX(22px); opacity: 0; } }

/* ОКНА РАСТУТ ПО СОДЕРЖИМОМУ, НО НЕ ВЫШЕ ПРЕДЕЛА (2026-08-17, второй заход).
   Сначала окна были фиксированной высоты -- чтобы подгрузка списка внутри не
   двигала рамку под курсором. Оказалось, что платят за это все короткие окна:
   покупка на шесть строк разворачивалась на три четверти экрана, кнопки
   оказывались посередине, а пустота под ними читается как «окно не
   догрузилось». Теперь высота -- по содержимому, с прежним пределом: всё, что
   длиннее, упирается в него и прокручивается внутри, как раньше.

   Из-за этого окно, которое подгружает содержимое уже открытым (выбор картинки
   из галереи, редактор каталога), однократно подрастает, когда оно приедет.
   Это сознательный размен: такие окна наперечёт, а коротких -- большинство.

   Устройство: сам dialog -- колонка без внутренних отступов и без прокрутки,
   отступы и прокрутка живут внутри. Именно fit-content, а не auto: окно в
   верхнем слое растянуто браузерным inset: 0, и `auto` для него означает «во
   всю высоту», а не «по содержимому».

   ВАЖНО про display: раскладка колонкой висит на `dialog[open]`, а не на самом
   `dialog`. Закрытое окно браузер прячет своим `dialog:not([open]){display:none}`,
   но это правило БРАУЗЕРНОЕ, и любой авторский `display` его перебивает
   независимо от специфичности -- пустая рамка окна оказывалась поверх страницы
   всегда, в том числе на экране входа. */
dialog {
  background: linear-gradient(180deg, var(--raised), var(--bg-deep));
  color: var(--text); border: 1px solid var(--line-hot);
  border-radius: 14px; padding: 0; overflow: hidden;
  width: min(660px, 92vw); max-width: none;
  height: fit-content; max-height: min(78vh, 720px);
}
dialog[open] { display: flex; flex-direction: column; }
dialog::backdrop { background: #000c; backdrop-filter: blur(3px); }

/* Окна-вопросы (askConfirm/askText) ещё и уже: в них две строки текста, и на
   общей ширине они выглядели как обрывок письма. */
dialog.ask { width: min(460px, 92vw); }

/* Заголовок прибит сверху и не уезжает вместе с содержимым: он же говорит, что
   это за окно, а из середины длинной формы это уже не видно. */
.modal-head {
  flex: none; display: flex; justify-content: space-between; align-items: center;
  gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid var(--line);
}
.modal-head h3 { margin: 0; font-size: 15px; }

/* Прокручивается только тело. Окон два: общее (#modal) и выбор картинки из
   галереи (#picker), который открывается ПОВЕРХ формы товара -- в общем окне он
   снёс бы наполовину заполненную форму. Устройство у них одно. */
#modalBody,
#pickerBody {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 20px 18px;
}
/* Выбор картинки стоит над формой товара, а backdrop у обоих окон один и тот же
   тёмный: без своего слоя выбор оказывался бы под формой. */
#picker { z-index: 60; }

/* Кнопки окна прибиты к низу. Их ряд лежит внутри формы, поэтому sticky, а не
   отдельный подвал: переносить кнопки из каждой формы в общий футер значило бы
   переписать все окна разом. Фон непрозрачный -- содержимое проезжает ПОД ними,
   а не сквозь. */
#modalBody .card-actions:last-child,
#pickerBody .card-actions:last-child {
  position: sticky; bottom: -18px; z-index: 2;
  margin-top: 14px; padding: 12px 0 6px;
  background: linear-gradient(180deg, #0b1116e6, var(--bg-deep));
  border-top: 1px solid var(--line);
}

/* Страница под окном не едет. showModal() делает фон inert, но колесо мыши на
   нём страницу всё равно прокручивает -- и, закрыв окно, игрок оказывается не
   там, где был. */
body:has(dialog[open]) { overflow: hidden; }

/* Текст и поля внутри окна-вопроса. Ширина у таких окон снова своя -- см.
   dialog.ask выше. */
.modal-text { margin: 0 0 9px; font-size: 13px; line-height: 1.5; }
.modal-text:last-child { margin-bottom: 0; }
.modal-field { display: grid; gap: 5px; margin-top: 13px; }
.modal-field > label { font-size: 12px; color: var(--muted); }
.modal-field input[type="text"], .modal-field select { width: 100%; }
.modal-field textarea { min-height: 84px; }

/* ------------------------------------------------- карточка дино из слота -- */

.sheet-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; padding-bottom: 13px; border-bottom: 1px solid var(--line);
}
.sheet-head .card-title { font-size: 16px; }
.sheet-head .card-sub { margin-top: 3px; display: flex; gap: 8px; flex-wrap: wrap; }
.sheet-head-tags { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.sex-tag {
  font-family: var(--mono); font-size: 11px; white-space: nowrap;
  color: var(--cyan); background: #46c8f514; border: 1px solid #46c8f533;
  padding: 2px 9px; border-radius: 6px;
}
.sex-tag.female { color: #f6a8d0; background: #f6a8d012; border-color: #f6a8d033; }

/* Подвал паспорта своего дино: «Утилизировать» и объяснение рядом. Отделён
   чертой, как шапка сверху: это не часть показателей, а действие над ними. */
.sheet-actions {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 16px; padding-top: 13px; border-top: 1px solid var(--line);
}
.sheet-actions .muted { font-size: 12px; }
@media (max-width: 560px) {
  .sheet-actions .btn { width: 100%; }
}

/* Заголовок раздела внутри карточки: разделитель, а не отдельный элемент. */
.sheet-section {
  margin: 16px 0 9px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .1em;
  color: var(--muted);
}
/* Полосы -- в колонки по ширине места, а не одной лентой во всю карточку. На
   всю ширину инвентаря полоса здоровья тянулась на метр: глазу приходилось
   мерить её от края до края, а шесть таких подряд занимали экран. В окне слота
   и на лоте аукциона тех же правил хватает: там место уже, и колонок выйдет
   меньше -- вплоть до одной на телефоне. */
/* В КОЛОНКИ ИДУТ РАЗДЕЛЫ ЦЕЛИКОМ, а не строки внутри них: столбец полос,
   столбец окраса, столбец мутаций. Резать по отдельности каждый раздел (как
   было до этого) на широком экране давало мозаику -- обрывок мутаций рядом с
   обрывком рациона, и найти нужное можно было только прочитав всё.
   Колонки газетные (multicol), а не сетка: разделы заполняют столбец СВЕРХУ
   ВНИЗ и переходят в соседний, а сетке пришлось бы заранее знать, сколько
   разделов в столбце, -- это зависит от их высоты. Ширина колонки -- 260px, но
   не больше четырёх: в окне слота выйдет две, на телефоне одна. */
/* КОЛОНКИ ПАСПОРТА -- СЕТКА, А НЕ ГАЗЕТНЫЕ КОЛОНКИ (2026-08-20). Было
   `columns: 260px 4`, и браузер сам решал, где чему быть: прайм оказывался под
   окрасом, а «Параметры» стояли отдельной колонкой в три строки. Теперь колонка
   -- свой контейнер, а раздел кладётся в неё по имени. Пустые колонки скрипт
   выбрасывает, чтобы в ряду не оставалось дырки.

   Колонок четыре: состояние с рационом, мутации, прайм, окрас с параметрами
   (окрас и прайм поменялись местами 2026-08-21, «Параметры» уехали за окрасом --
   тема, узор и вариация шкуры это его же числа). Рацион ненадолго уезжал в
   пятую колонку, но паспорт от этого только расползался вширь: высоту первого
   столбца сняли иначе -- строка состояния стала такой же низкой, как ячейка
   мутации.

   auto-fit -- ради узкого экрана: колонок становится меньше по мере сужения, и
   отдельная медиазапросная раскладка для этого не нужна. */
.sheet-cols {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 20px; align-items: start;
}
.sheet-lane { min-width: 0; }
.sheet-block { margin: 0 0 16px; }

/* Узкий экран: колонок две, и раскладывать разделы по именам там нечего --
   «правый столбец» кончился вместе с четырьмя колонками. Сетка на двух колонках
   оставляла дыру под коротким столбцом (ряд высотой с самый длинный), поэтому
   ниже 900 точек это снова газетные колонки: `display: contents` у дорожки
   отдаёт разделы прямо в поток, и они перетекают без дыр. */
@media (max-width: 900px) {
  .sheet-cols { display: block; columns: 260px 2; column-gap: 20px; }
  .sheet-lane { display: contents; }
  .sheet-block { break-inside: avoid; }
}
/* Отбивку между разделами держит сам блок, а не отступ заголовка: заголовок с
   верхним отступом уводил вниз тот раздел, который начинал столбец, и вершины
   столбцов переставали совпадать. */
.sheet-block > .sheet-section { margin-top: 0; }

/* Внутри раздела -- один столбец: делить его дальше нечем и незачем. Отступ у
   строк только снизу: у полосы есть свой верхний (.bar {margin: 7px 0}), из-за
   которого первая строка раздела опускалась ниже соседних -- ровно на эти 7px. */
.sheet-bars, .zone-grid, .mut-grid, .prime-grid { display: grid; gap: 6px; }
.sheet-bars > * { margin: 0; }

/* СТОЛБЕЦ СОСТОЯНИЯ -- ТАКИЕ ЖЕ ЯЧЕЙКИ, КАК У МУТАЦИЙ (2026-08-21). Мутации,
   окрас и прайм в паспорте -- ячейки с рамкой и цветной риской слева, а
   состояние с рационом оставались голыми полосами в столбик: рядом колонка
   выглядела черновиком, а не разделом. Рамка, отступы и скругление взяты у
   `.mut-grid .mut-cell` один в один, риска -- в цвет самого показателя (его
   отдаёт скрипт в `--c`: какого цвета здоровье, знает только он).

   СТРОКА ОДНА, как у слота мутации: подпись слева, число справа. В две строки
   (подпись с числом сверху, дорожка снизу) ячейка выходила вдвое выше соседних,
   и столбец состояния с рационом тянулся на четырнадцать таких -- вдвое длиннее
   любой другой колонки. Дорожка легла НА нижнюю кромку ячейки, поверх рамки:
   места в строке она не занимает, и высота выходит ровно та же, что у ячейки
   мутации, -- отступы у них теперь одни и те же.

   `display: contents` у подписи отдаёт название и число прямо в ряд ячейки --
   иначе они остались бы одним блоком и число не встало бы к правому краю. */
.sheet-bars .bar {
  position: relative; overflow: hidden;
  display: flex; align-items: baseline; gap: 10px;
  background: #05090c; border: 1px solid var(--line);
  border-left: 2px solid var(--c, var(--accent-dim));
  border-radius: 7px; padding: 4px 9px;
}
.sheet-bars .bar-label { display: contents; }
.sheet-bars .bar-label > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-bars .bar-label b { margin-left: auto; flex: 0 0 auto; font-size: 12px; }
.sheet-bars .bar-track {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; border: 0; border-radius: 0;
}
.sheet-bars .bar-fill { border-radius: 0; }
/* Ноль гаснет так же, как пустой слот мутации: у рациона нулевых строк бывает
   половина раздела, и пустые дорожки подряд читались как несработавший раздел,
   а не как «этого он не ел». */
.sheet-bars .bar-zero { border-left-color: var(--line); opacity: .55; }
.sheet-bars .bar-zero .bar-label b { color: var(--muted); }

/* Квадратик без подписи ничего не сообщает -- зона и hex идут рядом с цветом. */
.zone-chip {
  display: flex; align-items: center; gap: 8px;
  background: #05090c; border: 1px solid var(--line);
  border-radius: 6px; padding: 6px 9px; min-width: 0;
}
.zone-chip-name { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zone-chip-hex { font-family: var(--mono); font-size: 11px; color: var(--muted); }

/* Мутации ячейками: важно не только ЧТО за мутация, но и в каком она слоте. */
.mut-cell {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
  background: #05090c; border: 1px solid var(--line);
  border-left: 2px solid var(--accent-dim);
  border-radius: 8px; padding: 6px 10px;
}
/* В паспорте слот -- ОДНА строка: имя слота слева, мутация справа. В две строки
   шестнадцать слотов вытягивали колонку вдвое выше соседних, а читать там всё
   равно нечего -- у двенадцати из них написано «пусто». В выборе мутаций
   (`.mut-pick`) ячейка своя и этой правки не касается. */
.mut-grid { gap: 4px; }
.mut-grid .mut-cell {
  flex-direction: row; align-items: baseline; gap: 10px;
  padding: 4px 9px; border-radius: 7px;
}
.mut-grid .mut-cell-slot { flex: 0 0 auto; }
.mut-grid .mut-cell-value { margin-left: auto; text-align: right; }
.mut-cell.elder { border-left-color: var(--amber); }
/* Пустой слот виден, но не спорит с занятыми: он тут ради полноты картины. */
.mut-cell-empty { border-left-color: var(--line); opacity: .55; }
.mut-cell-empty .mut-cell-value { color: var(--muted); }
.mut-cell-slot {
  font-size: 10px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .08em;
}
.mut-cell-value { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Выбор мутаций: на карточке товара и при активации прайма в профиле. */
.mut-picker { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }

/* Шапка списка: сколько слотов открыто из скольких всего. */
.mut-head {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  margin-bottom: 4px; padding-bottom: 7px; border-bottom: 1px solid var(--line);
}
.mut-head b { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; }
.mut-count {
  font-size: 11px; color: var(--text);
  background: var(--raised-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 2px 9px;
}
.mut-bonus {
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--accent); background: #35e08a14; border: 1px solid #35e08a3d;
  border-radius: 999px; padding: 2px 9px;
}

/* Строка слота: подпись слева фиксированной колонкой (иначе «РОДИТЕЛЬСКИЙ 1»
   переносился на две строки и ряды переставали быть одной высоты), полоска
   слева -- семейство слота, те же цвета, что и в карточке дино. Рамка только у
   .mut-slot: тем же .mut-pick сделан выбор вида в карточке магазина. */
.mut-pick { display: flex; align-items: center; gap: 10px; }
.mut-pick.mut-slot {
  background: #05090c; border: 1px solid var(--line);
  border-left: 2px solid var(--accent-dim);
  border-radius: 9px; padding: 6px 10px;
}
.mut-pick.mut-elder { border-left-color: var(--amber); }
.mut-pick.mut-parent { border-left-color: var(--violet); }
.mut-pick .mut-cell-slot { flex: 0 0 108px; line-height: 1.25; }
.mut-pick select { flex: 1; min-width: 0; }

/* Закрытый слот: виден, но не выбирается. Пунктир и приглушение говорят это
   раньше, чем игрок ткнёт в неактивный список. */
.mut-pick.mut-locked {
  border-style: dashed; border-left-style: solid;
  border-left-color: var(--line-hot); opacity: .62;
}
.mut-pick.mut-locked select { cursor: not-allowed; }
.mut-lock { display: block; font-size: 9px; letter-spacing: .1em; }

/* Черта между купленным и закрытым. */
.mut-locked-note {
  display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
  margin: 9px 0 3px; padding-top: 9px; border-top: 1px dashed var(--line);
  font-size: 12px; color: var(--muted);
}
.mut-locked-note b { color: var(--text); font-weight: 600; }

/* Что делает выбранная мутация -- под её селектом: на телефоне наведения нет. */
.mut-what:not(:empty) { font-size: 11px; line-height: 1.35; padding: 0 0 2px 122px; }

/* Компактные слоты -- в окне сборки дино для лота. Там их сразу шестнадцать, и
   обычными строками они занимали пол-окна: до цены и срока приходилось листать.
   Две колонки, подпись НАД списком (а не отдельной колонкой слева) и меньшие
   отступы дают ту же самую сетку вчетверо ниже. Полоска семейства слота
   остаётся -- по ней «пятый слот» узнаётся везде одинаково. */
.mut-picker.mut-compact {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px;
}
.mut-compact .mut-pick.mut-slot {
  display: block; padding: 3px 7px 4px; border-radius: 9px;
}
.mut-compact .mut-pick .mut-cell-slot {
  display: block; flex: none; font-size: 9px; margin-bottom: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mut-compact .mut-pick select { width: 100%; height: 28px; font-size: 12px; padding: 0 6px; }
/* Пояснение под сеткой (сколько слотов закрыто) -- во всю ширину, не колонкой. */
.mut-compact > .muted, .mut-compact > .mut-locked-note { grid-column: 1 / -1; }

/* Порог ниже общего 560px: окно лота узкое и на широком экране, а два столбца
   селектов по 200px читаются нормально. Одна колонка -- только когда иначе уже
   не помещается название слота. */
@media (max-width: 430px) {
  .mut-picker.mut-compact { grid-template-columns: 1fr; }
}

@media (max-width: 560px) {
  .mut-pick { flex-wrap: wrap; }
  .mut-pick .mut-cell-slot { flex: 1 0 100%; }
  .mut-what:not(:empty) { padding-left: 12px; }
}

/* Справочник мутаций -- СПИСОК, А НЕ ПЛИТКА (2026-08-19). Карточки в три
   колонки показывали описание всегда, и сорок мутаций превращались в экраны
   текста, по которым не пробежаться глазами. Строка показывает только то, по
   чему мутацию ищут: имя игры (английское -- его игрок видит в слотах), перевод
   и эффект; описание и условие раскрываются кликом. Раскрытие -- родной
   <details>, без своего скрипта.

   ГРУППА -- ПАНЕЛЬ, А НЕ СТОЛБИК ОТДЕЛЬНЫХ КОРОБОЧЕК (2026-08-20). Раньше рамка
   была у каждой строки: сорок рамок в четыре столбца рябили так, что границы
   самих групп терялись, а заголовки групп висели прямо на фоновой картинке.
   Теперь рамка одна на группу, строки внутри разделены волосяной линией, а
   выделяется строка заливкой -- при наведении и в раскрытом виде.

   ЦВЕТ У КАЖДОЙ ГРУППЫ СВОЙ и живёт в трёх переменных (--g, --g-soft, --g-line);
   их берут заголовок, значок раскрытия, ярлык эффекта и полоса у описания -- то
   есть цвет задаётся один раз на панель, а не двенадцатью правилами. Голубой --
   общие, красный -- хищникам, зелёный -- травоядным, фиолетовый -- секретные.

   СПИСОК СВЁРНУТ (2026-08-20). Раскрытыми лежат только те группы, которые игрок
   открыл сам: сорок строк разом -- простыня, а вопрос у него обычно к одной
   группе. Свёрнутая группа -- одна строка с названием, счётчиком и пометкой
   «на дино», так что все четыре видны разом и без прокрутки. Раскрытие -- такой
   же <details>, как у строки внутри; поиск раскрывает группы, где что-то нашлось.

   ЧЕТЫРЕ КОЛОНКИ ПО ГРУППАМ. Групп ровно четыре, и вопрос у игрока обычно «что
   есть у моего рациона», а не «что есть вообще»: рядом группы сравнимы, друг под
   другом -- нет. Колонки разной длины (общих два десятка, хищных пяток), и
   `align-items: start` оставляет каждую своей высоты -- растягивать короткую до
   длинной нечем и незачем. */
.mut-book-cols {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px; align-items: start;
}
.mut-group {
  --g: var(--accent); --g-soft: #35e08a12; --g-line: #35e08a33;
  position: relative; overflow: hidden;
  background: linear-gradient(180deg, #0d1419f7 0%, #04070af7 100%);
  border: 1px solid var(--line); border-radius: 12px;
  padding: 12px 12px 8px;
}
.mut-group[data-group="common"]    { --g: var(--cyan);   --g-soft: #46c8f512; --g-line: #46c8f533; }
.mut-group[data-group="carnivore"] { --g: var(--danger); --g-soft: #ff5b5b12; --g-line: #ff5b5b33; }
.mut-group[data-group="herbivore"] { --g: var(--accent); --g-soft: #35e08a12; --g-line: #35e08a33; }
.mut-group[data-group="secret"]    { --g: var(--violet); --g-soft: #a78bfa12; --g-line: #a78bfa33; }
/* Цветная риска по верху панели -- как у .card, только в цвете группы: по ней
   четыре панели различимы боковым зрением, ещё до чтения заголовков. */
.mut-group::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg, var(--g), transparent 78%);
}
/* Заголовок группы -- это <summary>: по нему группа и раскрывается. Значок тот
   же уголок, что у строки, только в цвете группы -- одинаковое движение на обоих
   уровнях, и объяснять его не нужно. */
/* ЗАГОЛОВОК -- РОВНО ДВЕ СТРОКИ, И ВСЕ ЧЕТЫРЕ ПАНЕЛИ ПОЭТОМУ ОДНОЙ ВЫСОТЫ
   (2026-08-20). Метки («ваш рацион», «N на дино») стояли в одной строке с
   названием, и у группы, где их две, строка переносилась: свёрнутая панель
   становилась выше соседних, а ряд -- ступенькой. Теперь строки две всегда:
   сверху название со счётчиком и кнопками, снизу метки и пояснение группы.
   Обе строки однострочные и не переносятся -- высота панели не зависит ни от
   числа меток, ни от длины пояснения. */
.mut-group-head { display: block; cursor: pointer; list-style: none; }
.mut-group-head::-webkit-details-marker { display: none; }
.mut-group-top {
  display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;
  min-width: 0;
}
.mut-group-caret {
  flex: 0 0 auto; font-style: normal;
  font-family: var(--mono); font-size: 15px; line-height: 1;
  color: var(--g); opacity: .75;
  transition: transform .15s ease;
}
.mut-group[open] .mut-group-caret { transform: rotate(90deg); }
.mut-group-head:hover .mut-group-title { color: var(--g); }
/* Вторая строка заголовка. В свёрнутом виде она одна и та же по высоте у всех:
   метки короткие, а пояснение ужимается многоточием (целиком оно в подсказке).
   У раскрытой группы пояснение разворачивается: место есть, а читать его будут
   как раз там. */
.mut-group-sub {
  display: flex; align-items: center; gap: 6px;
  margin: 7px 0 9px; min-width: 0;
  /* Высота строки задана, а не набрана содержимым: ярлык на четыре точки выше
     голого текста, и без этого панель с меткой оказывалась выше соседней. */
  min-height: 20px;
}
.mut-group[open] .mut-group-sub { flex-wrap: wrap; align-items: flex-start; }
.mut-group-note {
  min-width: 0; font-size: 11px; line-height: 1.45; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mut-group[open] .mut-group-note { white-space: normal; overflow: visible; }
/* Название не переносится по слогам: в колонке шириной в четверть экрана
   «ДЛЯ ТРАВОЯДНЫХ» с разрядкой ломалось пополам. Ужиматься ему тоже почти не
   приходится -- в верхней строке, кроме него, только счётчик и две кнопки. */
.mut-group-title {
  margin: 0; min-width: 0; font-size: 12px; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color .15s ease;
}
.mut-group-title::before {
  content: ""; display: inline-block; width: 3px; height: 12px;
  background: var(--g); box-shadow: 0 0 8px var(--g);
  margin-right: 9px; vertical-align: -2px;
}
/* Сколько мутаций в группе. Вопрос «а много ли их вообще» раньше решался
   пересчётом строк глазами -- и по-разному в зависимости от того, ищет игрок
   что-то или нет: счётчик показывает найденное, а не весь список. */
.mut-group-count {
  margin-left: auto; font-size: 10px; padding: 1px 8px; border-radius: 999px;
  color: var(--g); background: var(--g-soft); border: 1px solid var(--g-line);
}
/* «ваш рацион» -- у той группы, которая доступна живому дино игрока. Метка
   зелёная, а не в цвет группы: она про игрока, а не про саму группу. */
.mut-group-mine {
  flex: 0 0 auto;
  font-size: 10px; padding: 1px 8px; border-radius: 999px; white-space: nowrap;
  color: var(--accent); background: #35e08a14; border: 1px solid #35e08a33;
}
.mut-book-list { display: block; }
.mut-book-row { border-top: 1px solid var(--line); }
.mut-book-row:first-child { border-top: 0; }
/* Строка сжата до предела читаемости: отступы в пять пикселей и плотные
   межстрочные -- сорок строк должны помещаться в экран, а не в полтора. */
.mut-book-row > summary {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  padding: 5px 7px; margin: 1px 0; border-radius: 8px;
  cursor: pointer; list-style: none;
}
/* Свой значок вместо браузерного треугольника: тот в разных браузерах разный и
   ломает выравнивание первой строки. Раскрытая строка поворачивает его на 90° --
   движение то же, что у треугольника, и объяснять его не нужно. */
.mut-book-row > summary::-webkit-details-marker { display: none; }
.mut-book-row > summary::before {
  content: "\203A"; flex: 0 0 auto; display: inline-block; width: 10px;
  font-family: var(--mono); font-size: 15px; line-height: 1;
  color: var(--muted); text-align: center;
  transition: transform .15s ease, color .15s ease;
}
.mut-book-row[open] > summary::before { transform: rotate(90deg); color: var(--g); }
.mut-book-row > summary:hover,
.mut-book-row[open] > summary { background: var(--g-soft); }
/* Имя и перевод стоят строками друг под другом: рядом они помещались только на
   всю ширину экрана, в колонке места нет.

   `flex: 1 1 0` -- не украшение: перенос строк flexbox считает по ЖЕЛАЕМОЙ
   ширине элемента, а не по ужатой, и с базой auto длинное имя утаскивало весь
   заголовок на отдельную строку под значком. С нулевой базой заголовок занимает
   то, что осталось от значка и ярлыков. */
.mut-book-title { flex: 1 1 0; min-width: 0; }
.mut-book-name {
  display: block; font-size: 13px; font-weight: 600; line-height: 1.2;
  transition: color .15s ease;
}
.mut-book-ru { display: block; font-size: 11px; line-height: 1.15; color: var(--muted); }
/* Надетая мутация подсвечена именем -- зелёным, тем же, что и ярлык «на дино».
   Правило стоит ДО наведения: специфичность у них одинаковая, выигрывает
   последнее, а под курсором строка должна светиться цветом своей группы. */
.mut-book-row.worn .mut-book-name { color: var(--accent); }
.mut-book-row > summary:hover .mut-book-name { color: var(--g); }
/* Эффект и «на дино» прижаты вправо: слева ищут по имени. */
.mut-book-row > summary > .mut-worn,
.mut-book-row > summary > .mut-eff { margin-left: auto; }
.mut-book-row > summary > .mut-worn + .mut-eff { margin-left: 0; }
.mut-eff {
  flex: 0 0 auto; font-size: 10px; padding: 1px 7px; border-radius: 6px;
  white-space: nowrap;
  color: var(--g); background: var(--g-soft); border: 1px solid var(--g-line);
}
/* У секретных числа эффекта нет вовсе -- игра его не называет, и правый край
   строки пустовал. Бледное «условие» говорит, что под строкой что-то есть. */
.mut-eff-ghost { color: var(--muted); background: transparent; border-color: var(--line); }
.mut-worn {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;
  font-size: 10px; padding: 1px 8px; border-radius: 999px; white-space: nowrap;
  color: var(--accent); background: #35e08a14; border: 1px solid #35e08a33;
}
.mut-worn::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 6px var(--accent);
}
/* Описание -- с цветной полосой слева, а не с пунктиром сверху: полоса привязана
   к строке, под которой стоит, а линия одинаково отделяла и от соседа снизу. */
.mut-book-body {
  margin: 0 7px 8px 13px; padding: 1px 0 4px 11px;
  border-left: 2px solid var(--g-line);
  font-size: 12px; line-height: 1.5; color: var(--muted);
}
.mut-book-how, .mut-book-diet { margin-top: 6px; }
.mut-book-how b { color: var(--text); font-weight: 600; }
#mutationSearch { min-width: 220px; }

/* ПРАВКА СПРАВОЧНИКА (2026-08-20). Админу с правом `content.mutations` каждая
   строка и каждый заголовок группы получают по маленькой кнопке: «✎» правит
   строку, «+» заводит новую в этом столбце. Игроку их нет вовсе -- разметка
   строится без них, а не прячется стилями.

   Кнопки бледные, пока на строку не навели: сорок карандашей в четыре столбца
   перебивали бы сам справочник, а нужны они изредка. */
.mut-edit {
  flex: 0 0 auto; width: 20px; height: 20px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--mono); font-size: 12px; line-height: 1;
  color: var(--muted); background: transparent;
  border: 1px solid var(--line); border-radius: 6px;
  cursor: pointer; opacity: .5;
  transition: opacity .15s ease, color .15s ease, border-color .15s ease;
}
.mut-book-row > summary:hover .mut-edit,
.mut-group-head:hover .mut-edit,
.mut-edit:focus-visible { opacity: 1; }
.mut-edit:hover { color: var(--g); border-color: var(--g-line); opacity: 1; }
/* Строка без эффекта и без ярлыков: карандаш всё равно стоит у правого края. */
.mut-book-row > summary > .mut-edit { margin-left: auto; }
.mut-book-row > summary > .mut-worn + .mut-edit,
.mut-book-row > summary > .mut-eff + .mut-edit { margin-left: 0; }
/* «+» крупнее карандаша: он заводит строку, а не правит, и промахиваться по
   нему в заголовке не должны. */
.mut-add { font-size: 15px; }
/* Флаг «A→Z» -- порядок строк в столбце. Он шире карандаша: внутри не один
   значок, а надпись; высота та же, чтобы кнопки в заголовке стояли в ряд. */
.mut-sort {
  width: auto; padding: 0 6px; font-size: 9px; letter-spacing: .04em;
  /* Заметнее карандаша и «+»: порядок столбца раскладывает себе любой вошедший,
     а не только админ, и прятать кнопку до наведения мыши -- значит не показать
     её вовсе. */
  opacity: .8;
}
/* Включённый флаг виден всегда, а не только под курсором: по нему читают, в
   каком порядке лежит столбец, не наводя мышь. */
.mut-sort.on {
  opacity: 1; color: var(--g);
  background: var(--g-soft); border-color: var(--g-line);
}
.mut-book-empty { padding: 8px 7px 10px; font-size: 11px; line-height: 1.45; }

/* Колонок становится меньше по мере сужения экрана: четыре узкие колонки на
   планшете превращаются в четыре столбика обрывков.

   И это уже не сетка, а газетные колонки. В сетке строка высотой с самую
   длинную группу: «Общих» два десятка, «Хищных» пяток -- под хищниками
   оставалось полэкрана пустоты. Многоколоночная раскладка просто переливает
   группы одну за другой, ничего не выравнивая; `break-inside: avoid` держит
   панель группы целой. */
@media (max-width: 1150px) {
  .mut-book-cols { display: block; columns: 2; column-gap: 14px; }
  .mut-group { break-inside: avoid; margin-bottom: 14px; }
}
@media (max-width: 640px) {
  .mut-book-cols { columns: 1; }
  .mut-book-body { margin-left: 7px; }
}

/* Условия прайма: все десять всегда на виду, выполненные отмечены. Вопрос у
   игрока не «что уже есть», а «чего не хватает до Prime Elder», и ответ на него
   -- как раз невыполненные строки, которых раньше не было видно вовсе. */
.prime-head { display: flex; align-items: center; gap: 9px; }
.prime-count { font-family: var(--mono); letter-spacing: 0; color: var(--text); }
.prime-ready {
  letter-spacing: .06em; color: var(--accent);
  background: #35e08a14; border: 1px solid var(--accent-dim); padding: 1px 8px;
}
.prime-cell {
  display: flex; align-items: baseline; gap: 8px; min-width: 0;
  background: #05090c; border: 1px solid var(--line);
  border-left: 2px solid var(--line); padding: 6px 10px;
}
.prime-cell.on { border-left-color: var(--accent-dim); }
.prime-mark { flex: none; width: 12px; text-align: center; color: var(--muted); }
.prime-cell.on .prime-mark { color: var(--accent); }
.prime-text { font-size: 12px; color: var(--muted); line-height: 1.3; }
.prime-cell.on .prime-text { color: var(--text); }

.kv-grid { display: grid; grid-template-columns: 1fr auto; gap: 5px 14px; align-items: baseline; }
.kv-key { font-size: 12px; color: var(--muted); }
.kv-val { font-size: 12px; }

.code-row { display: flex; align-items: center; gap: 8px; }
.code-row code {
  flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap;
  background: #05090c; border: 1px solid var(--line); border-radius: 8px;
  padding: 7px 10px; font-family: var(--mono); font-size: 11px; color: var(--cyan);
}

/* Свёрнутый блок «Сырые данные» из карточки динозавра убран -- правила .raw-json
   вместе с ним. */
pre {
  background: #05090c; border: 1px solid var(--line); border-radius: 12px;
  padding: 13px; overflow: auto; max-height: 58vh; font-size: 12px; font-family: var(--mono);
}
textarea {
  width: 100%; min-height: 110px; background: #05090c; color: var(--text);
  border: 1px solid var(--line); border-radius: 9px; padding: 11px;
  font-family: var(--mono); font-size: 12px; resize: vertical;
}

/* -------------------------------------------------------- scrollbars ---- */

/* Полоса САМОЙ СТРАНИЦЫ спрятана -- колесо, касание, клавиши и Ctrl+End
   работают как работали. Внутренние ленты полосу оставляют: в списке цветов, в
   дампе JSON или в переписке действительно нужно видеть, где ты находишься. */
html {
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* legacy Edge */
}
html::-webkit-scrollbar,
body::-webkit-scrollbar { width: 0; height: 0; }

/* Правило ОДНО НА ВСЕХ, а не список избранных блоков. Раньше здесь были
   перечислены шесть селекторов, и каждая новая лента (так вышло с чатом)
   получала системную полосу Windows: широкую, светлую, со стрелками по краям --
   самое яркое пятно на тёмной странице.

   Рецепт прежний: дорожка ПРОЗРАЧНАЯ -- сквозь неё виден фон самого блока, и
   полоса не режет его светлой лентой; ползунок отбит от краёв прозрачной рамкой
   через `background-clip: content-box`, иначе он лип бы к самому краю; стрелки
   убраны -- ими не пользуются, а занимают они больше, чем ползунок короткой
   ленты. Блоки, которым полоса не нужна вовсе (боковое меню, ленты категорий и
   вкладок), прячут её своими правилами -- у класса приоритет выше. */
* { scrollbar-width: thin; scrollbar-color: var(--line-hot) transparent; }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line-hot); border-radius: 999px;
  border: 2px solid transparent; background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; }
::-webkit-scrollbar-thumb:active { background: var(--accent-dim); background-clip: content-box; }
::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
::-webkit-scrollbar-corner { background: transparent; }

/* Свежий Chromium умеет и то, и другое -- и СТАНДАРТНОЕ правило у него сильнее:
   пока задан `scrollbar-width`, вся отделка выше (скруглённый ползунок, отклик
   на наведение, убранные стрелки) не применяется, и остаётся просто тонкая
   серая полоса. Поэтому там, где webkit-псевдоэлементы есть, стандартные
   свойства сбрасываем -- рисовать будет разметка выше. Firefox этой проверки не
   проходит (у него таких псевдоэлементов нет) и остаётся на `scrollbar-color`,
   который он единственный и понимает. */
@supports selector(::-webkit-scrollbar) {
  * { scrollbar-width: auto; scrollbar-color: auto; }
  html { scrollbar-width: none; }        /* полоса страницы прячется как прежде */
}

/* Узкий экран: столбец меню съедает половину ширины, поэтому он уезжает за край
   и выдвигается бургером поверх содержимого. Сдвигать содержимое вбок нельзя --
   на телефоне от него ничего не осталось бы. */
/* Пометка «сайт в разработке» стоит по центру шапки только там, где центр
   свободен. Ниже 1200px капсула справа дотягивается до середины, и надпись
   ложилась поверх имени сервера -- absolute её не двигает, она же не в потоке.
   Поэтому на узком экране она возвращается в поток и встаёт после бренда. */
@media (max-width: 1200px) {
  .dev-note { position: static; transform: none; }
}

@media (max-width: 980px) {
  .topbar { gap: 12px; padding: 10px 14px; }
  .burger { display: flex; }

  .sidenav {
    position: fixed; top: var(--topbar-h); left: 0; bottom: var(--foot-h);
    width: 250px; height: auto; flex-basis: 0;
    transform: translateX(-100%); transition: transform .18s ease;
    box-shadow: 0 18px 40px #000a;
  }
  body.menu-open .sidenav { transform: none; }
  /* Затемнение под выдвинутым меню: нажатие мимо закрывает его, и это должно
     быть видно, а не угадываться. */
  body.menu-open .shell::before {
    content: ""; position: fixed; inset: var(--topbar-h) 0 var(--foot-h) 0;
    background: #04070ab3; z-index: 24;
  }
  .tab { padding: 12px 13px; font-size: 15px; }

  /* Подпись под названием и ник съедают всю строку -- на телефоне от них
     отказываемся: логотип и аватар узнаются и без подписей. */
  .brand-sub { display: none; }
  .brand-logo { width: 34px; height: 34px; }
  .user .uname { display: none; }
  .foot-brand span { display: none; }
  /* На телефоне от пометки остаётся один знак: строка целиком выдавливала бы
     кошелёк на вторую строку, а сказать «сайт ещё делается» хватает и значка --
     полный текст остаётся подсказкой на удержание. */
  .dev-note { padding: 5px 9px; }
  .dev-note-text { display: none; }
}

@media (max-width: 560px) {
  main { padding: 14px; }
}

@media (max-width: 620px) {
  /* Совсем узко: пометка «в разработке» уезжает своей строкой (порядок 4 ниже),
     а капсула справа остаётся в строке шапки -- сколько у игрока монет, он
     смотрит чаще всего. */
  .topbar { row-gap: 8px; flex-wrap: wrap; }
  /* На узком экране от кнопки остаётся значок: подпись съедала ширину у
     шапки раздела, над которой кнопка и висит. */
  .head-refresh { right: 10px; padding: 7px 9px; }
  .head-refresh b { display: none; }
  /* Капсула не разрывается на две строки -- кольцо вокруг двух рядов выглядело
     бы порванным. Вместо этого она забирает себе всю ширину следующей строки:
     отсеки остаются в одном ряду, имя сервера читается целиком, а игрок
     прижимается к правому краю, где его и ищут. */
  .head-right { order: 3; width: 100%; margin-left: 0; }
  .head-right > * { flex: 0 0 auto; }
  .user { margin-left: auto; }
  /* Пометка «в разработке» возвращается в поток и уезжает своей строкой: по
     центру абсолютом она ложилась поверх аватара -- на 375px середина шапки уже
     занята. Порядок 4 ставит её последней, ниже пилюль. */
  .dev-note {
    position: static; transform: none; order: 4;
    width: 100%; justify-content: center;
  }
  .dev-note-text { display: inline; }
}

/* Уже 440px даже строка целиком не вмещает все отсеки: первым уходит имя
   сервера. «Живой или нет» -- это цвет точки, он остаётся; имя есть в подсказке
   и во вкладке «Онлайн». Аватар и кошелёк не трогаем: за ними в шапку и
   смотрят. Красная пилюля «только чтение» шире всего остального, поэтому в
   редкий свой день она уезжает горизонтальной прокруткой -- без полосы, но
   пальцем достижимая. */
@media (max-width: 440px) {
  #serverStatusText { display: none; }
  .server-status { padding: 0 11px; }
  .wallet-coin { padding: 0 9px; }
  .head-right { overflow-x: auto; scrollbar-width: none; }
  .head-right::-webkit-scrollbar { display: none; }
}

@media (max-width: 720px) {
  .stat-row { grid-template-columns: 84px 1fr 70px; }
  .mut-row { grid-template-columns: 1fr; }
}

/* --- окно выхода из игры (клавиша H) -------------------------------------
   Полоса поверх страницы: игрок в это время лежит в игре и заглядывает на сайт
   на секунду -- поэтому она видна с любой вкладки и прижата под шапку. */
.logout-bar {
  position: fixed; left: 0; right: 0; top: var(--topbar-h); z-index: 60;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 16px 12px;
  background: linear-gradient(180deg, #2a1a06, #1a1206);
  border-bottom: 1px solid #6b4410;
  box-shadow: 0 8px 24px #0009;
}
.logout-bar.hidden { display: none; }
.logout-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--amber); box-shadow: 0 0 10px var(--amber);
  animation: logout-pulse 1s ease-in-out infinite;
}
@keyframes logout-pulse { 50% { opacity: .25; } }
.logout-text { flex: 1; min-width: 0; color: #f6dfae; font-weight: 600; }
.logout-time { font-size: 18px; color: var(--amber); flex: none; }
.logout-hide { color: var(--muted); border-color: transparent; background: none; font-size: 16px; line-height: 1; }
/* Полоска остатка живёт по нижнему краю: она нужна боковым зрением, а место в
   строке занимать не должна. */
.logout-line {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: #4a3208;
}
.logout-fill { height: 100%; background: var(--amber); transition: width 1s linear; }
/* Кнопка сохранения вне окна выхода: не «сломана», а ждёт своего момента. */
.btn-locked {
  background: none; color: var(--amber); border-color: #6b4410;
  font-weight: 500;
}
.btn-locked:disabled { opacity: .8; }

@media (max-width: 620px) {
  .logout-bar { gap: 8px; padding: 8px 12px 10px; font-size: 13px; }
  .logout-time { font-size: 16px; }
}

/* ----------------------------------------------------------------- чат -- */

/* Значок непрочитанного в строке меню. Он же единственное, из чего игрок узнаёт
   про новое сообщение, пока сидит на другой вкладке, -- поэтому цвет акцентный,
   а не приглушённый, как остальное в столбце. */
.nav-badge {
  margin-left: auto; font-style: normal; font-size: 11px; font-weight: 700;
  min-width: 18px; padding: 1px 5px; border-radius: 999px;
  background: var(--accent); color: #04140c; text-align: center;
}
/* Тот же значок на подписи группы -- сумма её строк, пока группа свёрнута
   (syncGroupBadges). У раскрытой он прячется: число уже стоит в строке, и два
   одинаковых кружка друг над другом читались бы как два разных счёта. Разрядку
   и заглавные подписи группы число не наследует. Уголок за ним встаёт вплотную:
   два auto-отступа делили бы свободное место пополам, и значок уехал бы к
   середине подписи. */
.nav-group-badge { letter-spacing: 0; text-transform: none; line-height: 16px; }
.nav-sect.open .nav-group-badge { display: none; }
.nav-sect:not(.open) .nav-group-badge:not(.hidden) + .nav-caret { margin-left: 0; }

/* Лента и список комнат -- две колонки одной высоты. Высота задана, а не
   отдана содержимому: переписка растёт, и страница, которая тянется вниз на
   каждое сообщение, уводит поле ввода за нижний край экрана. */
.chat-wrap {
  display: grid; grid-template-columns: 250px 1fr; gap: 14px;
  /* Высота -- ровно то, что осталось от экрана: шапка, подвал и заголовок
     раздела. Постоянные «68vh» этого не знали: на узком экране шапка переносится
     в три строки, блок вылезал за нижний край, и поле ввода приходилось искать
     прокруткой страницы. Высоту шапки и подвала меряет сам сайт
     (--topbar-h/--foot-h, measureChrome), остальное -- заголовок раздела и
     отступы. */
  height: clamp(320px,
                calc(100vh - var(--topbar-h, 56px) - var(--foot-h, 44px) - 120px),
                760px);
}
.chat-rooms {
  display: flex; flex-direction: column; gap: 4px; overflow-y: auto;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 8px;
}
.chat-rooms-cap {
  color: var(--muted); font-size: 11px; text-transform: uppercase;
  letter-spacing: .08em; padding: 10px 8px 4px;
}
.chat-rooms-hint { color: var(--muted); font-size: 12px; padding: 10px 8px; }
.chat-room {
  display: grid; grid-template-columns: 1fr auto; gap: 2px 8px;
  background: none; border: 1px solid transparent; border-radius: 9px;
  padding: 7px 9px; cursor: pointer; font: inherit; text-align: left;
  color: var(--text);
}
.chat-room:hover { background: #ffffff08; }
.chat-room.active { background: #16222a; border-color: var(--line-hot); }
.chat-room-name { font-size: 13px; font-weight: 600; }
/* Последняя реплика -- одной строкой: список комнат отвечает на вопрос «где что
   происходит», а не показывает переписку. */
.chat-room-last {
  grid-column: 1 / -1; color: var(--muted); font-size: 11.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chat-room-badge {
  font-style: normal; font-size: 11px; font-weight: 700;
  min-width: 18px; padding: 1px 5px; border-radius: 999px;
  background: var(--accent); color: #04140c; text-align: center;
}

/* min-height: 0 -- не косметика, а условие прокрутки. У элемента flex-колонки
   min-height по умолчанию auto: он НЕ сжимается ниже своего содержимого, лента
   растёт вместе с разговором, вылезает за высоту блока и вниз уезжает страница
   целиком -- вместе с полем ввода. С нулём лента остаётся в своих границах и
   прокручивается внутри. */
.chat-main { display: flex; flex-direction: column; gap: 10px; min-width: 0; min-height: 0; }
.chat-stream {
  flex: 1; min-height: 0;
  overflow-y: auto; display: flex; flex-direction: column; gap: 10px;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px;
}
/* Разговор прижат КНИЗУ, к полю ввода, и уходит вверх по мере того, как его
   ведут: пока сообщений мало, пустота должна быть над лентой, а не под ней --
   иначе три реплики висят под заголовком, а между ними и полем ввода полэкрана
   пустоты. Отступ у первого узла, а не `justify-content: flex-end` у самой
   ленты: при переполнении flex-end прячет верх за границей прокрутки, и до
   начала разговора становится не долистать. */
.chat-stream > :first-child:not(.scroll-jump) { margin-top: auto; }
.chat-stream > .scroll-jump + * { margin-top: auto; }
.chat-empty { margin: auto; color: var(--muted); text-align: center; }
.chat-day {
  align-self: center; color: var(--muted); font-size: 11px;
  letter-spacing: .06em; text-transform: uppercase;
  padding: 2px 10px; border: 1px solid var(--line); border-radius: 999px;
}
.chat-msg {
  max-width: min(78%, 620px); align-self: flex-start;
  background: var(--raised-2); border: 1px solid var(--line);
  border-radius: 12px; padding: 8px 11px 9px;
}
/* Своё -- справа и в цвете акцента: в ленте на полсотни реплик «кто это
   написал» должно читаться раньше подписи. */
.chat-msg.mine {
  align-self: flex-end; background: #12261f; border-color: var(--accent-dim);
}
.chat-msg-head { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
.chat-who {
  background: none; border: none; padding: 0; cursor: pointer; font: inherit;
  font-size: 12.5px; font-weight: 650; color: var(--cyan);
}
.chat-who:disabled { color: var(--accent); cursor: default; }
.chat-who:hover:not(:disabled) { text-decoration: underline; }
.chat-time { color: var(--muted); font-size: 11px; font-family: var(--mono); }
/* Тег клана перед ником: моноширинно и приглушённо, как в игровых чатах. */
.chat-clan { font-family: var(--mono); font-size: 11.5px; color: var(--muted); white-space: nowrap; }
/* Метка подписчика перед ником: значок из подписки в рамке её цвета. */
.chat-badge {
  font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 999px;
  border: 1px solid var(--line-hot); color: var(--text); white-space: nowrap;
  max-width: 12em; overflow: hidden; text-overflow: ellipsis;
}
.chat-badge-sample { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; }
/* Кнопка ежедневных монет подписки -- во всю ширину плитки, как «Применить». */
.daily-claim { width: 100%; }
/* Метка строки, сказанной в игре. Приглушённая рамка, а не заливка: это пометка
   об источнике, а не состояние, за которым надо следить. */
.chat-ingame {
  font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--cyan); border: 1px solid var(--line-hot);
  border-radius: 999px; padding: 0 6px; line-height: 15px;
}
/* Сама строка из игры -- с цветной риской слева: в ленте на полсотни реплик
   метку у подписи видно не сразу. */
.chat-msg.ingame { border-left: 2px solid var(--cyan); }
/* Кнопки управы держатся бледными, пока на сообщение не навели: в ленте их
   столько же, сколько реплик, и в полную силу они читались бы как содержимое. */
.chat-act {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font: inherit; font-size: 13px; line-height: 1; padding: 2px 4px;
  border-radius: 6px; opacity: .35; transition: opacity .14s, color .14s;
}
.chat-msg:hover .chat-act { opacity: 1; }
.chat-act:hover { color: var(--danger); background: #ffffff08; }
/* Перенос строк -- от автора, поэтому pre-wrap; длинное слово рвётся, иначе
   одна «ссылка» на триста символов растягивает всю ленту. */
.chat-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13.5px; }

.chat-note {
  background: #2a1a0b; border: 1px solid #6b4410; border-radius: 9px;
  padding: 8px 12px; color: var(--amber); font-size: 12.5px;
}
.chat-form { display: flex; gap: 10px; align-items: stretch; }
.chat-form textarea {
  flex: 1; resize: vertical; min-height: 48px; max-height: 160px;
  font-size: 13.5px;
}
.chat-form-side {
  display: flex; flex-direction: column; align-items: stretch;
  justify-content: center; gap: 4px; text-align: center;
}

/* Правка комнат в окне настроек: строка на комнату, поля подписаны сверху --
   иначе шесть безымянных полей в ряд читаются как таблица без заголовка. */
.chat-rooms-edit { display: flex; flex-direction: column; gap: 8px; margin: 8px 0; }
.chat-room-edit {
  display: grid; gap: 8px; align-items: end;
  grid-template-columns: minmax(120px, 1.4fr) minmax(90px, 1fr) auto auto auto auto;
  background: var(--raised-2); border: 1px solid var(--line);
  border-radius: 9px; padding: 8px 10px;
}
.chat-room-cell { display: grid; gap: 2px; min-width: 0; }
.chat-room-cell input[type="checkbox"] { justify-self: start; width: 16px; height: 16px; }
.chat-room-edit input, .chat-room-edit select { width: 100%; min-width: 0; }
@media (max-width: 860px) {
  .chat-room-edit { grid-template-columns: 1fr 1fr; }
}

.chat-mutes { display: flex; flex-direction: column; gap: 8px; }
.chat-mute {
  display: grid; grid-template-columns: 1fr auto; gap: 2px 10px;
  align-items: center;
  background: var(--raised-2); border: 1px solid var(--line);
  border-radius: 9px; padding: 8px 11px;
}
.chat-mute-who { font-weight: 600; }
.chat-mute .muted { grid-column: 1; }
.chat-mute .btn { grid-row: 1 / span 2; }

/* Узкий экран: список комнат ложится сверху одной строкой с прокруткой вбок --
   отдавать ему четверть ширины телефона ради подписей незачем. */
@media (max-width: 860px) {
  /* Высота считается той же формулой, что и на широком экране: она уже учитывает
     переносы шапки, и своя высота здесь только мешала бы. */
  .chat-wrap { grid-template-columns: 1fr; }
  .chat-rooms {
    flex-direction: row; overflow-x: auto; overflow-y: hidden;
    flex: none; padding: 6px;
  }
  .chat-rooms-cap, .chat-rooms-hint { display: none; }
  .chat-room { grid-template-columns: auto auto; flex: none; align-items: center; }
  .chat-room-last { display: none; }
  .chat-msg { max-width: 90%; }
}

/* ------------------------------------------------------------------ друзья */
/* Счётчик заявок -- как значок непрочитанного в меню: его ищут глазами, а не
   читают, поэтому цвет, а не подпись. */
.btn-badge {
  font-style: normal; font-family: var(--mono); font-size: 10px;
  margin-left: 7px; padding: 1px 6px; border-radius: 9px;
  background: var(--accent); color: #04120c;
}

#friendsPanel { margin-top: 18px; }
.friend-list { display: grid; gap: 8px; }
/* Первый подзаголовок стопки («Зовут вас») стоит сразу под полосой -- его
   обычный верхний отступ здесь лишний. */
.friend-list .sub-head:first-child { margin-top: 4px; }
.friend-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 9px 13px;
}
.friend-row:hover { border-color: var(--line-hot); }
/* Тот, кто сейчас в игре, отбит зелёной кромкой -- как строка в списке игроков:
   ради этого список друзей и открывают. Свой цвет карточки её перекрывает:
   выбранный игроком цвет важнее нашей подсказки, а «в игре» и так написано
   словами. */
.friend-row.on { border-left: 2px solid var(--accent); }
.friend-face {
  flex: none; width: 38px; height: 38px; border-radius: 10px; overflow: hidden;
  display: grid; place-items: center;
  background: #05090c; border: 1px solid var(--line-hot);
  color: var(--muted); font-family: var(--mono); font-size: 15px;
}
.friend-face img { width: 100%; height: 100%; object-fit: cover; }
.friend-face-big { width: 64px; height: 64px; border-radius: 14px; font-size: 22px; }
.friend-mid { min-width: 0; flex: 1 1 180px; }
.friend-name { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.friend-where, .friend-note { font-size: 12px; margin-top: 3px; }
.friend-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }

.seg-count {
  font-style: normal; font-family: var(--mono); font-size: 10px;
  margin-left: 6px; color: var(--muted);
}
.seg button.on .seg-count { color: inherit; }

/* «Добавить»: две карточки в ряд -- кого зовём и чем позвать себя. Узкий экран
   ставит их друг под друга. */
.friend-add {
  display: grid; gap: 15px; align-items: start;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
}
.friend-ask, .friend-code { display: grid; gap: 10px; align-content: start; }
.friend-ask-form { display: grid; gap: 8px; margin-top: 2px; }
/* Кнопка по содержимому, а не во всю карточку: во всю ширину она читалась как
   полоса, а не как действие. */
.friend-ask-form .btn { justify-self: start; }
.friend-code .ref-ticket { flex: none; }
@media (max-width: 860px) {
  .friend-add { grid-template-columns: minmax(0, 1fr); }
}

.player-card {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 13px;
}

/* Реферальная программа как раздел профиля: своя карточка героя и те же четыре
   колонки, что были на вкладке. Отступ сверху -- как у остальных разделов
   (покупки, история): полоса переключателя не должна липнуть к содержимому. */
#referralPanel { margin-top: 18px; }

/* Ключ Steam Web API на вкладке «Доступ». */
.steam-key { display: grid; gap: 10px; margin-bottom: 12px; }
.steam-key-row { flex-wrap: wrap; }
.steam-key-row input { flex: 1 1 260px; min-width: 0; }

@media (max-width: 720px) {
  .friend-acts { margin-left: 0; width: 100%; }
}

/* ----------------------------------------------------------- колесо фортуны */
/* Колесо одно, по центру вкладки: списка призов справа больше нет (2026-09-14),
   подсказок при наведении тоже (2026-09-15). */
.wheel-wrap { display: grid; justify-items: center; }
.wheel-stage {
  display: grid; justify-items: center; gap: 12px; position: relative;
  width: 100%; max-width: 620px;
}
/* Колесо -- по высоте окна: вместе с шапкой, заголовком раздела, кнопкой и
   подписью под ней оно обязано помещаться без прокрутки. Во всю ширину (620px)
   на ноутбучном экране кнопка уезжала под подвал -- на сезонном дино её закрывало
   наполовину (жалоба пользователя 2026-09-15). `--wheel-reserve` -- сколько высоты
   занято всем, кроме колеса. */
.wheel-svg {
  width: min(100%, 620px, max(280px, calc(100vh - var(--wheel-reserve, 290px))));
  height: auto; display: block;
}
.wheel-full { color: var(--muted); opacity: .8; }
.wheel-free { font-size: 12px; color: var(--accent); text-align: center; }
/* Крутится только ротор -- стрелка и подпись стоят на месте. */
.wheel-rotor { transform-origin: 50% 50%; }
.wheel-hub { fill: var(--bg-deep); stroke: var(--line-hot); stroke-width: 1.5; }
.wheel-label { font-family: var(--mono); font-size: 11px; letter-spacing: .02em; }
/* Стрелка сверху: под неё и подводится выигравший сектор. */
.wheel-pin {
  position: absolute; top: -2px; left: 50%; transform: translateX(-50%);
  width: 0; height: 0; z-index: 2;
  border-left: 10px solid transparent; border-right: 10px solid transparent;
  border-top: 18px solid var(--accent);
  filter: drop-shadow(0 0 6px #35e08a88);
}
/* КНОПКА ПРОКРУТА СТОИТ В СТУПИЦЕ (просьба пользователя 2026-09-17): раньше она
   была под колесом, и взгляд ходил от середины круга вниз и обратно. Круг с
   накладкой -- свой слой (.wheel-disc), потому что «середина» здесь -- середина
   КОЛЕСА, а не всей полосы: под ним ещё строка бесплатных прокрутов и подпись. */
.wheel-disc { position: relative; display: grid; justify-items: center; width: 100%; }
/* Размер задаёт renderWheel: ступица нарисована в единицах viewBox, а колесо
   тянется по ширине окна. */
.wheel-hub-box {
  /* Кегль подписи -- от размера самой ступицы: она тянется вместе с колесом, и
     кнопка на узком экране вдвое меньше, чем на широком. */
  container-type: inline-size;
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: grid; place-items: center; border-radius: 50%; padding: 4px;
  box-sizing: border-box; text-align: center;
}
/* Кнопка КРУГЛАЯ и занимает почти всю ступицу (просьба пользователя
   2026-09-17), а сама ступица убавлена -- зелёным пятном на полколеса кнопка
   была раньше. Подпись в две строки: вширь в круге места нет. */
.wheel-go {
  min-width: 0; width: 100%; height: 100%; border-radius: 50%; padding: 4px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0; line-height: 1.15;
}
.wheel-go-word { font-size: clamp(9px, 13cqw, 15px); }
.wheel-go-cost {
  display: inline-flex; align-items: center; gap: 2px;
  font-size: clamp(8px, 11cqw, 13px);
}
/* Монета в цене -- по строке, а не своим размером: в маленьком круге картинка в
   14 пикселей занимала всю вторую строку. */
.wheel-go-cost .coin-sm { width: 1.05em; height: 1.05em; }
.wheel-go:disabled { opacity: .5; }

/* Что выпало -- кругом ВО ВСЁ КОЛЕСО поверх него. Размер задаёт renderWheel (по
   нынешней ширине svg), нажатие убирает круг, сам он уходит через
   WHEEL_PRIZE_MS. Колесо под ним просвечивает: остановившийся сектор всё ещё
   под стрелкой, и прятать его целиком незачем. */
.wheel-prize-box {
  container-type: inline-size;
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: grid; place-items: center; z-index: 3;
}
.wheel-prize {
  position: relative;
  width: 100%; height: 100%; border-radius: 50%; box-sizing: border-box;
  padding: 12% 14%; cursor: pointer; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; text-align: center;
  background: #040a0ded; border: 2px solid var(--accent-dim);
  box-shadow: 0 0 40px #35e08a3d;
  animation: wheel-pop .22s ease;
}
/* С картинкой круг -- это сама картинка: она заполняет его целиком, а подпись
   лежит поверх неё внизу, на затемнении. Широкая картинка при этом обрезается по
   бокам -- круг есть круг; целиком её видно в настройке колеса. */
.wheel-prize.has-photo { padding: 0; }
.wheel-prize.has-photo img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: 50%;
}
.wheel-prize-text {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.wheel-prize.has-photo .wheel-prize-text {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 18% 10% 9%;
  background: linear-gradient(to top, #030809f2 45%, #03080900);
}
/* Приз не достался (не было места, товара уже нет) -- тот же круг, но тревожный:
   зелёный ободок здесь читался бы как «получилось». */
.wheel-prize.miss { border-color: #f0b42999; box-shadow: 0 0 40px #f0b4293d; }
.wheel-prize b {
  font-size: clamp(16px, 4.4cqw, 30px); line-height: 1.15; max-width: 100%;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.wheel-prize-note { font-size: clamp(11px, 2.2cqw, 15px); color: var(--muted); }
.wheel-prize.miss .wheel-prize-note { color: var(--amber); }
@keyframes wheel-pop {
  from { transform: scale(.82); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
.wheel-note { font-size: 12px; min-height: 16px; }

/* Сезонный дино: под колесом -- плашка «кто сезонный сейчас» (сверху она
   сталкивала кнопку прокрута под подвал), под ней голоса недели, лидер своим цветом. */
.season-now {
  display: flex; align-items: center; gap: 12px;
  width: 100%; max-width: 620px; margin-top: 14px; box-sizing: border-box;
}
.season-now img { width: 56px; height: 56px; object-fit: contain; flex: none; }
.season-now-title { font-weight: 600; font-size: 16px; }
.season-tally { width: 100%; max-width: 620px; margin-top: 14px; box-sizing: border-box; }
.season-tally h3 { margin: 0 0 8px; font-size: 14px; }
.season-tally-row {
  display: flex; justify-content: space-between; gap: 12px; padding: 5px 0;
  border-top: 1px solid rgba(255, 255, 255, .08);
}
.season-tally-row.lead { color: var(--accent); font-weight: 600; }

/* Настройки: строка сектора -- фото, подпись, вид приза, сумма или id, вес и
   кнопки порядка. Поле приза зависит от вида, поэтому лишнее прячется, а не сереет. */
/* Кнопка «+ Сектор» стоит НАД списком и по его левому краю: список бывает в
   двадцать четыре строки, и кнопка под ним пряталась за нижним краем окна. */
.wheel-editor-wrap { display: grid; gap: 7px; }
.wheel-editor { display: grid; gap: 7px; }
.wheel-edit-row {
  display: grid; gap: 6px; align-items: center;
  grid-template-columns:
    auto minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, .8fr) minmax(0, 1.1fr)
    auto auto auto auto;
}
/* Фото сектора: кнопка сама служит предпросмотром, крестик рядом убирает фото. */
.wheel-photo-cell { display: flex; align-items: center; gap: 2px; }
.wheel-photo-pick {
  width: 40px; height: 34px; padding: 0; overflow: hidden;
  display: grid; place-items: center; font-size: 10px;
}
.wheel-photo-pick img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wheel-photo-pick.bad { outline: 1px solid var(--danger, #ff6b6b); }
.wheel-photo-clear { padding: 2px 6px; }
/* «Aa» -- текст на секторе показан; зачёркнутая и тусклая -- убран. */
.wheel-text-pick { padding: 2px 6px; font-size: 11px; }
/* Свой цвет сектора: образец-кнопка; тусклый -- цвет берётся от класса редкости. */
.wheel-color-pick {
  width: 30px; height: 30px; padding: 0; margin-left: 2px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 6px; background: none;
}
.wheel-color-pick.off { opacity: .45; }
.wheel-text-pick.off { opacity: .45; text-decoration: line-through; }
.wheel-edit-row input { width: 100%; min-width: 0; }
.wheel-prize-cell { display: grid; min-width: 0; }
/* Класс редкости в строке настроек -- своим цветом, тем же, что у сектора на
   колесе. Процент рядом -- подписью: его не задают, его показывают. */
/* Окно настроек колеса шире обычного: в строке сектора восемь полей. */
dialog.wide { width: min(1060px, 94vw); }
.wheel-rarity-pick { min-width: 0; }
.wheel-edit-pct {
  font-family: var(--mono); font-size: 11px; text-align: right; min-width: 46px;
}

@media (max-width: 900px) {
  .wheel-edit-row { grid-template-columns: auto minmax(0, 1fr) minmax(0, 1fr) auto; }
}

/* Счёт покупки -- карточкой под счётчиком: слева «сколько × почём», справа итог.
   Числа докручиваются (rollingNumber), поэтому у них своя подсветка. */
.qty-calc {
  margin-top: 10px; padding: 10px 12px; border-radius: 12px;
  background: linear-gradient(180deg, #0a1218cc, #060a0dcc);
  border: 1px solid var(--line); display: grid; gap: 6px;
}
.calc-line {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 15px;
}
.calc-left, .calc-right { display: flex; align-items: center; gap: 6px; }
.calc-line b { font-family: var(--mono); font-size: 16px; }
.calc-sign { color: var(--muted); font-style: normal; font-size: 13px; }
/* Кошелёк первой строкой карточки счёта: было столько, дальше вычитания. */
.calc-purse {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 12px; color: var(--muted);
  padding-bottom: 6px; border-bottom: 1px dashed var(--line);
}
.calc-purse-coin { display: inline-flex; align-items: center; gap: 4px; }
.calc-purse b { color: var(--text); font-size: 13px; }
/* Остаток кошелька -- мелкой строкой под итогом: ответ на «хватит ли». */
.calc-rest {
  display: flex; align-items: center; gap: 5px; justify-content: flex-end;
  font-size: 12px; color: var(--muted);
}
.calc-rest.short { color: var(--danger); }
.calc-rest b { font-family: var(--mono); }
/* Что придёт в кошелёк (обмен) -- своей строкой и цветом валюты. */
.calc-gain { border-top: 1px dashed var(--line); padding-top: 6px; }
.calc-gain b { color: var(--accent); }
.num-roll { display: inline-block; min-width: 1.1em; text-align: right; }
/* Короткая вспышка на смене числа: в мелком шрифте докрутку иначе не видно. */
.num-bump { animation: num-bump .26s ease-out; }
@keyframes num-bump {
  0% { transform: translateY(-2px) scale(1.14); color: var(--accent); }
  100% { transform: none; }
}
/* Быстрый выбор количества: ×10, ×50, ×100 и «Макс» по кошельку. */
.qty-quick {
  display: flex; gap: 6px; margin-top: 8px;
  justify-content: flex-end; flex-wrap: wrap;
}
.qty-chip { padding: 3px 10px; font-size: 12px; }
.qty-chip-max { border-color: var(--accent-dim); color: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .num-bump { animation: none; }
}

/* Каталог: вкладки состояния и отбор по классам над списком. */
.cat-filters { display: grid; gap: 8px; margin: 10px 0 12px; }
.cat-tabs { justify-self: start; }
.cat-classes { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* У значка класса своя полоска цвета типа -- та же, что у строки товара. */
.cat-class { padding: 3px 10px; font-size: 12px; border-left: 3px solid var(--type, transparent); }
.cat-class.on { border-color: var(--accent-dim); color: var(--accent); }
.cat-group { display: inline-flex; align-items: center; gap: 6px; margin-left: auto;
  font-size: 12px; color: var(--muted); }
/* Заголовок группы: тонкая строка с цветом класса, а не карточка. */
.cat-group-head {
  display: flex; align-items: center; gap: 8px; margin: 12px 0 4px;
  padding-left: 10px; border-left: 3px solid var(--type, var(--line-hot));
  font-size: 13px; letter-spacing: .04em; text-transform: uppercase;
}
.cat-group-head b { font-size: 13px; }
.cat-hint { font-size: 12px; margin-bottom: 8px; }
/* Место ручки в отборе остаётся пустым: строки не должны разъезжаться. */
.cat-grip-off { opacity: 0; cursor: default; }

/* Кнопки «в начало» и «в конец» у полосы прокрутки (см. scrollJumpFor в app.js).
   Столбик прижат к правому краю прокручиваемого ящика, стрелки -- к его концам,
   как у самой полосы. */
.scroll-jump {
  position: absolute; top: 0; right: 0; width: 32px;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 6px 3px; pointer-events: none; z-index: 6;
}
/* Кнопки лежат поверх содержимого, поэтому у них свой непрозрачный фон: на
   просвет стрелка терялась в строке под ней. */
.scroll-jump .sj-btn {
  pointer-events: auto; width: 26px; height: 26px;
  background: #0b1116e8; border-color: var(--line);
  backdrop-filter: blur(3px);
}
.scroll-jump .sj-btn svg { width: 15px; height: 15px; }
/* Кнопка своего края не исчезает, а гаснет: пропадающая сдвинула бы вторую. */
.scroll-jump .sj-btn:disabled { opacity: .3; cursor: default; }
.scroll-jump .sj-btn:disabled:hover {
  color: #56707d; background: #0b1116e8; border-color: var(--line);
}

/* Место под столбик -- только пока он на экране (класс вешает placeScrollJump).
   Без места кнопки легли бы на правый край строк: в редакторе колеса там стоит
   «×» (удаление сектора), в настройках -- поля. Короткому списку столбик не
   нужен, и лишний отступ справа там тоже ни к чему. */
#modalBody.has-scroll-jump, #pickerBody.has-scroll-jump { padding-right: 38px; }
.admin-pane.has-scroll-jump { padding-right: 36px; }
.bp-editor.has-scroll-jump { padding-right: 34px; }
.ticket-feed.has-scroll-jump { padding-right: 36px; }
.chat-stream.has-scroll-jump { padding-right: 38px; }

/* На узком экране окно и так впритык: столбик уже, кнопки мельче. */
@media (max-width: 720px) {
  .scroll-jump { width: 28px; padding: 5px 2px; }
  .scroll-jump .sj-btn { width: 24px; height: 24px; }
  #modalBody.has-scroll-jump, #pickerBody.has-scroll-jump,
  .chat-stream.has-scroll-jump { padding-right: 32px; }
  .admin-pane.has-scroll-jump, .bp-editor.has-scroll-jump,
  .ticket-feed.has-scroll-jump { padding-right: 30px; }
}

/* Фото лота аукциона (2026-09-18): во всю ширину карточки над шапкой. Высота --
   16:10 (просьба пользователя: выше, чем была): мутаций на карточке больше
   нет, и место под картинку освободилось. */
.lot-photo {
  margin: -15px -15px 11px; aspect-ratio: 16 / 10; overflow: hidden;
  border-radius: 11px 11px 0 0; background: var(--bg-deep);
}
/* Лоты -- на отступе от вкладок: без него верх первой карточки упирался в них. */
#auctionTabs { margin-bottom: 14px; }
/* Карточка лота -- уже общей плитки (просьба пользователя 2026-09-18, трижды):
   столбец не шире 230px, лишнее место остаётся справа, а не растягивает
   карточку. min(100%, ...) -- чтобы на экране уже 210px она не вылезала за край. */
#auctionBody .cards {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 210px), 230px));
}
.lot-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Выбор фото в окне выставления: превью-кнопкой, как у сектора колеса. */
.lot-photo-field { display: flex; align-items: center; gap: 6px; }
.lot-photo-pick {
  width: 120px; height: 54px; padding: 0; overflow: hidden;
  display: grid; place-items: center; font-size: 12px;
}
.lot-photo-pick img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lot-photo-clear { padding: 4px 9px; }

/* Кнопка-справка: голубая, как метки вида, -- видная, но не спорит с зелёной
   «Сделать ставку» и красной «Снять». */
.btn.btn-info { color: var(--cyan); border-color: #46c8f555; background: #46c8f514; }
.btn.btn-info:hover:not(:disabled) {
  color: #8fe0ff; border-color: #46c8f5aa; background: #46c8f526;
}

/* Кнопки лота: сетка в две равные колонки, сами кнопки мельче обычных -- в
   карточке шириной 230px обычный размер разносил их по трём неровным рядам. */
.auction-lot .card-actions.lot-actions {
  display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
}
.lot-actions .btn {
  width: 100%; min-width: 0; padding: 5px 6px; font-size: 11.5px;
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lot-actions .lot-wide { grid-column: 1 / -1; }
