/*
 * Единственное место, где в этом проекте объявляется цвет, размер, отступ и
 * радиус. Всё остальное ссылается сюда.
 *
 * Палитра — та же, что у l2endo.ru: глубокий индиго, золото и почти белый.
 * Олимпиада и сайт клана — один продукт, и человек, пришедший с одного на
 * другой, не должен гадать, туда ли он попал. Иерархия выражается уровнем
 * поверхности, а не тенями: в тёмной теме тень не читается, а более светлая
 * поверхность читается сразу.
 *
 * Тёмная — тема по умолчанию: приложение открывают вечером, рядом с
 * полноэкранной игрой. Светлая включается переключателем в шапке и лежит ниже
 * отдельным блоком: те же двенадцать имён, другие значения. Имена не
 * добавляются и не убывают — стили не знают, какая тема сейчас включена.
 *
 * Золото означает ровно одно — «сюда можно нажать», и прежде всего главное
 * действие экрана. Золото — всегда заливка, и текст на нём всегда --accent-ink:
 * золотой текст на светлом фоне не прочесть, а заливка читается в обеих темах.
 * Состояния несут только зелёный, оранжевый и красный.
 *
 * Три оттенка текста различаются заметно: каждый проходит 4.5:1 на своей
 * поверхности с запасом, и иерархию держит и масштаб, и яркость.
 */
:root {
  /* Встроенные элементы — выпадающий список, ползунок, полосы прокрутки —
     рисуются в той же теме, что и всё остальное. */
  color-scheme: dark;

  /* Поверхности: чем ближе к человеку, тем светлее. */
  --bg: #110a27;
  --surface: #191138;
  --surface-raised: #261c4e;
  --separator: rgba(255, 255, 255, 0.1);

  /* Текст. */
  --text: #f4f2f7;
  --text-secondary: #c4bfd8;
  --text-tertiary: #a59fbf;

  /* Интерактивность — и только она. Золото — заливка, на нём — тёмные
     буквы в обеих темах. */
  --accent: #f5b105;
  --accent-ink: #1d1d1f;

  /* Состояния. */
  --go: #82d7a9;
  --warn: #efca7e;
  --danger: #ffadb6;

  /* Шкала размеров. Отсчёт кратно крупнее всего: это и есть иерархия. */
  --size-countdown: 56px;
  --size-title: 20px;
  --size-ui: 15px;
  --size-caption: 13px;
  --size-data: 13px;

  /*
   * Ник, который обязан помещаться целиком.
   *
   * Шестнадцать символов — не догадка о том, сколько бывает достаточно, а
   * предел самой игры: длиннее ника в Lineage 2 не бывает, а логин здесь и
   * есть игровой ник. Поэтому это не «обычно влезает», а «влезает всегда»:
   * длиннее просто неоткуда взяться, и сервер такой логин не примет — см.
   * MaxLoginLength в internal/accounts.
   *
   * Единица посимвольная, а не пиксельная, потому что и требование
   * посимвольное: ch — ширина нуля в текущем шрифте, и она едет вместе с
   * размером текста, а пиксель бы не поехал.
   *
   * Двадцать, а не шестнадцать: ch — это ширина нуля, а кириллическая буква
   * шире нуля. Узкие (г, е, о) уже, широкие (Ш, Щ, Ж, Ы, М) — примерно на
   * четверть шире, и ник из одних широких занимает как раз двадцать нулей.
   * Обычный ник занимает около одиннадцати, то есть влезает вдвое.
   *
   * Мера была восемнадцать, пока столбцы были шире нужного и запас брался
   * оттуда. Как только столбец онлайна сузили ровно до ника, точность самой
   * меры и стала решающей.
   */
  --name-width: 20ch;

  /* Заголовок раздела панели. Крупнее заголовка окна: в панели он — первое,
     что говорит, где ты находишься. */
  --size-heading: 28px;

  /* Сетка отступов кратна четырём. Два крупных шага — для панели: там разделы
     читают, а не ловят боковым зрением, и воздух между блоками дешевле, чем
     поиск границы между ними. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;

  /* Значки и кружки: аватар с первой буквой, номер позиции, кнопка-значок. */
  --icon: 20px;
  --avatar: 32px;
  --control: 44px;

  /* Панель: столбец разделов слева и содержимое шириной в строку, которую
     удобно читать, а не во весь экран. */
  --sidebar-width: 240px;
  --content-width: 880px;

  /* Радиусы. */
  --radius-panel: 20px;
  --radius-button: 12px;
  --radius-row: 12px;
  /* Плашка-«таблетка»: счётчики и метки. Радиус больше любой высоты. */
  --radius-pill: 999px;

  /* Начертания. Оба лежат в бинарнике и отдаются с того же сервера. */
  --font-ui: "Onest", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-data: "JetBrains Mono", ui-monospace, "Cascadia Mono", monospace;
}

/*
 * Светлая тема. Те же двенадцать имён — и те же пары на контраст, что и у
 * тёмной: проверка гоняет обе (tools/check-design.ts).
 *
 * Поверхности здесь идут наоборот: ближе к человеку — белее. Фон не белый, а
 * чуть сиреневый: белые карточки на белом читались бы только тенью, а теней в
 * проекте нет. Приподнятая поверхность (кнопки, строки списков, поля) лежит
 * на белой карточке серой заливкой.
 *
 * Цвета состояний темнее своих тёмных двойников: светлые тёмной темы на белом
 * не проходят 4.5:1. Золото то же самое — оно всегда заливка.
 */
:root[data-theme="light"] {
  color-scheme: light;

  --bg: #f3f2f8;
  --surface: #ffffff;
  --surface-raised: #ecebf3;
  --separator: rgba(17, 10, 39, 0.12);

  --text: #1d1d1f;
  --text-secondary: #4a4857;
  --text-tertiary: #666474;

  --accent: #f5b105;
  --accent-ink: #1d1d1f;

  --go: #1f6b45;
  --warn: #7a5300;
  --danger: #b42332;
}

/*
 * Оба начертания лежат в бинарнике и отдаются тем же сервером, что и очередь.
 * Ни одного внешнего адреса: страница обязана подниматься на VPS без выхода в
 * интернет, а шрифт с чужого CDN — это ещё и адрес, по которому видно, кто
 * открыл страницу и когда.
 *
 * Подмножества только латиница и кириллица: интерфейс русский, остальное —
 * лишние килобайты в бинарнике.
 */
@font-face {
  font-family: "Onest";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("./fonts/onest-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: "Onest";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("./fonts/onest-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+2122, U+2191, U+2193, U+2212;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url("./fonts/jetbrains-mono-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url("./fonts/jetbrains-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+2122, U+2191, U+2193, U+2212;
}

/*
 * Оболочка из трёх частей: шапка и под ней две области — очередь и чат.
 * Следующие задачи наполняют области, не перестраивая оболочку.
 *
 * Ни одного цвета и ни одного размера мимо токенов: и то и другое роняет
 * сборку, см. tools/check-design.ts.
 */
* {
  box-sizing: border-box;
}

/*
 * Спрятанное — спрятано. Восклицательный знак здесь единственный в проекте и
 * стоит по делу.
 *
 * Браузер прячет элемент с атрибутом hidden правилом [hidden]{display:none} из
 * своей таблицы, а любое авторское объявление display эту таблицу перекрывает —
 * независимо от специфичности, потому что авторский слой каскада старше. Стоит
 * задать .modal{display:grid} — и модалка, которую представление честно
 * пометило hidden, остаётся на экране: закрывает окно целиком, а нажать в ней
 * нечего, потому что состояние и так пустое.
 *
 * Правило спрятано от чужого взгляда ровно потому, что чужой взгляд его и не
 * заметит: оба шва — поток кадров и редуктор — работают без браузера, и ни один
 * из них про раскладку ничего не знает.
 */
[hidden] {
  display: none !important;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--size-ui);
  line-height: 1.4;
  /* Табличные цифры везде: цифра, меняющая ширину, дёргает строку очереди. */
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
}

.shell {
  /* Шапка, вкладки, панель хода, содержимое.
   *
   * На широком экране панель стоит над содержимым: сколько осталось и что можно
   * нажать — первое, что читают, а глаз начинает сверху. Ни в одной строке она
   * не лежит внутри прокручиваемой области: то, ради чего это приложение
   * открывают, не должно уезжать вместе с лентой.
   *
   * На телефоне она переезжает вниз и закрепляется там — см. правило ниже: там
   * её место определяет не глаз, а экранная клавиатура и большой палец.
   *
   * Колонка, а не сетка со списком строк: спрятанные элементы строк не
   * занимают, и растягивающаяся строка сетки доставалась строке достижимости
   * внизу, а очередь с чатом стояли короткими посреди пустого экрана. Здесь
   * растягивается само содержимое — и только оно. */
  display: flex;
  flex-direction: column;
  height: 100%;
  min-width: 320px;
}

.shell__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  border-bottom: 1px solid var(--separator);
}

.shell__title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--size-title);
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.01em;
}

/* Знак приложения: кубок на золоте — цвета l2endo.ru, чтобы сайт клана и
   Олимпиада читались одним продуктом. */
.brand-mark {
  display: grid;
  place-items: center;
  width: var(--avatar);
  height: var(--avatar);
  border-radius: 10px;
  background: var(--accent);
  color: var(--accent-ink);
  flex: 0 0 auto;
}

/* Значок в строке текста. Цвет — от текста рядом: значок поясняет подпись, а
   не спорит с ней. */
.icon {
  display: block;
  width: var(--icon);
  height: var(--icon);
  flex: 0 0 auto;
}

/* Счётчик онлайна — плашкой, а не голым текстом: это сводка, а не подпись. */
.shell__online {
  color: var(--text-secondary);
  font-size: var(--size-caption);
  background: var(--surface-raised);
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
  white-space: nowrap;
}

.shell__online:empty {
  display: none;
}

/* Кнопки шапки — плашки на приподнятой поверхности, со значком. Голые синие
   ссылки в углу читались как подписи, а не как то, что нажимают. */
.shell__settings {
  background: var(--surface-raised);
  border: 0;
  color: var(--text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  padding: 0 var(--space-3);
  border-radius: var(--radius-button);
  /* Стандартная сенсорная цель. Шестерёнка — самая маленькая кнопка в шапке, и
     без этого в неё не попасть большим пальцем с первого раза. */
  min-width: var(--control);
  min-height: var(--control);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* «К очереди» в две строки на телефоне читается как две кнопки. */
  white-space: nowrap;
}

.shell__settings:hover {
  filter: brightness(1.08);
}

/*
 * Кто вошёл — и рядом то, что с этим делают: уйти в панель и выйти совсем.
 * Обе кнопки стоят у имени, а не по разным углам шапки: они про одного и того
 * же человека.
 */
.profile {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

/* Значок — первая буква ника на приподнятой поверхности. Картинок у аккаунтов
   нет: заводить их значит заводить их хранение, их размер и их модерацию. */
.profile__icon {
  display: grid;
  place-items: center;
  width: var(--avatar);
  height: var(--avatar);
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--size-caption);
  font-weight: 700;
  flex: 0 0 auto;
}

/* Имя жмётся первым, если шапке тесно: кнопки рядом с ним нажимают, а его
   читают, и прочитать его можно и по половине. */
.profile__name {
  color: var(--text);
  font-weight: 600;
  font-size: var(--size-caption);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Столбцы содержимого: очередь, чат, онлайн. */
.shell__body {
  display: grid;
  /*
   * Ширина столбца — то, что в нём обязано поместиться, и ни пикселем больше.
   *
   * Очередь: ник, номер позиции и края — строки, шире этого, ей нечем занять,
   * а пустое место между номером и именем только отдаляет одно от другого.
   * Онлайн: одни ники, перед ними не стоит ничего. Чат: ему достаётся всё
   * остальное, потому что реплики бывают длинными, а переносить их на десять
   * строк — значит выкидывать из ленты то, что было сказано минуту назад.
   *
   * Равными столбцы быть не должны. Раньше они были равными — auto-fit делит
   * место поровну, — и списку онлайна с одними никами доставалась ровно треть
   * экрана.
   */
  --queue-width: calc(var(--name-width) + var(--avatar) + var(--space-4) * 2 + var(--space-3) * 3);
  --online-width: calc(var(--name-width) + var(--avatar) + var(--space-3) + var(--space-4) * 2);
  /* Потолок очереди — на четверть больше её минимума: столько нужно предостережению об
     анонимности, чтобы читаться абзацем, а не столбиком в три слова. Выше
     потолка место уходит чату. */
  grid-template-columns:
    minmax(var(--queue-width), calc(var(--queue-width) * 1.25))
    minmax(calc(var(--name-width) + var(--space-2) + var(--space-3) * 2), 1fr)
    var(--online-width);
  gap: var(--space-4);
  padding: 0 var(--space-6) var(--space-4);
  flex: 1 1 auto;
  min-height: 0;
}

/* Вкладки. На широком экране их не показывают: там открыто всё сразу, и
   переключать нечего. */
.tabs {
  display: none;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: 0 var(--space-3);
}

.tabs__tab {
  /* Основа по содержимому, а не ноль: у админа вкладок шесть, и на трёхстах
     двадцати пикселях они обязаны переноситься, а не резаться и не заводить
     горизонтальную прокрутку. */
  flex: 1 1 auto;
  min-width: 0;
  /* Стандартная сенсорная цель. Кнопка, до которой не попасть большим пальцем с
     первого раза, — это кнопка, которую нажмут дважды и не туда. */
  min-height: 44px;
  background: none;
  border: 0;
  border-radius: var(--radius-button);
  color: var(--text-secondary);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  padding: var(--space-2) var(--space-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* Открытая вкладка — приподнятой поверхностью и основным цветом текста. Золото
   остаётся главному действию: «ты здесь» не должно спорить с «нажми». */
.tabs__tab[data-active="true"] {
  background: var(--surface-raised);
  color: var(--text);
}

/* Строка достижимости. Одна на всё приложение, у самого верха: человек должен
   видеть её до вставания в очередь, а не в момент собственного хода. */
.reach {
  margin: 0;
  padding: var(--space-2) var(--space-4);
  color: var(--text-secondary);
  font-size: var(--size-caption);
  border-top: 1px solid var(--separator);
}

/* Ни одного канала — это предупреждение, а не подпись. */
.reach[data-none="true"] {
  color: var(--warn);
}

.install {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-raised);
  display: flex;
  gap: var(--space-3);
  align-items: center;
  font-size: var(--size-caption);
}

/* Бейдж ожидающих заявок. Оранжевый: это не кнопка, а состояние — заявки ждут
   решения, и решение принимает человек, а не приложение. */
.badge {
  background: var(--warn);
  color: var(--bg);
  font-weight: 700;
  border-radius: var(--radius-pill);
  padding: 0 var(--space-2);
  font-size: var(--size-caption);
}

/* Пауза: очередь стоит, и вот что об этом написал админ. Оранжевый — состояние,
   а не приглашение нажать. */
.pause {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-raised);
  color: var(--warn);
  font-size: var(--size-caption);
}

/* Баннер: предупреждение перед регистрацией в игре. Оранжевый — цвет
   состояния, а не кнопки: нажимать здесь можно только «ОК». */
.banner {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-raised);
  color: var(--warn);
  display: flex;
  gap: var(--space-3);
  align-items: center;
  font-size: var(--size-caption);
}

/*
 * Модалка об удалении: посреди экрана, поверх затемнённого приложения.
 *
 * Затемнение, а не сплошной фон: человек должен видеть, куда вернётся, и что
 * очередь на месте — чёрный экран вместо приложения читается как поломка. Нажать
 * при этом нельзя ни на что: подложка перехватывает всё до самого нажатия
 * «Понятно».
 *
 * Затемняется яркостью, а не цветом: цвет в этом проекте объявляется только
 * токеном, а токенов ровно одиннадцать и все они заняты делом. Там, где яркость
 * подложки не поддерживается, окно всё равно стоит посреди экрана и всё равно
 * ничего не пропускает — теряется только затемнение.
 */
.modal {
  position: fixed;
  inset: 0;
  z-index: 4;
  display: grid;
  place-items: center;
  padding: var(--space-4);
  backdrop-filter: brightness(0.35) saturate(0.8);
}

/* Карточка уведомления: заголовок и одна кнопка, больше в ней ничего нет. */
.card--notice {
  background: var(--surface-raised);
  display: grid;
  gap: var(--space-4);
  justify-items: start;
}

.sound {
  display: grid;
  gap: var(--space-2);
}

/*
 * Окно настроек. Ниже модалки об удалении: та говорит, что записи больше нет, и
 * спорить с ней громкостью не о чем.
 *
 * Прокрутка внутри карточки, а не страницы: на телефоне в ландшафте высоты
 * меньше, чем в этом окне содержимого, и без неё кнопка «Готово» оказывается за
 * нижним краем — окно, которое нечем закрыть.
 */
.modal--settings {
  z-index: 3;
}

.modal--settings .card {
  max-height: 100%;
  overflow-y: auto;
  display: grid;
  gap: var(--space-4);
}

/* Шестерёнка. Размер сенсорной цели держит кнопка, значок внутри неё меньше:
   значок в 44 пикселя выглядел бы не кнопкой, а иллюстрацией. */
.gear {
  display: block;
  width: 20px;
  height: 20px;
}

/*
 * Пульс: фон всего окна, а не значок в углу.
 *
 * Человек возвращается из полноэкранной игры боковым зрением, и поле цвета он
 * поймает, а бейдж — нет. Цвет несёт состояние, поэтому это оранжевый и
 * красный, а не акцентный синий: синий в этом приложении значит «сюда можно
 * нажать», и ничего больше.
 */
body[data-pulse="warn"] {
  animation: pulse-warn 600ms ease-out;
}

body[data-pulse="danger"] {
  animation: pulse-danger 600ms ease-out;
}

@keyframes pulse-warn {
  0% {
    background: var(--warn);
  }
  100% {
    background: var(--bg);
  }
}

@keyframes pulse-danger {
  0% {
    background: var(--danger);
  }
  100% {
    background: var(--bg);
  }
}

/* Ступенчатая смена насыщенности без движения: сигнал остаётся, уходит
   анимация. Значение поля состояния, а не медиазапрос: спросить систему —
   работа представления, а решение о способе исполнения принимает редуктор. */
body[data-pulse-motion="step"] {
  animation: none;
}

body[data-pulse-motion="step"][data-pulse="warn"] {
  background: var(--surface-raised);
}

body[data-pulse-motion="step"][data-pulse="danger"] {
  background: var(--surface);
}

/*
 * Панель хода: слева кольцо с отсчётом, справа — что происходит и что нажать.
 *
 * Своя очередь ходить — золотая рамка вокруг всей панели: её ловят боковым
 * зрением, вернувшись из полноэкранной игры, раньше, чем прочтут цифры.
 */
.dock {
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  margin: var(--space-4) var(--space-6);
  padding: var(--space-6);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "turn status"
    "turn actions";
  align-items: center;
  column-gap: var(--space-6);
  row-gap: var(--space-4);
}

.dock[data-state="yours"] {
  border-color: var(--accent);
}

/* Отсчёта нет — кольцу нет и места: текст и кнопки встают от края. */
.dock:has(> .turn[hidden]) {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "status"
    "actions";
}

.dock > .status {
  grid-area: status;
  align-self: end;
}

.dock > .turn {
  grid-area: turn;
}

/* Кнопки в одну строку с переносом: панель рассчитана на самый широкий набор,
   который присылает сервер, — выход, продление и пропуск разом. */
.dock__actions {
  grid-area: actions;
  align-self: start;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.dock__actions .button {
  min-height: var(--control);
  flex: 1 1 auto;
}

/*
 * Главное действие — по тому, что человек делает чаще всего.
 *
 * В свой ход это выход: зарегистрировавшись в игре, человек нажимает его
 * каждый раз, чтобы ход ушёл следующему. Продлевают изредка, пропускают реже
 * всего. Поэтому выход крупный, золотой и стоит первой строкой во всю ширину, а
 * продление и пропуск — строкой ниже, и пропуск тише всех.
 *
 * Вне своего хода выход значит другое — потерять место в очереди, — и он
 * остаётся обычной кнопкой: золото не должно звать туда, где ошибка дорога.
 * Вне очереди главное — встать в неё.
 */
.dock[data-state="yours"] .button[data-action="leave"],
.dock__actions .button[data-action="join"] {
  flex: 1 1 100%;
  min-height: 56px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--size-title);
  font-weight: 700;
}

.dock__actions .button[data-action="skip"] {
  background: none;
  color: var(--text-secondary);
}

/* Выключенная кнопка не обещает нажатия ни цветом, ни заливкой. */
.dock[data-state] .dock__actions .button[disabled] {
  background: var(--surface-raised);
  color: var(--text-tertiary);
}

.panel {
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  padding: var(--space-4);
  min-height: 0;
  overflow: auto;
}

/* Чат: лента растягивается, поле ввода прижато к низу и никуда не уезжает.
   Прокручивается лента, а не вкладка целиком — иначе поле, в которое человек
   печатает, уходит с экрана вместе с прочитанными строками. */
/* Подпись, лента, выбор смайлов, форма, отказ, предостережение. Выбор смайлов
   закрыт почти всегда, и его строка тогда схлопывается сама. */
.panel[data-panel="chat"] {
  display: grid;
  /* Явных дорожек две: подпись и лента. Остальное — выбор смайлов, форма, отказ,
     предостережение — встаёт в неявные дорожки, и спрятанное дорожки не
     занимает: иначе каждый скрытый элемент оставлял бы после себя пустой
     промежуток, и под полем ввода копился воздух. */
  grid-template-rows: auto minmax(0, 1fr);
  grid-auto-rows: auto;
  gap: var(--space-3);
  overflow: hidden;
}

.panel[data-panel="chat"] .chat {
  min-height: 0;
}

/* Заголовок блока: значок и слово, жирно. Раньше это была серая подпись
   капителью, и блоки на экране не читались как разделы. */
.panel__label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text);
  font-size: var(--size-ui);
  font-weight: 700;
  margin: 0 0 var(--space-3) 0;
}

.panel__label .icon {
  color: var(--text-secondary);
}

/* Что сейчас с человеком: «Идёт Ваша регистрация», «Вы в очереди…». Крупно:
   это заголовок панели хода, а не подпись к ней. */
.status {
  color: var(--text);
  font-size: var(--size-heading);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0;
}

/* Число секунд до конца хода — в кольце. Цифрами того же начертания, что и
   всё счётное в приложении: цифры пропорционального шрифта прыгают по ширине,
   и отсчёт дёргается на каждой секунде.

   Кольцо — оправа, а не прогресс: полной длины хода клиент не знает, и дуга,
   которая что-то измеряет, обещала бы то, чего нет. */
.turn {
  font-family: var(--font-data);
  font-size: var(--size-countdown);
  font-weight: 700;
  line-height: 1;
  margin: 0;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  width: calc(var(--space-12) * 3);
  height: calc(var(--space-12) * 3);
  border-radius: 50%;
  border: 10px solid var(--surface-raised);
  display: grid;
  place-items: center;
}

/* Приоритет — обводкой номера цветом предупреждения: строка не меняет ни
   размера, ни фона и не соревнуется за внимание с отсчётом хода. */
.queue__row[data-priority="true"] .queue__pos {
  box-shadow: inset 0 0 0 2px var(--warn);
  color: var(--warn);
}

/* Лента. Строки читаются снизу вверх, как в любом чате. */
.chat {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  overflow-y: auto;
}
/* Реплика — автор строкой над текстом, как в мессенджере: так длинное сообщение
   не уезжает вправо от ника и читается одним абзацем. */
.chat__row {
  display: grid;
  gap: 2px;
}
/* Автор не жмётся и не переносится: по нему узнают, кто написал, и обрезанный
   он спорит сам с собой. Переносится реплика — ей это ничего не стоит. */
.chat__author {
  color: var(--text);
  font-size: var(--size-caption);
  font-weight: 700;
  white-space: nowrap;
}
/* Реплика занимает остаток строки и переносится где придётся: слово в сорок
   букв без пробелов иначе растянуло бы ленту вбок вместе со всей вкладкой. */
.chat__body {
  color: var(--text);
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Системная строка — не чья-то реплика, и выглядит иначе: посередине и мельче. */
.chat__row[data-kind="system"] {
  justify-items: center;
}

.chat__row[data-kind="system"] .chat__body {
  color: var(--text-tertiary);
  font-size: var(--size-caption);
}
/* Личная строка адресована одному, и это должно быть видно. */
.chat__row[data-private="true"] .chat__body {
  color: var(--warn);
}
.chat__form {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

/* Отправка — значком, как в мессенджерах: слово «Отправить» съедало в узком
   столбце треть строки, и поле ввода сжималось до щели. */
.chat__send {
  width: var(--control);
  padding: 0;
  flex: 0 0 auto;
}

/* Поле забирает всю ширину, что осталась от кнопок, и сжимается первым — но
   не до нуля. */
.chat__form .form__input {
  flex: 1 1 calc(var(--space-12) * 2);
  min-width: 0;
}

/* Кнопка смайлов у самого поля, как в Telegram: контур серым, без заливки.
   Заливка спорила бы за внимание с кнопкой отправки, а нажимают чаще её. */
.chat__emoji {
  background: none;
  border: 0;
  color: var(--text-tertiary);
  cursor: pointer;
  padding: 0;
  /* Стандартная сенсорная цель: значок внутри неё меньше. */
  min-width: 44px;
  min-height: 44px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}

.chat__emoji[data-open="true"] {
  color: var(--text);
}

.smiley {
  display: block;
  width: 24px;
  height: 24px;
}

/*
 * Выбор смайлов: вкладки разделов и сетка.
 *
 * Стоит над полем ввода и отнимает место у ленты, а не у хода: сколько осталось
 * секунд и какие есть действия — это панель хода, и закрывать её ради смайлов
 * нельзя ни на секунду.
 *
 * Высота в долях экрана, а не в пикселях: на телефоне в ландшафте фиксированная
 * высота съела бы поле ввода вместе с лентой.
 */
.emoji {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--space-2);
  max-height: 40vh;
  padding: var(--space-2);
  background: var(--surface-raised);
  border-radius: var(--radius-panel);
}

/* Вкладки разделов: значки в строку. Переносить их нельзя — две строки вкладок
   съедают сетку, — поэтому строка прокручивается вбок. */
.emoji__tabs {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
}

.emoji__tab {
  background: none;
  border: 0;
  border-radius: var(--radius-button);
  cursor: pointer;
  font-size: var(--size-ui);
  line-height: 1;
  padding: var(--space-2);
  min-width: 44px;
  min-height: 44px;
  flex: 0 0 auto;
  /* Значок раздела — цветной смайл, и приглушать его нечем: серым он бы не
     стал. Открытый раздел отмечается поверхностью под ним. */
  opacity: 0.55;
}

.emoji__tab[data-active="true"] {
  background: var(--surface);
  opacity: 1;
}

.emoji__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  overflow-y: auto;
  /* Прокрутка пальцем не должна утягивать за собой страницу. */
  overscroll-behavior: contain;
}

.emoji__cell {
  background: none;
  border: 0;
  border-radius: var(--radius-row);
  cursor: pointer;
  font-size: var(--size-title);
  line-height: 1;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
}
.online {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.online__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text);
  min-width: 0;
}

/* Ник в одну строку. Перенос на вторую рвал бы список на разновысокие строки
   ровно там, где его читают взглядом сверху вниз; если ник всё же длиннее
   обещанных двадцати символов, он ужимается многоточием, а не ломает список. */
.online__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Кружок с первой буквой ника: картинок у аккаунтов нет и не будет. */
.avatar {
  display: grid;
  place-items: center;
  width: var(--avatar);
  height: var(--avatar);
  border-radius: 50%;
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: var(--size-caption);
  font-weight: 700;
  flex: 0 0 auto;
}

/* Предупреждение о том, чего режим не даёт. Не ошибка и не новость: это то,
   что человек должен прочитать один раз и запомнить. */
.caution {
  color: var(--text-tertiary);
  margin: 0;
}

/* Новость получателю: не ошибка и не состояние очереди, поэтому и цвет свой —
   тот, которым приложение обращает внимание, не пугая. */
.news {
  color: var(--warn);
  margin: 0;
}
/* Свой ход выделен золотым кольцом, чужой — нет: смотреть на чужой отсчёт
   полезно, но он не требует от человека ничего. */
.turn[data-yours="true"] {
  border-color: var(--accent);
}
/* На нуле цвет уходит: экран больше ничего не обещает и ждёт сервер. */
.turn[data-expired="true"] {
  color: var(--text-tertiary);
  border-color: var(--surface-raised);
}

/* Пока состояние не подтверждено сервером, экран говорит об этом прямо. */
.status[data-sync="syncing"] {
  color: var(--text-tertiary);
}

.queue {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.queue__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  border-radius: var(--radius-row);
  padding: var(--space-2) var(--space-3);
}

/* Номер позиции — в кружке одного размера на любой позиции: строка с девятой и
   строка с десятой начинают имя на одной высоте столбца, и глаз, идущий по
   списку сверху вниз, не спотыкается о каждый десяток. */
.queue__pos {
  display: grid;
  place-items: center;
  width: var(--avatar);
  height: var(--avatar);
  border-radius: 50%;
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-family: var(--font-data);
  font-size: var(--size-data);
  font-weight: 700;
  flex: 0 0 auto;
}

.queue__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Своя строка — не псевдоним, а «Вы»: называть свой псевдоним в чате значит
   выдать себя, и подсказывать его лишний раз незачем. Строка приподнята, номер
   в золотом кружке: себя в списке находят первым. */
.queue__row[data-you="true"] {
  background: var(--surface-raised);
}

.queue__row[data-you="true"] .queue__name {
  font-weight: 700;
}

.queue__row[data-you="true"] .queue__pos {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: none;
}

/* Фокус клавиатуры виден всегда и везде: пройти страницу одной клавиатурой,
   ни разу не потеряв, где ты находишься, — требование, а не любезность.
   Цветом основного текста: золото на белом не видно. */
:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
  border-radius: var(--radius-button);
}

@media (max-width: 720px) {
  /*
   * Один столбец на всю ширину. Отступы при этом те же, что и на широком
   * экране: они и там, и там урезаны до шага сетки, и второго набора значений
   * для узкого экрана больше нет — два набора расходятся, а считать место под
   * ник приходится по обоим.
   *
   * Считая от края на трёхстах двадцати: 320 − 24 (края содержимого) − 24
   * (края панели) − 16 (края строки) − 26 (номер и просвет за ним) = 230
   * пикселей на имя. Ник в шестнадцать символов занимает от 132 до 180 —
   * влезает с запасом даже из одних широких букв.
   */
  .shell__body {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Место под закреплённую панель отводится всей оболочке: под ней внизу стоит
     ещё и строка достижимости, и без этого отступа обе они — и последняя строка
     очереди, и строка достижимости — остались бы под панелью навсегда. */
  .shell {
    padding-bottom: var(--dock-height, 0px);
  }

  /* В панели закреплённой панели хода нет, и отводить под неё место незачем:
     это была бы полоса пустоты внизу каждого её раздела. */
  .shell--admin {
    padding-bottom: 0;
  }

  .tabs {
    display: flex;
  }

  /* Открыта ровно одна вкладка. Какая — решает редуктор; здесь только прячут
     остальные, и спрятать «забытую» нельзя: их ровно столько, сколько он
     назвал. */
  .shell__body > .panel[data-active="false"] {
    display: none;
  }

  /*
   * Панель хода закреплена поверх страницы.
   *
   * Отсчёт стоит от нижней границы видимой области, а не окна: экранная
   * клавиатура окно не уменьшает, и панель, поставленная по окну, уезжает под
   * клавиатуру — вместе с оставшимися секундами и кнопкой выхода. Отступ снизу
   * подставляет представление из размеров видимой области.
   */
  /* На телефоне панель уходит из потока вниз и закрепляется там. Строка сетки,
     в которой она стояла, схлопывается сама — закреплённый элемент места в
     сетке не занимает. */
  .dock {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--dock-bottom, 0px);
    z-index: 2;
    margin: 0;
    border-width: 1px 0 0;
    border-radius: var(--radius-panel) var(--radius-panel) 0 0;
    padding: var(--space-4);
    padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
    column-gap: var(--space-4);
    row-gap: var(--space-3);
  }

  /* Кольцо на телефоне меньше: место внизу экрана делят отсчёт и большой
     палец, и кнопкам его нужнее. */
  .dock > .turn {
    width: calc(var(--space-12) * 2);
    height: calc(var(--space-12) * 2);
    border-width: 8px;
    font-size: var(--size-title);
  }

  .status {
    font-size: var(--size-title);
  }

  .shell__header {
    padding: var(--space-3) var(--space-4);
  }

  .shell__body {
    padding: 0 var(--space-4) var(--space-4);
  }
}

/* Экраны до очереди: заявка, вход, статус, смена пароля.
   Все они — одна карточка по центру: до входа человеку нечего выбирать, и
   лишняя раскладка здесь только мешает. */
.gate {
  display: grid;
  place-items: center;
  min-height: 100%;
  padding: var(--space-4);
}

.card {
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  padding: var(--space-6);
  width: 100%;
  max-width: calc(var(--space-12) * 8);
}

.card__title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--size-title);
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-3) 0;
}

.card__note {
  color: var(--text-secondary);
  margin: 0 0 var(--space-4) 0;
}

.form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.form__label {
  color: var(--text-tertiary);
  font-size: var(--size-caption);
}

.form__input {
  background: var(--surface-raised);
  border: 0;
  border-radius: var(--radius-button);
  color: var(--text);
  font: inherit;
  min-height: var(--control);
  padding: var(--space-2) var(--space-3);
  width: 100%;
}

/* Ошибка объясняет, что случилось и что делать, и не извиняется. */
.form__error {
  color: var(--danger);
  font-size: var(--size-caption);
  margin: 0;
}

/* Удача под формой. Красная строка «Заявка отправлена» читается как отказ — и
   человек жмёт кнопку второй раз, что и происходило. Зелёный здесь означает
   состояние, а не кнопку: на кнопке он значит «нажми», вне её — «получилось». */
.form__done {
  color: var(--go);
  font-size: var(--size-caption);
  margin: 0;
}

.form__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.button {
  background: var(--surface-raised);
  border: 0;
  border-radius: var(--radius-button);
  color: var(--text);
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  min-height: var(--control);
  padding: var(--space-2) var(--space-4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

.button:hover:not([disabled]) {
  filter: brightness(1.08);
}

/* Главное действие экрана — золотом: войти, сменить пароль, подтвердить. */
.button--go {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
}

/* Тихое действие: нужное, но редкое. Без заливки, чтобы не спорить с главным. */
.button--quiet {
  background: none;
  color: var(--text-secondary);
}

.button[disabled] {
  color: var(--text-tertiary);
  cursor: default;
}

/* Выключенная подтверждающая кнопка зелёной не остаётся: зелёный здесь обещает,
   что нажатие сработает, а оно пока не сработает. Заодно выключенная кнопка
   любого вида читается на одной и той же поверхности — той, для которой
   контраст этого серого и проверен. */
.button--go[disabled] {
  background: var(--surface-raised);
}

/* Кнопка необратимого действия: удалить, заблокировать, убрать, погасить.
   Красная надпись, а не красная плашка: в карточке таких кнопок бывает две, и
   две красные плашки рядом кричат громче, чем само действие. Плашкой она
   становится только в окне подтверждения — там это последний шаг. */
.button--danger {
  color: var(--danger);
}

.button--danger.button--solid {
  background: var(--danger);
  color: var(--bg);
  font-weight: 600;
}

/*
 * Переключатель темы.
 *
 * Похож на переключатель, а не на значок: значок луны оставляет вопрос, это
 * «сейчас тёмная» или «сделать тёмной». Ручка справа — тёмная, слева —
 * светлая, как в системных настройках. Сенсорная цель — вся кнопка, дорожка
 * внутри неё меньше.
 */
.theme-switch {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  min-width: 44px;
  min-height: 44px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  border-radius: var(--radius-button);
}

.theme-switch__track {
  position: relative;
  display: block;
  width: 44px;
  height: 24px;
  border-radius: 12px;
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 1px var(--separator);
}

/* Ручка акцентная: переключатель — это то, что нажимают. */
.theme-switch__knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  display: grid;
  place-items: center;
  transform: translateX(20px);
  transition: transform 160ms ease;
}

.theme-switch__icon {
  display: block;
  width: 12px;
  height: 12px;
}

.theme-switch__icon--sun,
:root[data-theme="light"] .theme-switch__icon--moon {
  display: none;
}

:root[data-theme="light"] .theme-switch__icon--sun {
  display: block;
}

:root[data-theme="light"] .theme-switch__knob {
  transform: translateX(0);
}

@media (prefers-reduced-motion: reduce) {
  .theme-switch__knob {
    transition: none;
  }
}

/*
 * Панель: шапка во всю ширину, под ней столбец разделов и один открытый раздел.
 *
 * Раньше все шесть разделов стояли сеткой разом, каждый в своём окошке со
 * своей прокруткой, — экран был занят целиком, а прочесть целиком нельзя было
 * ни один. Теперь раздел один и читается сверху вниз, как страница:
 * прокручивается содержимое, а шапка и столбец разделов стоят на месте.
 */
.shell--admin {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-areas:
    "head head"
    "nav body";
}

.shell--admin > .shell__header {
  grid-area: head;
  padding: var(--space-3) var(--space-6);
}

/* Столбец разделов. Открытый раздел — поверхностью и акцентом, а не жирной
   полосой: «ты здесь» должно читаться, но не спорить с содержимым. */
.admin-nav {
  grid-area: nav;
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  gap: var(--space-1);
  padding: var(--space-6) var(--space-4);
  border-right: 1px solid var(--separator);
  overflow-y: auto;
}

.admin-nav .tabs__tab {
  flex: 0 0 auto;
  justify-content: flex-start;
  text-align: left;
  border-radius: var(--radius-button);
  padding: var(--space-3) var(--space-4);
  gap: var(--space-3);
}

.admin-nav .tabs__tab:hover {
  background: var(--surface);
  color: var(--text);
}

.admin-nav .tabs__tab[data-active="true"] {
  background: var(--surface-raised);
  color: var(--text);
}

.shell__body--admin {
  grid-area: body;
  display: block;
  padding: var(--space-8) var(--space-10) var(--space-12);
  overflow-y: auto;
}

/* Открыт ровно один раздел — на любой ширине. Какой — решает редуктор. */
.shell__body--admin > .panel[data-active="false"] {
  display: none;
}

/* Раздел — не окошко, а страница: фона и своей прокрутки у него нет, блоки
   внутри разделены воздухом. */
.admin-section {
  background: none;
  border-radius: 0;
  padding: 0;
  overflow: visible;
  max-width: var(--content-width);
  display: grid;
  align-content: start;
  gap: var(--space-6);
}

.admin-section__head {
  display: grid;
  gap: var(--space-2);
}

.admin-section__title {
  font-size: var(--size-heading);
  font-weight: 650;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0;
}

.admin-section__lead {
  color: var(--text-secondary);
  margin: 0;
  max-width: 64ch;
}

/* Блок раздела. Один уровень поверхности над фоном — и всё: кнопки внутри
   стоят ещё на уровень выше, и этого хватает, чтобы их было видно. */
.admin-card {
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  padding: var(--space-6);
  display: grid;
  gap: var(--space-4);
  min-width: 0;
}

.admin-card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.admin-card__title {
  font-size: var(--size-ui);
  font-weight: 600;
  margin: 0;
}

.admin-card__tools {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.admin-card__note {
  color: var(--text-secondary);
  font-size: var(--size-caption);
  margin: 0;
}

/* Поле с подписью над ним. */
.field {
  display: grid;
  gap: var(--space-2);
}

/*
 * Строки списков: журнал, устройства, входы, сигнализаторы, живая очередь.
 *
 * Строка — это заголовок, под ним подробности мельче, справа время или
 * кнопки. Между строками волосяная линия, а не отдельные плашки: плашка на
 * каждую из сотни строк журнала превращает ленту в мозаику.
 */
.rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
}

.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--separator);
}

.row:first-child {
  border-top: 0;
  padding-top: 0;
}

.row:last-child {
  padding-bottom: 0;
}

.row__main {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.row__title {
  color: var(--text);
  overflow-wrap: anywhere;
}

.row__strong {
  font-weight: 600;
}

.row__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  color: var(--text-secondary);
  font-size: var(--size-caption);
  overflow-wrap: anywhere;
}

/* Время и адреса — моноширинно: их сравнивают глазами столбцом. */
.row__data {
  font-family: var(--font-data);
  font-size: var(--size-data);
}

.row__time {
  color: var(--text-tertiary);
  font-family: var(--font-data);
  font-size: var(--size-data);
  white-space: nowrap;
}

.row__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* Журнал: время слева столбцом — спор разбирают по времени. */
.journal .row {
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
}

/* Строка живой очереди: номер, имя, кнопки. Номер под два знака всегда — как
   и в очереди игрока, чтобы имена стояли ровно. */
.row--queue {
  grid-template-columns: 2ch minmax(0, 1fr) auto;
}

.row__pos {
  color: var(--text-tertiary);
  font-family: var(--font-data);
  font-size: var(--size-data);
  text-align: right;
}

/* Пустой список говорит, что он пуст и почему это нормально, — а не молчит
   пустым местом, которое читается как «не загрузилось». */
.rows__empty {
  color: var(--text-tertiary);
  margin: 0;
}

/*
 * Метка состояния: точка и слово. Цвет несёт состояние — зелёный, оранжевый,
 * красный, — и только он; синий здесь не встречается, синий значит «нажми».
 */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-secondary);
  font-size: var(--size-caption);
  font-weight: 500;
  white-space: nowrap;
}

.chip::before {
  content: "";
  width: var(--space-2);
  height: var(--space-2);
  border-radius: 50%;
  background: currentColor;
}

.chip[data-tone="go"] {
  color: var(--go);
}

.chip[data-tone="warn"] {
  color: var(--warn);
}

.chip[data-tone="danger"] {
  color: var(--danger);
}

.chip[data-tone="muted"] {
  color: var(--text-tertiary);
}

/* Диагностика очереди: четыре показания плитками, неисправность — строкой
   над ними, красным и словами. */
.stats {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-12) * 3), 1fr));
  gap: var(--space-3);
}

.stat {
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  padding: var(--space-4);
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.stat__label {
  color: var(--text-secondary);
  font-size: var(--size-caption);
}

.stat__value {
  margin: 0;
  font-size: var(--size-title);
  font-weight: 600;
  overflow-wrap: anywhere;
}

.stats__alert {
  grid-column: 1 / -1;
  background: var(--surface);
  border-radius: var(--radius-panel);
  padding: var(--space-4);
  color: var(--danger);
  margin: 0;
}

/* Сведения «название — значение» в две колонки: эксплуатация. */
.facts {
  margin: 0;
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-3) var(--space-6);
}

.facts dt {
  color: var(--text-secondary);
}

.facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.facts dd[data-tone="danger"] {
  color: var(--danger);
}

/*
 * Карточка аккаунта. Кто это и в каком он состоянии — в одну строку, крупно;
 * что с ним можно сделать — под ней. Состояние справа, точкой и словом:
 * «Ждёт решения» должно бросаться в глаза в списке из тридцати человек.
 */
.accounts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

.account {
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-panel);
  padding: var(--space-4) var(--space-6);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-3) var(--space-4);
}

.account__avatar {
  display: grid;
  place-items: center;
  width: var(--space-10);
  height: var(--space-10);
  border-radius: 50%;
  background: var(--surface-raised);
  color: var(--text);
  font-weight: 600;
}

.account__main {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.account__name {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.account__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  color: var(--text-secondary);
  font-size: var(--size-caption);
}

/* Кнопки — под именем, с отступом на ширину значка: так они читаются как
   действия над этим человеком, а не как отдельная строка. Опасные — в конце. */
.account__actions {
  grid-column: 2 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.account__push {
  margin-left: auto;
}

/*
 * Меню «…» в карточке аккаунта: одноразовый пароль, блокировка, удаление.
 *
 * Редкие и опасные действия — не кнопками в ряд под каждым именем, а за одним
 * нажатием. Две красные плашки в каждой карточке кричали громче, чем сами
 * действия, и в «Удалить» было легко промахнуться мимо «Одноразового пароля».
 * Раскрывашка встроенная: клавиатура и чтение с экрана работают без кода.
 */
.menu {
  position: relative;
}

.menu > summary {
  list-style: none;
}

.menu > summary::-webkit-details-marker {
  display: none;
}

.menu__list {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 2;
  min-width: calc(var(--space-12) * 5);
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius-row);
  padding: var(--space-1);
  display: grid;
  gap: 2px;
}

.menu__list .button {
  justify-content: flex-start;
  background: none;
  width: 100%;
}

.menu__list .button:hover {
  background: var(--surface-raised);
  filter: none;
}

/* Настройки: строка на поле, между строками линия. Название поля — основным
   цветом: здесь его читают, а не пробегают. */
.settings {
  margin: 0;
  gap: 0;
}

.settings__row {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4) 0;
  border-top: 1px solid var(--separator);
}

.settings__row:first-child {
  border-top: 0;
  padding-top: 0;
}

.settings__row:last-child {
  padding-bottom: 0;
}

.settings__row .form__label {
  color: var(--text);
  font-size: var(--size-ui);
}

.settings__row .form__input {
  max-width: calc(var(--space-12) * 5);
}

/* Флажок — справа от названия, как в системных настройках. */
.settings__row--toggle {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
}

.settings__row--toggle > :not(.form__label):not(.form__check) {
  grid-column: 1 / -1;
}

.form__check {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.settings__effect {
  color: var(--text-tertiary);
  font-size: var(--size-caption);
  margin: 0;
}

.settings__note {
  color: var(--warn);
  font-size: var(--size-caption);
  margin: 0;
}

/*
 * Окно вопроса перед рычагом. Шире окна входа: в нём бывает и текст паузы, и
 * причина, и оба читают целиком.
 */
/* Тема в окне настроек: название слева, переключатель справа. */
.appearance {
  display: grid;
  gap: var(--space-2);
}

.appearance__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.modal--ask {
  z-index: 3;
}

.card--ask {
  max-width: calc(var(--content-width) / 2);
  display: grid;
  gap: var(--space-4);
}

.card--ask .card__title,
.card--ask .card__note {
  margin: 0;
}

textarea.form__input {
  min-height: calc(var(--space-12) * 2);
  resize: vertical;
  line-height: 1.4;
}

.form__actions--end {
  justify-content: flex-end;
  margin-top: 0;
}

/* Одноразовый пароль: крупно, моноширинно, выделяется целиком одним нажатием —
   его диктуют голосом или копируют, и третьего не бывает. */
.secret {
  margin: 0;
  padding: var(--space-4);
  background: var(--surface-raised);
  border-radius: var(--radius-row);
  color: var(--text);
  font-family: var(--font-data);
  font-size: var(--size-heading);
  letter-spacing: 0.08em;
  text-align: center;
  user-select: all;
}

/* Тот же порог, что и выше: панель на телефоне. Отдельным блоком в конце, а не
   в общем, — иначе правила панели, объявленные ниже общего блока, перекрыли
   бы его. */
@media (max-width: 720px) {
  .shell--admin {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr);
    grid-template-areas:
      "head"
      "nav"
      "body";
  }

  /* Шапке на телефоне разрешено переноситься: у админа в ней на кнопку
     больше, и без переноса она раздвигала страницу шире экрана. */
  .shell__header {
    flex-wrap: wrap;
  }

  /* Переключатель темы в шапке очереди на телефоне спрятан — он есть в окне
     настроек. Место наверху принадлежит ходу, а не оформлению. */
  [data-screen="queue"] .shell__header .theme-switch {
    display: none;
  }

  .shell--admin > .shell__header {
    padding: var(--space-3) var(--space-4);
  }

  .admin-nav {
    flex-direction: row;
    flex-wrap: wrap;
    padding: var(--space-2) var(--space-3);
    border-right: 0;
    border-bottom: 1px solid var(--separator);
  }

  .admin-nav .tabs__tab {
    flex: 1 1 auto;
    text-align: center;
    padding: var(--space-2) var(--space-3);
  }

  .shell__body--admin {
    padding: var(--space-6) var(--space-4) var(--space-10);
  }

  .admin-section {
    gap: var(--space-4);
  }

  .admin-card,
  .account {
    padding: var(--space-4);
  }

  /* Кнопки строки уходят под неё: рядом с именем им на телефоне тесно. */
  .row {
    grid-template-columns: minmax(0, 1fr);
  }

  .row--queue {
    grid-template-columns: 2ch minmax(0, 1fr);
  }

  .row--queue > .row__actions {
    grid-column: 2;
  }

  .row__actions {
    justify-content: flex-start;
  }

  .journal .row {
    grid-template-columns: minmax(0, 1fr);
  }

  .account {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .account > .chip {
    grid-column: 2;
  }

  .account__actions {
    grid-column: 1 / -1;
  }

  .account__push {
    margin-left: 0;
  }
}
