/* Шрифты подключены отдельным тегом в index.html — так они грузятся параллельно */

/* ---------- Палитра: интерфейс Terraria ---------- */
:root {
  --panel: rgba(43, 54, 117, .95);
  --panel-hi: rgba(53, 67, 140, .95);
  --panel-lo: rgba(33, 42, 92, .96);
  --panel-solid: #2b3675;

  --ink: #ffffff;
  --ink-dim: #c9d2f5;
  --ink-faint: #98a4d6;
  --gold: #ffd166;
  --gold-deep: #e0a72e;

  --field: rgba(14, 19, 46, .92);
  --field-line: #5a67ad;

  --stone: #9aa2c4;
  --stone-dark: #10142e;

  --green: #4c9a3f;
  --green-hi: #7ed957;
  --green-line: #2f6b25;
  --red: #e05a5a;
  --red-line: #8a2b2b;

  --dirt: #7a5334;
  --dirt-dark: #573a24;
  --grass: #4e9c3a;

  --mono: "Cascadia Mono", "JetBrains Mono", Consolas, "Courier New", monospace;
  --ui: "Nunito", "Nunito Fallback", "Segoe UI", system-ui, sans-serif;
  --pixel: "Pixelify Sans", var(--ui);

  /* Тень под текстом — так подписи выглядят в самой игре */
  --shadow-text: 0 2px 0 rgba(0, 0, 0, .55);
  --shadow-title: 0 3px 0 rgba(0, 0, 0, .6);

  /*
   * Темп всей панели. Меняется в одном месте, а в режиме экономии движения
   * обнуляется — поэтому под каждую новую анимацию не нужно дописывать
   * исключение вручную.
   *
   * Правило, по которому здесь всё написано: движение — это transform
   * и opacity. Их браузер отдаёт композитору и не перерисовывает пиксели.
   * Всё, что двигает width, top, box-shadow или background-position,
   * заставляет пересчитывать раскладку или заново красить кадр.
   *
   * Исключение одно и осознанное: filter: brightness на кнопках и карточках.
   * Простой фильтр считается на видеокарте поверх готового слоя. А вот
   * filter: blur на больших областях — уже настоящая работа, его тут нет.
   */
  --t-fast: 110ms;
  --t: 160ms;
  --t-slow: 320ms;
  --ease: cubic-bezier(.22, .61, .36, 1);
  --ease-out-back: cubic-bezier(.2, 1.5, .4, 1);
}

/*
 * Светлая тема — дневная Terraria. Не инверсия ночной: у дневного неба свои
 * цвета, и просто «сделать белым» получилось бы грязно. Земля и трава те же,
 * поэтому панель остаётся узнаваемой.
 */
:root.day {
  --panel: rgba(214, 226, 255, .96);
  --panel-hi: rgba(232, 240, 255, .96);
  --panel-lo: rgba(196, 211, 245, .97);
  --panel-solid: #d6e2ff;

  --ink: #16204a;
  --ink-dim: #33406e;
  --ink-faint: #5d6a99;
  --gold: #b8791a;
  --gold-deep: #8f5c0c;

  --field: rgba(255, 255, 255, .85);
  --field-line: #8e9ecf;

  --stone: #6c78a4;
  --stone-dark: #2b3454;

  --shadow-text: 0 1px 0 rgba(255, 255, 255, .6);
  --shadow-title: 0 2px 0 rgba(255, 255, 255, .55);
}
:root.day body { background: #7fb6e8; }
:root.day .sky { background: url('/scene.svg') center bottom / cover no-repeat, #7fb6e8; filter: brightness(1.45) saturate(.85); }
:root.day .sky::after {
  background:
    radial-gradient(130% 100% at 50% 45%, transparent 40%, rgba(255, 255, 255, .35) 100%),
    linear-gradient(180deg, rgba(255, 255, 255, .3) 0%, rgba(255, 255, 255, .18) 100%);
}
:root.day .sky-clouds::before { filter: brightness(1.6); }
:root.day .console { background: rgba(248, 250, 255, .97); }
:root.day .console-line.out .m { color: #2a3560; }
:root.day .console-line .t { color: #8d97bd; }
:root.day .fireflies { opacity: .35; }

* { box-sizing: border-box; }

/*
 * Доля заполнения объявлена типизированной: браузер понимает, что это число,
 * и умеет плавно вести его между значениями. Без @property такая переменная
 * для него просто текст, и переход шёл бы скачком.
 */
@property --p {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

html {
  /* Плавно — только к якорям; списки и консоль решают за себя сами */
  scroll-behavior: smooth;
  color-scheme: dark;
  /* Место под полосу прокрутки зарезервировано: страница не дёргается, когда она появляется */
  scrollbar-gutter: stable;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--ui);
  font-size: 16px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink);
  background: #0e1030;
}

/*
 * Пока вкладка скрыта, крутить украшения незачем: класс ставит скрипт по
 * событию видимости. Светлячки останавливают себя сами, а вот CSS-анимации
 * на отдельных слоях браузер тормозит не всегда.
 */
body.away *, body.away *::before, body.away *::after { animation-play-state: paused !important; }

::selection { background: var(--gold); color: #241a05; }

/* Фон-сцена: пиксель-арт, растянутый на весь экран */
.sky {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: url('/scene.svg') center bottom / cover no-repeat, #0e1030;
  image-rendering: pixelated;
}
/* Холст светлячков лежит поверх фона, но под содержимым */
.fireflies { position: fixed; inset: 0; z-index: -1; pointer-events: none; }

/*
 * Облака плывут отдельным слоем — в игре они так же неспешно идут по небу.
 * Едет не фон, а сам слой: полоса вдвое шире экрана сдвигается на половину
 * себя и начинает круг заново. Сдвиг слоя композитор делает даром, а вот
 * анимация background-position перерисовывала бы эти 46vh каждый кадр.
 */
.sky-clouds {
  position: fixed; inset: 0 0 auto 0; height: 46vh; z-index: -1; pointer-events: none;
  overflow: hidden;
}
.sky-clouds::before {
  content: ""; position: absolute; inset: 0 auto 0 0;
  width: 200%;
  background: url('/clouds.svg') repeat-x top left;
  background-size: auto 100%;
  image-rendering: pixelated;
  animation: drift 200s linear infinite;
  /*
   * Единственное место, где постоянная подсказка оправдана: слой едет всегда,
   * и держать его отдельным дешевле, чем создавать заново.
   */
  will-change: transform;
}
/* translate3d, а не translateX: так слой остаётся на видеокарте и не
   перерастрируется при изменении размера окна */
@keyframes drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

/* Затемняем ровно настолько, чтобы закат читался, а текст поверх не спорил с ним */
.sky::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(130% 100% at 50% 45%, transparent 35%, rgba(8, 6, 24, .5) 100%),
    linear-gradient(180deg, rgba(10, 10, 34, .42) 0%, rgba(12, 10, 30, .5) 60%, rgba(10, 8, 24, .58) 100%);
}

button, input, select, textarea { font: inherit; color: inherit; }
a { color: var(--gold); }
h1, h2, h3 { margin: 0; font-weight: 800; }

/* Полосы прокрутки в тон интерфейсу */
* { scrollbar-width: thin; scrollbar-color: var(--field-line) rgba(0, 0, 0, .3); }
*::-webkit-scrollbar { width: 12px; height: 12px; }
*::-webkit-scrollbar-track { background: rgba(0, 0, 0, .3); }
*::-webkit-scrollbar-thumb { background: var(--field-line); border: 2px solid var(--stone-dark); }

/* Пиксель-арт не размывать */
.mark, .ic { image-rendering: pixelated; }
.ic { width: 20px; height: 20px; display: block; flex-shrink: 0; }
.ic-sm { width: 18px; height: 18px; }
.ic-lg { width: 28px; height: 28px; }

.mark { width: 38px; height: 38px; display: block; flex-shrink: 0; }
.mark-lg { width: 60px; height: 60px; margin: 0 auto 16px; }
.mark-xl { width: 76px; height: 76px; margin: 0 auto 16px; }

/* ---------- Рамки в духе игровых окон ---------- */

.frame {
  background: linear-gradient(180deg, var(--panel-hi) 0%, var(--panel) 55%, var(--panel-lo) 100%);
  box-shadow:
    0 0 0 3px var(--stone-dark),
    0 0 0 6px var(--stone),
    0 0 0 9px var(--stone-dark),
    0 16px 38px rgba(0, 0, 0, .6);
}

.frame-sm {
  background: linear-gradient(180deg, var(--panel) 0%, var(--panel-lo) 100%);
  box-shadow:
    0 0 0 2px var(--stone-dark),
    0 0 0 4px #6a72a8,
    0 0 0 6px var(--stone-dark),
    0 8px 20px rgba(0, 0, 0, .45);
}

/* Ячейка предмета: чуть светлее фона, с внутренней подсветкой по краю */
.slot {
  background: rgba(255, 255, 255, .09);
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, .16),
    inset 0 3px 0 rgba(0, 0, 0, .28);
}

/* ---------- Каркас ---------- */

.app { max-width: 1180px; margin: 0 auto; padding: 0 26px 80px; }
.hidden { display: none !important; }

/*
 * Смена экрана. Где браузер умеет переходы между состояниями страницы, он
 * делает перекрёстное затухание сам; ниже мы только задаём темп. Где не умеет —
 * работает обычная анимация появления.
 */
#view-list, #view-detail { animation: view-in var(--t) var(--ease); }
@keyframes view-in { from { opacity: 0; } }

::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: var(--t);
  animation-timing-function: var(--ease);
}
/* Заголовок сервера — общий элемент: он переезжает, а не мигает */
.detail-head h2 { view-transition-name: server-title; }

/*
 * Раскрытие блока без замера высоты в JS: строка грида едет от 0fr к 1fr.
 * Так появляется и исчезает полоса генерации мира — раньше она просто
 * выскакивала и сдвигала всё под собой.
 */
.collapse {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows var(--t-slow) var(--ease);
}
.collapse.open { grid-template-rows: 1fr; }
.collapse > * { overflow: hidden; min-height: 0; }

.topbar {
  display: flex; align-items: center; gap: 14px;
  padding: 24px 0 20px;
  margin-bottom: 26px;
  border-bottom: 3px solid var(--stone-dark);
  position: relative;
}
/*
 * Тень под шапкой проявляется по мере прокрутки страницы. Анимацию ведёт сама
 * прокрутка, обработчика в скрипте нет вовсе. Где такие шкалы не поддержаны,
 * тени просто не будет — ломаться нечему.
 */
@supports (animation-timeline: scroll()) {
  .topbar::after {
    content: ""; position: absolute; inset: auto 0 -3px 0; height: 14px;
    background: linear-gradient(180deg, rgba(0, 0, 0, .45), transparent);
    opacity: 0; pointer-events: none;
    animation: topbar-shade linear both;
    animation-timeline: scroll();
    animation-range: 0 90px;
  }
  @keyframes topbar-shade { to { opacity: 1; } }
}
.brand {
  display: flex; align-items: center; gap: 13px;
  background: none; border: 0; padding: 0; cursor: pointer; text-align: left;
}
/* Искры над костром: три точки всплывают и гаснут, каждая со своим ритмом */
.ember-nest { position: relative; display: block; }
.ember {
  position: absolute; bottom: 34px; width: 3px; height: 3px;
  background: var(--gold); opacity: 0; pointer-events: none;
  animation: ember-rise 3.2s linear infinite;
}
.ember:nth-child(2) { left: 14px; animation-delay: 0s; }
.ember:nth-child(3) { left: 25px; animation-delay: 1.1s; animation-duration: 3.8s; }
.ember:nth-child(4) { left: 34px; animation-delay: 2.2s; animation-duration: 2.9s; }
@keyframes ember-rise {
  0% { transform: translateY(0); opacity: 0; }
  15% { opacity: .9; }
  70% { opacity: .5; }
  100% { transform: translateY(-26px) translateX(4px); opacity: 0; }
}

/* Эмблема лежит в ячейке — как предмет в инвентаре */
.brand .mark {
  width: 50px; height: 50px; padding: 6px;
  background: rgba(255, 255, 255, .09);
  box-shadow:
    0 0 0 2px var(--stone-dark), 0 0 0 4px #6a72a8,
    inset 0 0 0 2px rgba(255, 255, 255, .16);
}
.brand-text b {
  display: block;
  font-family: var(--pixel);
  font-size: 24px; line-height: 1.1; letter-spacing: .04em;
  text-shadow: var(--shadow-title);
}
.brand-text > span { font-size: 13.5px; color: var(--ink-faint); }
.topbar-spacer { flex: 1; }

.conn-pill {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 13px;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #4a5290;
  font-size: 14px; font-weight: 600; color: var(--ink-dim);
}
.dot { width: 9px; height: 9px; background: var(--ink-faint); flex-shrink: 0; }
.dot.live { background: var(--green-hi); box-shadow: 0 0 0 3px rgba(126, 217, 87, .25); }
.dot.dead { background: var(--red); }

@media (max-width: 660px) {
  .app { padding: 0 14px 60px; }
  .conn-pill span:last-child { display: none; }
  .btn-label { display: none; }

  /*
   * На телефоне в шапке остаются одни значки, и разная ширина у них бросается
   * в глаза сильнее всего. Приводим к одному квадрату — включая индикатор
   * связи, который без подписи тоже стал значком.
   */
  .topbar .conn-pill,
  .topbar .btn {
    width: 46px; height: 46px;
    padding: 0;
    flex: 0 0 auto;
    justify-content: center;
  }
  .topbar { gap: 10px; }
}

/* ---------- Кнопки ---------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  padding: 11px 18px;
  border: 0; cursor: pointer; white-space: nowrap;
  font-family: var(--ui); font-size: 16px; font-weight: 700; color: var(--ink);
  text-shadow: var(--shadow-text);
  background: linear-gradient(180deg, #4657ab, #2e3a78);
  box-shadow:
    0 0 0 2px var(--stone-dark),
    0 0 0 4px #6a72a8,
    0 0 0 6px var(--stone-dark),
    0 4px 0 6px rgba(0, 0, 0, .35);
  /* Переход только по этим двум свойствам: обводку фокуса тормозить нельзя */
  transition: filter var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
/* Кнопки в игровом меню подрастают под курсором */
.btn:hover:not(:disabled) { filter: brightness(1.2); transform: scale(1.035); }
.btn:active:not(:disabled) { transform: scale(.99) translateY(2px); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
/* Обводка появляется мгновенно: задержка тут вредит, а не украшает */
.btn:focus-visible { outline: 3px solid var(--gold); outline-offset: 7px; }

/* Иконка перезапуска доворачивается при нажатии — маленький отклик на действие */
.btn .ic { transition: transform var(--t) var(--ease); }
.btn[data-action="restart"]:active:not(:disabled) .ic { transform: rotate(90deg); }

/*
 * Ряд кнопок не должен дёргаться, когда «Запустить» сменяется «Остановить»:
 * ширина под самую длинную надпись задана заранее.
 */
.detail-actions > .btn:first-child { min-width: 168px; }
.card-actions > .btn:first-child { min-width: 132px; }

.btn-primary {
  background: linear-gradient(180deg, #6fc056, var(--green));
  box-shadow:
    0 0 0 2px var(--stone-dark), 0 0 0 4px var(--green-hi),
    0 0 0 6px var(--stone-dark), 0 4px 0 6px rgba(0, 0, 0, .35);
}
.btn-danger {
  background: linear-gradient(180deg, #b04747, #7d2f2f);
  box-shadow:
    0 0 0 2px var(--stone-dark), 0 0 0 4px #c76a6a,
    0 0 0 6px var(--stone-dark), 0 4px 0 6px rgba(0, 0, 0, .35);
}
/* Второстепенная кнопка — тоже с рамкой, иначе не читается как кнопка */
.btn-ghost {
  background: linear-gradient(180deg, rgba(255, 255, 255, .1), rgba(255, 255, 255, .03));
  color: var(--ink-dim);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #545c92;
}
.btn-ghost:hover:not(:disabled) { color: var(--ink); }

.btn-sm { padding: 8px 13px; font-size: 14.5px; gap: 7px; }
.btn-block { width: 100%; }
.btn-close { width: 42px; height: 42px; padding: 0; }

/* ---------- Список серверов ---------- */

.section-head { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; flex-wrap: wrap; }
.section-head h2 { font-size: 25px; text-shadow: var(--shadow-title); }
.list-search { flex: 0 1 220px; min-width: 140px; }
.list-sort {
  padding: 8px 12px; border: 0; cursor: pointer;
  background: var(--field); color: var(--ink-dim);
  font-family: var(--ui); font-size: 14px; font-weight: 600;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.list-sort option { background: #1c2450; color: var(--ink); }
@media (max-width: 760px) { .list-search, .list-sort { flex: 1 1 100%; } }

.section-head .count {
  font-size: 14px; font-weight: 800; color: var(--gold);
  padding: 5px 11px; background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark);
}

.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 22px; }

.card {
  padding: 23px 19px 19px; cursor: pointer;
  position: relative; overflow: hidden;
  /* Изменение внутри карточки не заставляет пересчитывать всю страницу */
  contain: layout paint;
  transition: transform var(--t-fast) var(--ease), filter var(--t-fast) var(--ease);
}
.card:hover { transform: translateY(-3px); filter: brightness(1.12); }

/*
 * Появляются только новые карточки, и лесенкой: задержка берётся из --i,
 * который проставляет разметка. Таймеров в JS для этого не нужно, а при
 * обычном обновлении состояния карточка не мигает — класс ей не выдают.
 */
.card.is-new {
  animation: card-in var(--t-slow) var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(14px); }
}

/* Карточки ниже сгиба браузер не рисует, пока до них не долистают */
.card { content-visibility: auto; contain-intrinsic-size: auto 330px; }

/* Полоса генерации в карточке появляется и уходит той же свёрткой, что на странице сервера */
.card-progress > div { padding-bottom: 0; }
.card-progress.open > div { padding-bottom: 0; }

/*
 * Перестановка при смене сортировки. Скрипт замеряет, где карточка была,
 * сдвигает её обратно этим смещением и снимает класс — она доезжает на место
 * сама. Двигается только transform, раскладка при этом уже окончательная.
 */
.card.moving { transition: none; }
.card.settling { transition: transform var(--t-slow) var(--ease); transform: none !important; }

/* Полоса состояния по верхней кромке — видно, что с сервером, ещё до чтения бейджа */
.card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 5px; z-index: 1;
  background-color: var(--state, var(--stone));
  box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .3);
  /* Смена цвета при переходе состояния — глазу заметно, машине бесплатно */
  transition: background-color var(--t) var(--ease);
}
.card[data-state="running"] { --state: var(--green-hi); }
.card[data-state="starting"] { --state: var(--gold); }
.card[data-state="stopping"] { --state: var(--gold-deep); }
.card[data-state="crashed"] { --state: var(--red); }

/*
 * Пока сервер поднимается, по полосе бежит блик — видно боковым зрением.
 * Блик едет через translateX собственным слоем: раньше он двигал
 * background-position, а это перерисовка кадра, и крутилась она вечно
 * на каждой карточке в списке.
 */
.card::after {
  content: ""; position: absolute; inset: 0 auto auto 0; height: 5px; z-index: 2;
  width: 55%;
  background: linear-gradient(90deg,
    transparent 0%, rgba(255, 255, 255, .55) 50%, transparent 100%);
  opacity: 0;
  pointer-events: none;
}
.card[data-state="starting"]::after,
.card[data-state="stopping"]::after {
  opacity: 1;
  animation: bar-sweep 1.6s linear infinite;
}
@keyframes bar-sweep {
  from { transform: translateX(-100%); }
  /* Сдвиг считается от ширины самого блика: 100% / 0.55 ≈ 182% — ровно край карточки */
  to { transform: translateX(182%); }
}

.card-top { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 15px; }
.card-title { font-size: 19px; font-weight: 800; line-height: 1.25; word-break: break-word; text-shadow: var(--shadow-text); }
.card-sub { font-size: 14px; color: var(--ink-faint); margin-top: 4px; }

.badge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; font-size: 12.5px; font-weight: 800;
  letter-spacing: .03em; text-transform: uppercase;
  background: var(--field); color: var(--ink-dim);
  box-shadow: 0 0 0 2px var(--stone-dark);
  white-space: nowrap;
}
.badge::before { content: ""; width: 8px; height: 8px; background: currentColor; }
.badge.running { color: var(--green-hi); }
.badge.running::before { animation: pulse 2s steps(2, end) infinite; }
.badge.starting, .badge.stopping { color: var(--gold); }
.badge.starting::before { animation: pulse 1s steps(2, end) infinite; }
.badge.crashed { color: #ff9c9c; }
@keyframes pulse { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }

.card-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 13px 14px; margin-bottom: 17px; }
.meta-item .k {
  font-size: 12px; font-weight: 700; color: var(--gold-deep);
  text-transform: uppercase; letter-spacing: .06em;
}
.meta-item .v { font-size: 16px; font-weight: 600; margin-top: 3px; }
.meta-item .v.mono { font-family: var(--mono); font-size: 15px; }

.card-actions { display: flex; gap: 11px; flex-wrap: wrap; align-items: center; }

/* Кнопка без подписи — только иконка, для частых мелких действий */
.btn-icon { padding: 8px; }
/* Удаление отодвинуто вправо, чтобы не попасть по нему случайно */
.card-delete { margin-left: auto; }

/*
 * Ряд действий на телефоне.
 *
 * Свободный перенос строк здесь разваливается: кнопки разной ширины, а
 * «удалить» с прижатием вправо улетает на отдельную строку к самому краю и
 * задевает рамкой край карточки. Поэтому на узком экране раскладываем их
 * жёсткой сеткой из шести долей — две подписи по половине сверху, три значка
 * по трети снизу. Получается ровно, и удаление остаётся с краю, но внутри.
 */
@media (max-width: 560px) {
  .card-actions {
    display: grid;
    /* Двенадцать долей делятся и на две, и на четыре — ряды получаются полными */
    grid-template-columns: repeat(12, 1fr);
    /* Рамка кнопки выходит за её границы на 6px, поэтому зазор не меньше 14px */
    gap: 18px 14px;
  }
  /* Значки — по четыре в ряд, подписи — по две */
  .card-actions > .btn { grid-column: span 3; min-width: 0; }
  .card-actions > .btn:nth-child(1),
  .card-actions > .btn:nth-child(2) { grid-column: span 6; }
  /* В сетке место кнопки задано ячейкой — прижимать её вправо больше нечем */
  .card-delete { margin-left: 0; }
}

/* Готовые сиды: набирать их руками без опечатки трудно */
.seed-picks { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 9px; }
.seed-picks .chip {
  padding: 5px 10px; font-size: 12.5px; font-weight: 700; cursor: pointer;
  background: var(--field); color: var(--ink-dim); border: 0;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 3px var(--field-line);
  transition: transform var(--t-fast) var(--ease);
}
.seed-picks .chip:hover { transform: translateY(-1px); }
.seed-picks .chip.on { color: var(--select); box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 3px var(--select); }

/* Подсказка под полем: занят порт или свободен — видно до отправки */
.field-note { margin-top: 7px; font-size: 13px; color: var(--ink-faint); animation: hint-in var(--t) var(--ease) backwards; }
.field-note.bad { color: #ff9c9c; }
.field-note.good { color: var(--green-hi); }

/* Строку консоли можно скопировать одним нажатием */
.console-line { cursor: copy; }
.console-line.copied { animation: copied var(--t-slow) var(--ease); }
@keyframes copied {
  from { transform: scale(1); }
  40% { transform: scale(1.012); }
  to { transform: scale(1); }
}
.console-line.copied .m { color: var(--select); }

/* Кривая памяти: одно число не показывает, ползёт она вверх или стоит */
.spark-row { display: flex; align-items: center; gap: 10px; margin: 4px 0 2px; }
.spark {
  flex: 1 1 auto; height: 26px; min-width: 0;
  background: rgba(6, 9, 26, .55);
  box-shadow: inset 0 0 0 2px var(--stone-dark);
}
.spark polyline { fill: none; stroke: var(--select); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.spark-note { font-size: 12px; color: var(--ink-faint); white-space: nowrap; }

/* Живая сводка мира: часы крупно, события рядом */
.world-now { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.world-clock {
  font-family: var(--pixel); font-size: 34px; line-height: 1;
  color: var(--gold); text-shadow: var(--shadow-title);
}
.world-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.world-tags .chip {
  padding: 5px 11px; font-size: 13px; font-weight: 700;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 3px var(--field-line);
  color: var(--ink-dim);
}
.world-tags .chip.calm { color: var(--green-hi); }
.world-tags .chip.bad { color: #ff9c9c; }

/* Кто сейчас в мире — прямо на карточке, без захода внутрь */
.card-players { display: flex; flex-wrap: wrap; gap: 7px; margin: -6px 0 14px; }
.card-players .chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 3px var(--field-line);
  font-size: 13px; font-weight: 700; color: var(--ink-dim);
  animation: card-in var(--t) var(--ease) backwards;
}
.tag.warn { color: var(--gold); }

/* Полоса генерации мира */
.worldgen { margin-bottom: 15px; }
.worldgen .lbl {
  display: flex; justify-content: space-between; gap: 10px;
  font-size: 14px; font-weight: 700; color: var(--gold); margin-bottom: 7px;
}
.worldgen .lbl .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.progress {
  height: 14px; background: rgba(0, 0, 0, .45);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #4a5290;
  margin: 0 4px;
}
/*
 * Заполнение растягивается масштабом, а не шириной: ширина двигает раскладку
 * и заставляет пересчитывать соседей, масштаб — чистая работа композитора.
 */
.progress span {
  display: block; height: 100%; position: relative; overflow: hidden;
  width: 100%;
  transform: scaleX(var(--p, 0));
  transform-origin: left center;
  background: linear-gradient(180deg, var(--green-hi), var(--green));
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .22);
  transition: transform var(--t-slow) steps(20, end);
}
/*
 * Бегущие полосы: видно, что генерация идёт, даже когда процент стоит на месте.
 * Заполнение сжато масштабом, поэтому полосы внутри пришлось бы растянуть вместе
 * с ним — угол наклона поплыл бы. Внутренний слой отменяет масштаб родителя,
 * а бежит уже его псевдоэлемент: два разных элемента, два разных transform.
 */
.progress i {
  position: absolute; inset: 0; overflow: hidden;
  transform: scaleX(calc(1 / max(var(--p, 1), .02)));
  transform-origin: left center;
}
.progress i::after {
  content: ""; position: absolute; inset: 0 -20px 0 0;
  background: repeating-linear-gradient(115deg,
    rgba(255, 255, 255, .18) 0 8px, transparent 8px 18px);
  animation: crawl 1.1s linear infinite;
}
@keyframes crawl { to { transform: translateX(18px); } }

/* Пустой экран */
.empty { padding: 46px 26px; text-align: center; color: var(--ink-dim); }
.empty h3 { font-size: 23px; color: var(--ink); margin-bottom: 12px; text-shadow: var(--shadow-title); }
.empty > p { margin: 0 auto 28px; max-width: 500px; line-height: 1.65; font-size: 16px; }

.steps {
  list-style: none; counter-reset: step;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 15px; margin: 0 auto 30px; padding: 0; max-width: 780px; text-align: left;
}
.steps li {
  counter-increment: step;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #4a5290;
  padding: 16px 17px 16px 54px; position: relative;
}
.steps li::before {
  content: counter(step);
  position: absolute; left: 15px; top: 15px;
  width: 27px; height: 27px; display: grid; place-items: center;
  background: linear-gradient(180deg, #ffe08a, var(--gold-deep)); color: #3a2a06;
  font-size: 15px; font-weight: 800;
  box-shadow: 0 0 0 2px var(--stone-dark);
}
.steps b { display: block; font-size: 16px; color: var(--gold); margin-bottom: 5px; }
.steps span { font-size: 14px; color: var(--ink-dim); line-height: 1.55; }

/* ---------- Уведомления ---------- */

.notice {
  display: flex; gap: 13px; align-items: flex-start;
  padding: 15px 17px; margin-bottom: 17px;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold-deep);
  font-size: 15px; line-height: 1.6;
}
.notice .ico { flex-shrink: 0; margin-top: 2px; }
.notice-body { flex: 1; min-width: 0; }
.notice code {
  font-family: var(--mono); font-size: 13.5px;
  background: rgba(0, 0, 0, .4); padding: 2px 6px; word-break: break-all;
}
.notice.info { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #4a86c8; }
.notice.bad { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #c76a6a; }
.notice-actions { display: flex; gap: 11px; margin-top: 14px; flex-wrap: wrap; }

/* ---------- Страница сервера ---------- */

.detail-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
.detail-head h2 {
  font-size: 29px; display: flex; align-items: center; gap: 13px; flex-wrap: wrap;
  text-shadow: var(--shadow-title);
}
.detail-head .sub { color: var(--ink-faint); font-size: 15px; margin-top: 9px; }
.detail-actions { display: flex; gap: 11px; margin-left: auto; flex-wrap: wrap; }

/*
 * Страница сервера.
 *
 * Слева консоль, справа стопка карточек. Беда была в пропорциях: консоль
 * фиксированной высоты кончалась через треть экрана, а справа тянулась узкая
 * колонка из десятка панелей — под консолью зияла пустота высотой в два
 * экрана. Издалека страница выглядела разъехавшейся.
 *
 * Лечим двумя правилами. Консоль растягивается на высоту экрана и остаётся
 * на месте, пока листаешь правую часть, — она главное на этой странице.
 * А на широком мониторе правая стопка раскладывается в две колонки, и
 * страница становится вдвое короче и вдвое ровнее.
 */
.detail-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 350px;
  gap: 22px;
  align-items: start;
}

.detail-grid > .panel:first-child {
  position: sticky;
  top: 16px;
  /* Высота экрана минус отступы сверху и снизу */
  max-height: calc(100vh - 32px);
  display: flex;
  flex-direction: column;
}
/* Тянется именно окно вывода, а шапка, фильтры и поле ввода остаются как есть */
.detail-grid > .panel:first-child .console-wrap { flex: 1 1 auto; min-height: 220px; }
.detail-grid > .panel:first-child .console { height: 100%; }

/*
 * Порог в 1380 подобран по остатку: правой части нужно 728, зазору 22, полям
 * страницы около 70 — консоли остаётся под 560, и это ещё читаемая ширина.
 * Ниже порога две колонки справа зажали бы консоль до бесполезной щели.
 */
@media (min-width: 1380px) {
  .detail-grid { grid-template-columns: minmax(0, 1fr) 728px; }
  /*
   * Колонки, а не сетка: у карточек разная высота, и сетка оставляла бы под
   * короткими дыры. Колонки укладывают их встык, а запрет разрыва не даёт
   * панели разъехаться между колонками.
   */
  /* Через .detail-grid — иначе базовое правило .side-stack ниже по файлу
     перебивает display и колонки не включаются */
  .detail-grid > .side-stack { display: block; columns: 2; column-gap: 22px; }
  .detail-grid > .side-stack > * { break-inside: avoid; margin-bottom: 22px; }
}

@media (max-width: 1000px) {
  .detail-grid { grid-template-columns: 1fr; }
  /* В одну колонку липкая консоль только мешала бы: она заняла бы весь экран */
  .detail-grid > .panel:first-child { position: static; max-height: none; display: block; }
  .detail-grid > .panel:first-child .console { height: 420px; }
}
.side-stack { display: flex; flex-direction: column; gap: 22px; }

/* Правки внутри панели не тянут за собой пересчёт всей страницы */
.panel { overflow: hidden; contain: layout paint; }
/* Заголовок панели — земляной блок с травой, у которой пиксельный неровный край */
.panel-head {
  padding: 20px 17px 13px;
  display: flex; align-items: center; gap: 10px;
  font-size: 15px; font-weight: 800; color: #ffe9b0;
  text-shadow: var(--shadow-text);
  background:
    repeating-linear-gradient(90deg, var(--grass) 0 6px, var(--dirt) 6px 12px) 0 9px / 100% 3px no-repeat,
    linear-gradient(180deg, #6ec25a 0 3px, var(--grass) 3px 9px, transparent 9px),
    linear-gradient(180deg, var(--dirt), var(--dirt-dark));
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .35);
}
.panel-head .spacer { flex: 1; }
.panel-body { padding: 17px; }

/* Панель инструментов консоли: поиск, фильтр, кнопки */
.console-tools {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 11px 13px;
  background: rgba(0, 0, 0, .22);
  box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .3);
}
.console-search {
  flex: 1; min-width: 150px; padding: 8px 11px;
  border: 0; background: var(--field); color: var(--ink);
  font-family: var(--ui); font-size: 14px;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.console-search:focus { outline: none; box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }
.console-count { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }

/* Переключатели фильтра — маленькие «чипы» */
.chip {
  padding: 6px 11px; font-size: 13px; font-weight: 700; cursor: pointer;
  background: var(--field); color: var(--ink-faint); border: 0;
  box-shadow: 0 0 0 2px var(--stone-dark);
  font-family: var(--ui);
}
.chip { position: relative; }
.chip:hover { color: var(--ink); }
.chip.on { color: #10142e; background: var(--gold); }
/*
 * Подчёркивание выбранного фильтра растёт масштабом из центра. Единой едущей
 * полоской это было бы красивее, но чипы переносятся на вторую строку —
 * пришлось бы каждый раз мерить их положение в JS.
 */
.chip::after {
  content: ""; position: absolute; inset: auto 0 -3px 0; height: 3px;
  background: var(--gold);
  transform: scaleX(0); transform-origin: center;
  transition: transform var(--t) var(--ease);
}
.chip.on::after { transform: scaleX(1); }

/* Консоль: моноширинная — логи должны выравниваться */
.console-wrap { position: relative; }
.console mark { background: var(--gold); color: #241a05; border-radius: 2px; }
.console-line.hidden-by-filter { display: none; }
.console.no-time .console-line .t { display: none; }
.console {
  height: 450px; overflow-y: auto; padding: 13px 15px;
  /* Высота строки в пикселях, а не множителем: тогда запасной размер ниже точен */
  font-family: var(--mono); font-size: 13px; line-height: 23px;
  background: rgba(6, 9, 26, .96);
  box-shadow: inset 0 0 0 2px var(--stone-dark);
  /* Прокрутка к новым строкам — делом браузера, а не покадровым циклом в JS */
  scroll-behavior: smooth;
  /* Докрутив консоль до конца, не утаскиваем за собой всю страницу */
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
}
.console-line {
  display: flex; gap: 11px; white-space: pre-wrap; word-break: break-word;
  /*
   * Строк в консоли до пятисот. Всё, что за краем окна, браузер не рисует
   * вовсе; запасная высота нужна, чтобы полоса прокрутки не прыгала.
   */
  content-visibility: auto;
  contain-intrinsic-size: auto 23px;
  /* Прокрутка к строке не прижимает её вплотную к краю */
  scroll-margin: 8px;
}
/* Страж в самом низу: по нему наблюдатель понимает, что мы доехали до конца */
.console-end { height: 1px; }
/* Свежая строка коротко подсвечивается — в потоке логов сразу видно, что прилетело */
.console-line.fresh { animation: line-flash 1.1s ease-out; }
@keyframes line-flash {
  0% { background: rgba(255, 209, 102, .22); }
  100% { background: transparent; }
}
.console-line .t { color: #5b64a0; flex-shrink: 0; }
.console-line.system .m { color: #8ec2ff; }
.console-line.error .m { color: #ff9c9c; }
.console-line.input .m { color: var(--gold); }
.console-line.out .m { color: #dde3ff; }
.console-line.join .m { color: var(--green-hi); }
.console-line.leave .m { color: #f0a0c8; }
.console-line.chat .m { color: #ffe9a8; }

/*
 * Кнопка не исчезает через display: none, а уезжает прозрачностью. Скрытую
 * убираем из проверки попаданий курсора через visibility — иначе она
 * продолжала бы ловить клики поверх консоли.
 */
.jump-latest {
  position: absolute; right: 18px; bottom: 16px;
  padding: 8px 14px; font-size: 14px; font-weight: 700; font-family: var(--ui);
  background: linear-gradient(180deg, #4657ab, #2e3a78); color: var(--ink);
  border: 0; cursor: pointer; text-shadow: var(--shadow-text);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #6a72a8;
  transition: opacity var(--t) var(--ease), transform var(--t) var(--ease), visibility var(--t);
}
.jump-latest.away {
  opacity: 0; visibility: hidden; transform: translateY(8px);
}

.console-input {
  display: flex; gap: 11px; padding: 13px;
  background: rgba(0, 0, 0, .28);
  box-shadow: inset 0 3px 0 rgba(0, 0, 0, .3);
}
.console-input input {
  flex: 1; min-width: 0; padding: 11px 13px;
  border: 0; background: var(--field); color: var(--ink);
  font-family: var(--mono); font-size: 14px;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.console-input input:focus { outline: none; box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }

/* Игроки */
.player-list { display: flex; flex-direction: column; gap: 10px; }
.player {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; font-size: 16px; font-weight: 600;
  background: var(--field); box-shadow: 0 0 0 2px var(--stone-dark);
}
.player .av {
  width: 32px; height: 32px; display: grid; place-items: center;
  font-size: 15px; font-weight: 800; color: #10142e; flex-shrink: 0;
  box-shadow: 0 0 0 2px var(--stone-dark), inset 0 0 0 2px rgba(255, 255, 255, .25);
}
.player .nm { flex: 1; word-break: break-word; }
/*
 * Только что зашедший игрок появляется с подскоком. backwards нужен, чтобы
 * первый кадр применился сразу: иначе строка успевает мигнуть в полный рост
 * перед стартом анимации.
 */
.player.is-new { animation: player-in .38s var(--ease-out-back) backwards; }
@keyframes player-in {
  from { transform: scale(.7); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

/* Подключение */
.connect-item { margin-bottom: 13px; }
.connect-item:last-of-type { margin-bottom: 19px; }
.connect-row {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 13px;
  background: var(--field); box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
  font-family: var(--mono); font-size: 14.5px;
}
.connect-row .addr { flex: 1; word-break: break-all; }
.connect-item .iface {
  font-size: 13px; color: var(--ink-faint); margin: 8px 0 0 6px; font-family: var(--ui);
}

.kv {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 10px 0; font-size: 15px;
  border-bottom: 2px solid rgba(0, 0, 0, .28);
}
.kv:last-child { border-bottom: 0; }
.kv .k { color: var(--ink-faint); flex-shrink: 0; }
.kv .v { text-align: right; word-break: break-word; font-weight: 600; }
.kv .v.mono { font-family: var(--mono); font-size: 14px; }

.hint-block { font-size: 14px; color: var(--ink-faint); line-height: 1.65; margin: 15px 0 0; }

/* Мелкие служебные роли — чтобы не латать разметку инлайновыми отступами */
.back-link { margin-bottom: 12px; }
.hint-lead { margin: 0 0 14px; font-size: 13.5px; color: var(--ink-faint); line-height: 1.6; }
.stack-top { margin-top: 12px; }
.flush { margin: 0; }
.warn-text { color: #ffbdbd; }

.file-row {
  display: flex; align-items: center; gap: 11px; padding: 11px 0; font-size: 15px;
  border-bottom: 2px solid rgba(0, 0, 0, .28);
  position: relative;
  content-visibility: auto;
  contain-intrinsic-size: auto 45px;
}
.file-row:last-child { border-bottom: 0; }
/*
 * Подсветка строки под курсором — прозрачностью подложки, а не перекраской
 * фона самой строки: текст поверх при этом не перерисовывается.
 */
.file-row::before {
  content: ""; position: absolute; inset: 0 -9px;
  background: rgba(255, 255, 255, .06);
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-fast) var(--ease);
}
.file-row:hover::before { opacity: 1; }
.file-row .nm { flex: 1; font-family: var(--mono); font-size: 13.5px; word-break: break-all; }
.file-row .sz { color: var(--ink-faint); font-size: 13.5px; }

.muted { color: var(--ink-faint); font-size: 15px; }

/*
 * Заглушки на время загрузки. Анимация одна на всех, ступенька задаётся
 * задержкой — так браузер держит один набор ключевых кадров, а не по своему
 * на каждый прямоугольник.
 */
.skeleton { display: block; height: 15px; margin: 13px 0; background: rgba(255, 255, 255, .09); }
.skeleton:nth-child(2) { width: 78%; }
.skeleton:nth-child(3) { width: 91%; }
.skeleton:nth-child(4) { width: 64%; }
.skeleton { animation: sk-fade 1.4s var(--ease) infinite; }
.skeleton:nth-child(2) { animation-delay: .12s; }
.skeleton:nth-child(3) { animation-delay: .24s; }
.skeleton:nth-child(4) { animation-delay: .36s; }
@keyframes sk-fade { 0%, 100% { opacity: .35; } 50% { opacity: .8; } }

/*
 * Цифра сменилась — короткий подскок, чтобы глаз это поймал. Вспышка цветом
 * читалась бы ярче, но перекраска текста — это перерисовка; масштаб и
 * прозрачность обходятся даром. Строчному элементу нужен inline-block,
 * иначе transform к нему не применяется вовсе.
 */
.flash { display: inline-block; animation: value-flash .5s var(--ease); }
@keyframes value-flash {
  from { opacity: .3; transform: scale(1.12); }
}

/* Пароль показывается только по нажатию — незачем светить его на весь экран */
.secret { letter-spacing: .12em; }

.say-row { display: flex; gap: 9px; margin-bottom: 13px; }
.say-row input {
  flex: 1; min-width: 0; padding: 10px 12px; border: 0;
  background: var(--field); color: var(--ink);
  font-family: var(--ui); font-size: 15px;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.say-row input:focus { outline: none; box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }
.say-row input:disabled { opacity: .5; }

/* QR-код адреса — для тех, кто заходит с телефона */
.qr-box { display: grid; place-items: center; padding: 20px; }
.qr-box canvas, .qr-box svg { image-rendering: pixelated; box-shadow: 0 0 0 8px #fff; }

/* ---------- Модальные окна ---------- */

/*
 * Окно — нативный <dialog>. Браузер сам кладёт его в верхний слой (никаких
 * состязаний z-index), сам ловит фокус внутри и сам закрывает по Escape.
 *
 * Подложка гасится только прозрачностью. Размытие фона (backdrop-filter)
 * выглядит красиво и стоит дорого — каждый кадр пересчитывается весь экран
 * под окном, поэтому его здесь нет и не будет.
 */
dialog.sheet {
  border: 0; padding: 32px; margin: auto;
  max-width: none; max-height: none; width: 100%; height: 100%;
  background: none; color: inherit; overflow: auto;
  display: grid; place-items: center;
}
dialog.sheet::backdrop {
  background: rgba(4, 6, 20, .74);
  animation: fade var(--t) var(--ease);
}
dialog.sheet[open] { animation: fade var(--t) var(--ease); }
dialog.sheet.closing, dialog.sheet.closing::backdrop {
  animation: fade var(--t-fast) var(--ease) reverse forwards;
}
@keyframes fade { from { opacity: 0; } }

/*
 * Всплывающая справка. Popover — тоже верхний слой, но без модальности:
 * его не нужно закрывать явно, браузер сам уберёт по клику мимо или по Escape.
 * allow-discrete позволяет анимировать сам факт появления, без подпорок
 * с visibility; где этого нет, окно просто появится сразу.
 */
[popover].tip {
  border: 0; padding: 20px 22px; margin: auto;
  max-width: 460px; color: var(--ink); font-size: 14.5px; line-height: 1.65;
  opacity: 0; transform: scale(.97);
  transition: opacity var(--t) var(--ease), transform var(--t) var(--ease), overlay var(--t) allow-discrete, display var(--t) allow-discrete;
}
[popover].tip:popover-open { opacity: 1; transform: scale(1); }
@starting-style { [popover].tip:popover-open { opacity: 0; transform: scale(.97); } }
[popover].tip::backdrop { background: rgba(4, 6, 20, .5); }
.tip h4 { margin: 0 0 10px; font-size: 15px; color: var(--gold); }
.tip p { margin: 0 0 10px; }
.tip p:last-child { margin-bottom: 0; }

.tip-btn { width: 30px; height: 30px; padding: 0; font-size: 15px; }

/*
 * Кнопка в работе: подпись не меняется, поверх ложится крутилка. Ширина
 * остаётся прежней, и ряд кнопок не прыгает в момент нажатия.
 */
.btn.busy { position: relative; color: transparent !important; pointer-events: none; }
.btn.busy .ic { opacity: 0; }
.btn.busy::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 16px; height: 16px;
  background: var(--ink);
  clip-path: polygon(0 0, 100% 0, 100% 25%, 0 25%);
  animation: spin .8s steps(8, end) infinite;
}

.modal {
  width: 100%; max-width: 640px; max-height: 86vh; overflow-y: auto;
  animation: modal-in var(--t) var(--ease);
}
@keyframes modal-in {
  from { opacity: 0; transform: scale(.97); }
}

/* Закрытие проигрывает то же самое задом наперёд, а не обрывает окно на полуслове */
.modal-root.closing .modal { animation: modal-in var(--t-fast) var(--ease) reverse forwards; }

/*
 * will-change вешаем только на время проигрыша и снимаем после. Постоянный
 * will-change держит отдельный слой в памяти под каждым таким элементом.
 */
.modal-root.closing .modal, .modal-root.opening .modal { will-change: transform, opacity; }

/* Заголовок-табличка: земляной блок с травой сверху */
.modal-head {
  position: sticky; top: 0; z-index: 2;
  padding: 26px 20px 17px;
  display: flex; align-items: center; gap: 13px;
  background:
    repeating-linear-gradient(90deg, var(--grass) 0 8px, var(--dirt) 8px 16px) 0 11px / 100% 4px no-repeat,
    linear-gradient(180deg, #6ec25a 0 4px, var(--grass) 4px 11px, transparent 11px),
    linear-gradient(180deg, var(--dirt) 0%, var(--dirt-dark) 100%);
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .4);
}
.modal-head h3 {
  font-size: 21px; flex: 1;
  text-transform: uppercase; letter-spacing: .05em;
  text-shadow: var(--shadow-title);
}
.modal-body { padding: 24px; }
.modal-foot .spacer { flex: 1; }
.modal-foot {
  padding: 18px 24px;
  display: flex; gap: 13px; justify-content: flex-end;
  position: sticky; bottom: 0;
  background: linear-gradient(180deg, rgba(20, 26, 62, .95), rgba(16, 21, 50, .97));
  box-shadow: inset 0 3px 0 rgba(0, 0, 0, .3);
}

/* ---------- Полоса инструментов сервера ---------- */

.detail-tools {
  display: flex; flex-wrap: wrap; gap: 9px;
  margin: 0 0 18px;
}
.tool-btn { flex: 0 1 auto; }

/* Сетка кнопок быстрых команд — одинаковая ширина, чтобы не рябило */
.quick-grid {
  display: grid; gap: 9px; margin-bottom: 14px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
.quick-btn { justify-content: flex-start; }

/* Загрузка файла: своя кнопка вместо системной надписи браузера */
.upload-row { display: flex; gap: 11px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; }
.upload-row input[type="file"] {
  flex: 1 1 200px; min-width: 0;
  padding: 9px 11px; color: var(--ink-dim); font-size: 13px; font-family: var(--ui);
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.upload-row input[type="file"]::file-selector-button {
  margin-right: 11px; padding: 5px 11px; border: 0; cursor: pointer;
  font-family: var(--ui); font-weight: 700; font-size: 12.5px; color: #3a2a06;
  background: linear-gradient(180deg, #ffe08a, var(--gold-deep));
}

/* Метка рядом с названием в списках: «выключен», «в игре», причина копии */
.file-row .tag {
  margin-left: 9px; padding: 2px 7px;
  font-size: 11.5px; font-weight: 700; color: var(--ink-faint);
  background: rgba(0, 0, 0, .28);
}
.file-row .tag.on { color: #10142e; background: var(--green-hi); }
.file-row select { flex: 0 1 160px; min-width: 0; }

.event-grid {
  display: grid; gap: 11px; margin-bottom: 18px;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
.event-grid .switch { font-size: 14px; font-weight: 600; }

/* ---------- Ползунки игрового баланса ---------- */

.tune { margin-bottom: 20px; }
.tune:last-child { margin-bottom: 0; }

.tune-head {
  display: flex; align-items: baseline; gap: 10px; margin-bottom: 9px;
}
.tune-head .name {
  font-size: 14.5px; font-weight: 700; color: var(--gold);
  text-shadow: var(--shadow-text);
}
.tune-head .spacer { flex: 1; }
/* Крупное значение множителя — по нему и ориентируешься */
.tune-head .amount {
  font-family: var(--mono); font-size: 17px; font-weight: 700; color: var(--ink);
  min-width: 62px; text-align: right; font-variant-numeric: tabular-nums;
}
.tune-head .raw { font-size: 12.5px; color: var(--ink-faint); font-family: var(--mono); }

.tune .hint { margin-top: 8px; }
.tune .warn-restart { color: var(--gold); font-size: 12.5px; margin-top: 6px; }

/* Сам ползунок — в духе игровых полос */
.tune input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 14px; margin: 0; cursor: pointer;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
  /*
   * На телефоне палец, попавший на ползунок, иначе не даёт пролистать страницу:
   * браузер отдаёт весь жест ползунку. Так вертикальное движение остаётся прокруткой.
   */
  touch-action: pan-y;
}
.tune input[type="range"]:focus-visible { outline: 3px solid var(--gold); outline-offset: 6px; }
.tune input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 22px; margin-top: -4px;
  background: linear-gradient(180deg, #ffe08a, var(--gold-deep));
  box-shadow: 0 0 0 2px var(--stone-dark);
  cursor: grab;
}
.tune input[type="range"]::-moz-range-thumb {
  width: 18px; height: 22px; border: 0;
  background: linear-gradient(180deg, #ffe08a, var(--gold-deep));
  box-shadow: 0 0 0 2px var(--stone-dark);
  cursor: grab;
}

.tune-presets { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }

/* ---------- Подтверждение действия ---------- */

/* Подтверждение — тоже <dialog>. Верхний слой браузера складывает окна
   стопкой в порядке открытия, поэтому оно само встаёт поверх настроек. */
.confirm { width: 100%; max-width: 470px; }
.confirm-body { padding: 24px; display: flex; gap: 16px; align-items: flex-start; }
.confirm-body .ic { width: 34px; height: 34px; flex-shrink: 0; margin-top: 2px; }
.confirm-body p { margin: 0; font-size: 16px; line-height: 1.6; color: var(--ink-dim); }
.confirm-body p b { color: var(--ink); }
.confirm-note {
  display: block; margin-top: 10px; font-size: 14px; color: #ffbdbd;
}

/* ---------- Поля ---------- */

.section-label {
  font-size: 14px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--gold); font-weight: 800; margin: 30px 0 16px;
  text-shadow: var(--shadow-text);
}
.section-label:first-child { margin-top: 0; }

.field { margin-bottom: 22px; }
.field:last-child { margin-bottom: 0; }
.field > label {
  display: block; font-size: 14px; color: var(--gold);
  margin-bottom: 9px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em;
  text-shadow: var(--shadow-text);
}
.field .hint { font-size: 13.5px; color: var(--ink-faint); margin-top: 9px; line-height: 1.6; }
.field .hint code {
  font-family: var(--mono); background: rgba(0, 0, 0, .4); padding: 2px 6px;
}

/* Поле с ячейкой-иконкой слева */
.input-wrap {
  display: flex; align-items: stretch;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.input-wrap:focus-within { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }
.input-ic {
  display: grid; place-items: center; width: 46px; flex-shrink: 0;
  background: rgba(255, 255, 255, .09);
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, .16),
    inset 0 3px 0 rgba(0, 0, 0, .28);
}
.input-wrap > input,
.input-wrap > select {
  flex: 1; min-width: 0;
  padding: 13px 14px; border: 0; outline: none;
  background: transparent; color: var(--ink);
  font-family: var(--ui); font-size: 16px; font-weight: 600;
}
.input-wrap > select {
  appearance: none; cursor: pointer; padding-right: 42px;
  background-image: linear-gradient(45deg, transparent 50%, var(--gold) 50%),
                    linear-gradient(135deg, var(--gold) 50%, transparent 50%);
  background-position: calc(100% - 22px) 23px, calc(100% - 15px) 23px;
  background-size: 7px 7px, 7px 7px;
  background-repeat: no-repeat;
}
.input-wrap > select option { background: #1c2450; color: var(--ink); }
.input-wrap > input::placeholder { color: var(--ink-faint); font-weight: 500; }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 560px) { .field-row { grid-template-columns: 1fr; } }

/* Выбор движка */
.choice-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
@media (max-width: 560px) { .choice-row { grid-template-columns: 1fr; } }
.choice {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 15px; cursor: pointer;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
  transition: filter .12s, transform .1s;
}
.choice:hover:not(.off) { filter: brightness(1.3); transform: translateY(-2px); }
/* Выбранная ячейка светится, как активный слот в инвентаре */
.choice.on {
  box-shadow:
    0 0 0 2px var(--stone-dark), 0 0 0 4px var(--green-hi),
    0 0 16px rgba(126, 217, 87, .35);
}
.choice .ic { margin-top: 2px; }
.choice b { display: block; font-size: 17px; font-weight: 800; margin-bottom: 4px; }
.choice .txt { min-width: 0; }
.choice span { font-size: 13.5px; color: var(--ink-faint); line-height: 1.5; display: block; }
.choice input { display: none; }
.choice.off { opacity: .42; cursor: not-allowed; }

/* Пиксельный чекбокс */
.switch { display: inline-flex; align-items: center; gap: 12px; cursor: pointer; font-size: 16px; font-weight: 600; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch .box {
  width: 26px; height: 26px; flex-shrink: 0; display: grid; place-items: center;
  background: rgba(255, 255, 255, .09);
  box-shadow:
    0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line),
    inset 0 0 0 2px rgba(255, 255, 255, .16);
}
.switch .box .ic { display: none; }
.switch input:checked ~ .box .ic { display: block; }
.switch input:checked ~ .box { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--green-hi); }
.switch input:focus-visible ~ .box { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }

.form-error {
  padding: 13px 15px; margin-bottom: 18px;
  background: rgba(58, 20, 32, .95); color: #ffbdbd; font-size: 15px; font-weight: 600;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #c76a6a;
}

/* ---------- Доступ извне ---------- */

/* Вердикт по сети: что именно сработает у этого пользователя */
.verdict {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 13px 15px; font-size: 14.5px; line-height: 1.55;
  background: var(--field); color: var(--ink-dim);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.verdict.ok { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--green-hi); color: #d6f5c8; }
.verdict.warn { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }
.verdict.bad { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #c76a6a; color: #ffd6d6; }
.verdict .ic { margin-top: 1px; }

/* Способ подключения: заголовок, содержимое, кнопки */
.way { margin-top: 22px; }
.way:first-of-type { margin-top: 18px; }
.way > h4 {
  margin: 0 0 11px; font-size: 13.5px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .07em; color: var(--gold);
  text-shadow: var(--shadow-text);
  display: flex; align-items: center; gap: 8px;
}
.way > h4 .tag {
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: none; color: #10142e; padding: 2px 8px;
  background: var(--green-hi);
}
.way .hint { margin-top: 10px; }
.way .notice-actions { margin-top: 12px; }

/* Статус движка */
.engine-status {
  display: flex; align-items: center; gap: 9px;
  font-size: 14px; font-weight: 600; margin-top: 11px; padding: 10px 13px;
  background: var(--field); box-shadow: 0 0 0 2px var(--stone-dark);
  color: var(--ink-dim);
}
.engine-status.ok { color: var(--green-hi); }
.engine-status.bad { color: #ffbdbd; }

/* ---------- Вход ---------- */

.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: 32px; }
.login-card { width: 100%; max-width: 420px; text-align: center; padding: 36px 32px; }
.login-card h1 {
  font-family: var(--pixel);
  font-size: 30px; margin-bottom: 10px; letter-spacing: .04em;
  text-shadow: var(--shadow-title);
}
.login-card p { color: var(--ink-faint); font-size: 15px; margin: 0 0 26px; }

/* ---------- Тосты ---------- */

.toasts { position: fixed; right: 24px; bottom: 24px; z-index: 100; display: flex; flex-direction: column; gap: 13px; }
.toast {
  min-width: 270px; max-width: 420px;
  padding: 14px 17px; font-size: 15px; font-weight: 600; line-height: 1.55;
  background: linear-gradient(180deg, var(--panel-hi), var(--panel));
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #6a72a8, 0 8px 22px rgba(0, 0, 0, .55);
  animation: slide var(--t) var(--ease);
  /* Соседи доезжают на новое место сами, когда один из тостов уходит */
  transition: transform var(--t-slow) var(--ease);
}
@keyframes slide { from { opacity: 0; transform: translateX(22px); } }
/* Уходит той же дорогой, а не пропадает рывком через пять секунд */
.toast.leaving { animation: slide var(--t) var(--ease) reverse forwards; }
.toast.err { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px #c76a6a, 0 8px 22px rgba(0, 0, 0, .55); color: #ffbdbd; }
.toast.ok { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--green-hi), 0 8px 22px rgba(0, 0, 0, .55); color: #c6f5d4; }
@media (max-width: 660px) {
  .toasts { right: 12px; left: 12px; bottom: 12px; }
  .toast { min-width: 0; max-width: none; }
}

.spin { display: inline-block; animation: spin 1s steps(8, end) infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ==========================================================================
   Выделение, телефон, движение
   ========================================================================== */

/*
 * Цвет выделения.
 *
 * Зелёный занят главными кнопками, красный — опасными, золото — подписями.
 * Выделению нужен цвет, которого в панели больше нигде нет, иначе «вот это
 * сейчас выбрано» теряется среди обычного оформления. Холодная бирюза не
 * встречается ни в одном другом состоянии и читается сразу.
 */
:root {
  --select: #5ad9ff;
  --select-soft: rgba(90, 217, 255, .28);
}
:root.day {
  --select: #0f7ea8;
  --select-soft: rgba(15, 126, 168, .22);
}

.btn:focus-visible,
.card:focus-visible,
a:focus-visible,
[tabindex]:focus-visible { outline: 3px solid var(--select); outline-offset: 6px; }

/* Поля обводим изнутри: снаружи обводка налезала бы на соседние */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--select);
  outline-offset: -2px;
}

::selection { background: var(--select-soft); color: var(--ink); }

/*
 * На телефоне система рисует свою серую вспышку под пальцем поверх нашей.
 * Гасим её и показываем собственную — тем же цветом выделения.
 */
* { -webkit-tap-highlight-color: transparent; }

/* ---------- Телефон ---------- */

@media (max-width: 660px) {
  /* Под вырез, скруглённые углы и полосу жестов */
  .app { padding-left: max(14px, env(safe-area-inset-left)); padding-right: max(14px, env(safe-area-inset-right)); }
  .toasts { bottom: max(12px, env(safe-area-inset-bottom)); }

  /* Палец — не курсор: всё нажимаемое не мельче 48 px */
  .btn, .conn-pill, select, input[type="search"], input[type="text"], input[type="number"] { min-height: 48px; }
  .btn-sm { min-height: 48px; }

  /* Поиск и сортировка занимали две строки каждая — теперь делят одну */
  .section-head { gap: 10px; }
  .section-head h2 { flex: 1 1 auto; }
  .list-search { flex: 1 1 auto; min-width: 0; }
  .list-sort { flex: 1 1 100%; }

  /* Значение важнее подписи — на маленьком экране это должно быть видно */
  .meta-item .k { font-size: 11px; }
  .meta-item .v { font-size: 17px; }

  /* Инерция и запрет «протаскивания» страницы за краем списка */
  .console, .grid { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
}

/*
 * Кнопки на странице сервера.
 *
 * Свободный перенос давал рваную лесенку: две, две, две, три и одна сиротой.
 * Раскладываем обе группы одинаковой сеткой по две в ряд — кнопки одной
 * ширины, последняя нечётная тянется на всю строку и не висит огрызком.
 */
@media (max-width: 660px) {
  .detail-actions,
  .detail-tools {
    display: grid;
    grid-template-columns: 1fr 1fr;
    /* Рамка кнопки выходит за её края на 6px — зазор не меньше 14px */
    gap: 18px 14px;
    margin-left: 0;
    width: 100%;
  }
  .detail-actions > .btn,
  .detail-tools > .btn { min-width: 0; width: 100%; }

  /* Нечётная последняя занимает обе доли, иначе строка выглядит недоделанной */
  .detail-actions > .btn:last-child:nth-child(odd),
  .detail-tools > .btn:last-child:nth-child(odd) { grid-column: 1 / -1; }

  /* Длинные подписи не должны распирать колонку */
  .detail-actions > .btn,
  .detail-tools > .btn { white-space: normal; padding-left: 10px; padding-right: 10px; }
}

/* Кнопка «наверх»: появляется, только когда пролистали заметно вниз */
.to-top {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  width: 50px; height: 50px; padding: 0;
  opacity: 0; transform: translateY(14px) scale(.9);
  pointer-events: none;
  transition: opacity var(--t) var(--ease), transform var(--t) var(--ease);
}
.to-top.on { opacity: 1; transform: none; pointer-events: auto; }
@media (max-width: 660px) {
  .to-top { right: max(14px, env(safe-area-inset-right)); bottom: max(70px, env(safe-area-inset-bottom)); }
}

/* ---------- Отклик на нажатие ---------- */

/*
 * Волна от точки касания. Круг заранее лежит в кнопке невидимым и только
 * растёт через transform — ни одного пересчёта раскладки, ни одной новой
 * заливки. Точку касания скрипт кладёт в --rx/--ry.
 */
.btn { position: relative; overflow: hidden; }
.btn > .ripple {
  position: absolute; left: var(--rx, 50%); top: var(--ry, 50%);
  width: 12px; height: 12px; margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .5);
  pointer-events: none;
  animation: ripple 480ms var(--ease) forwards;
}
@keyframes ripple {
  from { transform: scale(0); opacity: .55; }
  to { transform: scale(18); opacity: 0; }
}

/* Отпустил — кнопка коротко отыгрывает обратно, а не просто встаёт на место */
.btn:not(:disabled) { transition: filter var(--t-fast) var(--ease), transform var(--t-fast) var(--ease-out-back); }

/* Карточка при нажатии слегка проседает — понятно, что нажатие поймано */
.card:active { transform: scale(.995) translateY(-1px); }

/* ---------- Поля и переключатели ---------- */

.switch .box { transition: transform var(--t) var(--ease-out-back); }
.switch input:checked + .box { transform: scale(1.12); }

input[type="range"]::-webkit-slider-thumb { transition: transform var(--t-fast) var(--ease); }
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.25); }
input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--select-soft); }

/* Ошибка поля — короткое дрожание, а не молчаливое покраснение */
.shake { animation: shake 320ms var(--ease); }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-5px); }
  45% { transform: translateX(4px); }
  70% { transform: translateX(-2px); }
}

/* Подсказка под полем выезжает, а не возникает */
.field .hint { animation: hint-in var(--t) var(--ease) backwards; }
@keyframes hint-in { from { opacity: 0; transform: translateY(-4px); } }

/* ---------- Мелкая жизнь ---------- */

/* Кролик в шапке моргает редко — раз в полминуты, иначе это раздражает */
.theme-btn { animation: blink 30s steps(1, end) infinite; }
@keyframes blink {
  0%, 98.5%, 100% { transform: scaleY(1); }
  99.2% { transform: scaleY(.1); }
}

/* Работающий сервер чуть дышит — заметно только боковым зрением */
.card[data-state="running"] .badge::before { animation: pulse 2s steps(2, end) infinite; }
.card[data-state="running"] .card-title { animation: breathe 5s ease-in-out infinite; }
@keyframes breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.012); }
}

/* Связь потеряна — точка мигает, а не просто краснеет */
.dot.dead { animation: pulse 1.4s steps(2, end) infinite; }

/* Звёзды на фоне мерцают вразнобой */
.fireflies { animation: twinkle 9s ease-in-out infinite; }
@keyframes twinkle {
  0%, 100% { opacity: 1; }
  50% { opacity: .72; }
}

/* Огонь на эмблеме колышется */
.brand .mark { animation: flicker 3.4s ease-in-out infinite; transform-origin: 50% 80%; }
@keyframes flicker {
  0%, 100% { transform: scale(1) rotate(0deg); }
  35% { transform: scale(1.03) rotate(-1deg); }
  70% { transform: scale(.985) rotate(1deg); }
}

/* ---------- Списки ---------- */

/* Отфильтрованное не исчезает рывком, а тает на месте */
.card.filtered-out {
  animation: fade-out var(--t) var(--ease) forwards;
  pointer-events: none;
}
@keyframes fade-out { to { opacity: 0; transform: scale(.97); } }

/* Удаляемая карточка уезжает вбок и уступает место соседям */
.card.removing { animation: card-out var(--t-slow) var(--ease) forwards; pointer-events: none; }
@keyframes card-out {
  to { opacity: 0; transform: translateX(-28px) scale(.94); }
}

/* Пустой список приподнимается, а не выпрыгивает */
.empty { animation: card-in var(--t-slow) var(--ease) backwards; }

/* ---------- Вкладки ---------- */

/*
 * Подчёркивание едет за выбранной вкладкой. Полоса одна на весь ряд, её
 * положение и ширину задаёт скрипт переменными — двигается transform.
 */
.tabs { position: relative; }
.tabs .tab-underline {
  position: absolute; left: 0; bottom: 0; height: 3px; width: 1px;
  background: var(--select);
  transform: translateX(var(--tab-x, 0)) scaleX(var(--tab-w, 0));
  transform-origin: left center;
  transition: transform var(--t) var(--ease);
  pointer-events: none;
}

/* ---------- Заглушки ---------- */

/* Скелет уступает место данным растворением, а не подменой кадра */
.skeleton-fade { animation: fade var(--t-slow) var(--ease); }

/* ---------- Консоль ---------- */

/* Совпадение поиска подсвечивается волной, а не загорается разом */
.console-line mark {
  background: var(--select-soft); color: inherit;
  animation: mark-in var(--t) var(--ease) backwards;
  animation-delay: calc(var(--mi, 0) * 18ms);
}
@keyframes mark-in { from { opacity: 0; transform: scaleX(.6); } }

/* ---------- Бережно к устройству ---------- */

/*
 * Спрятанная вкладка не должна греть телефон в кармане: всё лишнее замирает.
 * Класс вешает скрипт по событию видимости.
 */
body.away .ember,
body.away .fireflies,
body.away .brand .mark,
body.away .theme-btn,
body.away .card::after,
body.away .card-title,
body.away .spin { animation-play-state: paused; }

/*
 * Слабое устройство. Скрипт ставит класс, если у машины мало ядер или памяти;
 * длительности сокращаются вдвое — движение остаётся, но стоит дешевле.
 */
html.lite { --t-fast: 60ms; --t: 90ms; --t-slow: 150ms; }
html.lite .ember,
html.lite .fireflies,
html.lite .brand .mark,
html.lite .card-title { animation: none; }

/*
 * Экономия движения. Темп обнуляется в одном месте — новые переходы,
 * написанные через переменные, гаснут сами, дописывать сюда ничего не нужно.
 * Общее правило ниже оставлено для анимаций с явной длительностью.
 */
@media (prefers-reduced-motion: reduce) {
  :root { --t-fast: 0s; --t: 0s; --t-slow: 0s; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
  .btn:hover:not(:disabled) { transform: none; }
  /* Заглушки без пульсации всё равно должны быть видны */
  .skeleton { opacity: .5; }
  /* Свёрнутый блок без перехода просто раскрывается сразу */
  .collapse.open { grid-template-rows: 1fr; }
  /* Холст светлячков гасит себя сам, но на всякий случай убираем и его */
  .fireflies { display: none; }
  /* Плавная прокрутка тоже относится к движению */
  .console { scroll-behavior: auto; }
}

/* ---------- Заметка к серверу ---------- */

.note-line {
  margin-top: 13px; padding: 11px 13px;
  font-size: 14px; line-height: 1.55; color: var(--ink-dim);
  background: rgba(255, 255, 255, .06);
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, .25);
  white-space: pre-wrap; word-break: break-word;
}
.note-line:empty { display: none; }
.note-line .ic { float: left; margin: 1px 9px 0 0; }

/* ---------- Корзина ---------- */

.trash-row .when { color: var(--ink-faint); font-size: 13px; }
.trash-row .sz { white-space: nowrap; }

/* ---------- Подтверждение по имени ---------- */

.confirm-typed { margin-top: 15px; }
.confirm-typed input {
  width: 100%; padding: 10px 12px; border: 0;
  background: var(--field); color: var(--ink);
  font-family: var(--mono); font-size: 14.5px;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.confirm-typed input:focus { outline: none; box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--red); }
.confirm-typed .hint { margin-top: 7px; }

/* ---------- Подсказка по горячим клавишам ---------- */

.keys-grid { display: grid; grid-template-columns: auto 1fr; gap: 11px 16px; align-items: center; }
kbd {
  display: inline-block; min-width: 26px; padding: 4px 8px; text-align: center;
  font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--ink);
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 2px 0 2px rgba(0, 0, 0, .35);
}

/* ---------- Переключатель темы в шапке ---------- */

.theme-btn { width: 42px; height: 42px; padding: 0; }

/* ---------- Место на диске ---------- */

.disk-bar {
  height: 10px; margin-top: 9px; overflow: hidden;
  background: rgba(0, 0, 0, .4);
  box-shadow: 0 0 0 2px var(--stone-dark);
}
.disk-bar span {
  display: block; height: 100%; width: 100%;
  transform: scaleX(var(--used, 0)); transform-origin: left;
  background: linear-gradient(180deg, var(--green-hi), var(--green));
  transition: transform var(--t-slow) var(--ease);
}
.disk-bar.warn span { background: linear-gradient(180deg, #ffe08a, var(--gold-deep)); }
.disk-bar.bad span { background: linear-gradient(180deg, #ff9c9c, var(--red)); }

/* ---------- Подключение: то, за чем сюда и заходят ---------- */

.connect-panel { box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold), 0 0 0 6px var(--stone-dark), 0 8px 20px rgba(0, 0, 0, .45); }

.conn-verdict { font-size: 12px; font-weight: 700; color: var(--ink-faint); white-space: nowrap; }
.conn-verdict.ok { color: var(--green-hi); }
.conn-verdict.bad { color: #ff9c9c; }

/* Адрес — крупно и в одну строку: его переписывают глазами и копируют */
.addr-big {
  display: flex; align-items: center; gap: 11px; flex-wrap: wrap;
  padding: 14px 15px; margin-bottom: 11px;
  background: var(--field);
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold-deep);
}
.addr-big code {
  flex: 1; min-width: 0;
  font-family: var(--mono); font-size: 21px; font-weight: 700; color: var(--gold);
  letter-spacing: .02em; word-break: break-all;
  text-shadow: var(--shadow-text);
}
:root.day .addr-big code { color: var(--gold-deep); }
.addr-note { font-size: 13.5px; color: var(--ink-dim); line-height: 1.6; margin-bottom: 15px; }

.version-line {
  display: flex; justify-content: space-between; gap: 12px; align-items: baseline;
  padding: 11px 0 4px;
  border-top: 2px solid rgba(0, 0, 0, .28);
}
.version-line .k { color: var(--ink-faint); font-size: 14px; }
.version-line .v { font-weight: 700; font-size: 16px; color: var(--gold); }

/* Локальный адрес рядом с онлайновым путали — теперь он явно вторичен */
#local-panel .panel-head { opacity: .85; }

/* ---------- Поиск по настройкам баланса ---------- */

.tune-search { display: flex; align-items: center; gap: 11px; margin-bottom: 15px; }
.tune-search input {
  flex: 1; min-width: 0; padding: 9px 12px; border: 0;
  background: var(--field); color: var(--ink);
  font-family: var(--ui); font-size: 14.5px;
  box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--field-line);
}
.tune-search input:focus { outline: none; box-shadow: 0 0 0 2px var(--stone-dark), 0 0 0 4px var(--gold); }
.tune-search .count { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
