/* Экран «Догадайки». Спека 09.
 *
 * ⚠️ Это СТРАНИЦА в браузере, а не слой поверх видео. Прежняя версия была
 * оверлеем для OBS Browser Source, и оттуда тянулись прозрачный фон,
 * полупрозрачные панели и потолок по высоте кадра. Ничего этого больше нет:
 * страницу стримят как есть, она листается, фон под текстом известен.
 *
 * Что осталось важнее красоты:
 *
 * 1. Фон страницы НЕПРОЗРАЧЕН и задан явно. Иначе при захвате окна зритель
 *    поймает белую вспышку системного фона на каждой перезагрузке.
 * 2. Минимум 24 px и жирное начертание: зритель смотрит через сжатый
 *    видеопоток. Мельче 24 px только техническое — HUD и плашки в углу.
 * 3. В переходах и кадрах ТОЛЬКО transform и opacity. Всё остальное
 *    перерисовывает слой каждый кадр, а процессор делится с кодировщиком.
 * 4. [hidden] { display: none !important } — на нём стоит сторожевой тест
 *    tests/cases/overlay-styles.test.js, см. комментарий у правила.
 *
 * Значения палитры, контраста и ритма — в макете, артборд «Система».
 */

/* Onest, переменная по весу 400–800. Кириллица родная.
   Файлы лежат свои: CSP разрешает только font-src 'self', а сети у источника
   может не быть. Два подмножества с unicode-range — браузер тянет только то,
   что реально встретилось: кириллица 14 КБ, латиница 32 КБ.
   font-display: swap, потому что страница стартует в эфире — лучше системные
   буквы сразу, чем пустое место. Подробности и лицензия — fonts/README.md. */
@font-face {
  font-family: 'Onest';
  src: url('fonts/onest-cyrillic.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Onest';
  src: url('fonts/onest-latin.woff2') format('woff2');
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}

:root {
  --page: #0b0e13;
  --page-hi: #11151b;

  /* Поверхность плашки. Считается поверх --page и даёт #181b20 —
     именно от него посчитан весь контраст. */
  --surface: rgba(255, 255, 255, 0.05);
  --surface-hi: rgba(255, 255, 255, 0.08);
  --hairline: rgba(255, 255, 255, 0.09);

  --ink: #eef1f4; /* 15,2 : 1 */
  --ink-soft: #a8b0b9; /* 7,9 : 1 */
  --ink-dim: #9aa3ad; /* 6,8 : 1 */
  --ink-faint: #8c959f; /* 5,7 : 1 */

  /* Теплота. Цвет ВСЕГДА продублирован подписью в .scale-label: половина
     шкалы — красный и зелёный, и часть зрителей их не различает. */
  --win: #f2fbf5;
  --hot: #5bd98c;
  --warm: #f2d047;
  --cool: #f08a46;
  --cold: #ee6b60;

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

     Фиолетовый, а не красный: красный в палитре уже занят — это `--cold`,
     метка «new» и строка «нет такого слова». Второй красный означал бы то же
     самое, что и первый, и повтор слился бы с промахом.

     Замерено, не на глаз: 5,2 : 1 к подложке самого двойника, 6,6 : 1 к
     подложке списка — обе выше пола 4,5. Зелёный был ярче (9,0 : 1), поэтому
     вспышка на приземлении идёт с большей непрозрачностью: см. @keyframes hit. */
  --dup: #b18cf5;

  /* Ширина колонки — ДОЛЯ ОТ ОКНА, а не число пикселей.
     ⚠️ Фиксированный потолок в пикселях отменяет резиновость целиком: выше
     него окно растёт, а колонка стоит колом. Так уже было четыре раза подряд
     (480, 960, 1080, 1400 px) — см. спеку 09, раздел 3.
     Пол 360 px: ниже ужимать нечего, пол текста — 24 px.
     Потолок 1600 px — предохранитель для 4K без масштабирования, а не рабочее
     значение: колонка добирается до него только на окне шире 2500 CSS-пикселей,
     то есть в рабочем диапазоне (850–1700) он не срабатывает никогда. */
  --col: clamp(360px, 64vw, 1600px);
  /* Внешнее поле от краёв окна. ⚠️ Никогда не ноль: раньше колонка была
     `width: 100%` с `max-width` и `margin: auto`, и на узком окне автоотступы
     схлопывались — коробка упиралась в оба края, воздуха не оставалось вовсе.
     На экране с масштабом Windows 225 % это ловится сразу: развёрнутое окно
     1920 даёт вёрстке всего ~853 CSS-пикселя, то есть режим без полей —
     основной, а не краевой случай. */
  --gutter: clamp(16px, 5vw, 96px);

  /* Размеры тянутся за шириной окна, но НИКОГДА не опускаются ниже 24 px:
     это пол из спеки 09, зритель смотрит через сжатый видеопоток. Всё, что
     ниже пола, — только техническое (HUD и плашки в углу). */
  /* Столбец места. Держит и номер, и плашку «new», которая стоит на его месте,
     пока догадка во входящих. Было 46 px — под плашку не хватало, а заодно
     четырёхзначное место (список показывает ВСЕ догадки раунда) в 46 px не
     помещалось и раньше. Ширина одна на все окна: плашка «new» от ширины окна
     не зависит, а вылезти за пределы столбца ей негде. */
  --place-col: 60px;

  --t-word: clamp(24px, 2.1vw, 30px);
  --t-body: clamp(24px, 1.7vw, 26px);
  --t-brand: clamp(26px, 2.4vw, 34px);
  --t-answer: clamp(38px, 5.4vw, 76px);
}

* {
  box-sizing: border-box;
}

html::-webkit-scrollbar,
body::-webkit-scrollbar {
  display: none;
}

/* ⚠️ Без этого правила атрибут hidden не скрывает НИЧЕГО из того, что имеет
   свой display. Браузерное [hidden] { display: none } живёт в таблице агента,
   а любое правило авторской таблицы сильнее неё — специфичность тут ни при
   чём. Итог: .play-head { display: flex } рисует логотип поверх экрана
   победы, а .finish-da { display: inline-flex } показывает кнопку доната,
   когда ссылки нет.

   !important, а не «просто правило»: иначе достаточно завести ниже по файлу
   селектор той же специфичности с display — и всё вернётся, молча.
   Ровно на этом сторож overlay-styles.test.js и стоит. */
[hidden] {
  display: none !important;
}

html,
body {
  margin: 0;
  min-height: 100%;
  /* Страницу захватывают в эфир — полосе прокрутки там делать нечего.
     Колесо и клавиши листают по-прежнему, скрыт только сам ползунок.
     color-scheme: dark на случай, если полосу всё же покажут (другой браузер,
     системная настройка «всегда показывать»): родная светлая полоса на тёмной
     странице выглядит чужеродной заплаткой. */
  color-scheme: dark;
  scrollbar-width: none;
  /* Заливка обязательна: окно захватывают целиком, и незакрашенная страница
     даёт белую вспышку при каждой перезагрузке источника. */
  background: var(--page);
}

body {
  /* Колонка встаёт по центру окна, пока помещается в него. Переросла —
     прижимается к верху и листается. Делает это margin: auto на флекс-
     элементе: при нехватке места автоотступы схлопываются в ноль, и верх
     не срезается. justify-content: center так не умеет — он обрезал бы
     шапку, как только список станет длиннее экрана. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  font-family: 'Onest', 'Segoe UI', system-ui, sans-serif;
  font-weight: 500;
  color: var(--ink);
  font-size: 24px;
  line-height: 1.2;
  -webkit-font-smoothing: antialiased;
}

.overlay {
  /* Ширина считается одним выражением, а не парой width + max-width: так поле
     вычитается ВСЕГДА, и схлопнуться ему негде. */
  width: min(var(--col), 100% - 2 * var(--gutter));
  margin: auto;
  padding: 0 0 48px;
}

/* Строка команд включена: подвал лежит поверх списка, и колонке нужно поле
   ровно на его высоту — иначе хвост списка не долистывается до конца никогда.
   Класс вешает overlay.js по параметру cmds: с выключенной строкой поле не
   нужно, а колонка на коротком списке стоит по центру окна и уехала бы вверх
   на пустом месте.

   ⚠️ Высоту подвала МЕРЯЕТ overlay.js и присылает сюда переменной --foot-h.
   Числом её здесь писать нельзя: она меняется и от текста строки команд, и от
   ширины окна — на узком он переносится на вторую строку, и подвал вырастает.
   Число пришлось бы править руками при каждой правке текста, и никто бы его
   не поправил. Запасное значение на случай, если измерение не доехало: лишний
   воздух под списком не видно, спрятанный хвост списка — видно. */
.has-cmds .overlay {
  padding-bottom: calc(24px + var(--foot-h, 160px));
}

/* ⚠️ Здесь НЕЛЬЗЯ ставить overflow-x на .overlay. Любое значение, кроме
   visible, делает её собственным контейнером прокрутки (overflow-y при этом
   считается auto), и прилипающая шапка начинает липнуть к НЕЙ, а не к окну.
   Прокручивается-то страница — шапка просто уезжает вверх вместе со списком.
   Заодно всё, что шире колонки, обрезалось вместо того чтобы ужаться.
   Ограничение по ширине держат minmax(0, …) в сетке плашки и media ниже. */

/* ── шапка ──
   Прилипает к верху. Фон непрозрачный и совпадает с фоном страницы: список
   уезжает ПОД неё, а не просвечивает сквозь. */
.head {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 0 0 20px;
  background: var(--page);
  border-bottom: 1px solid var(--hairline);
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* Логотип по центру, счётчик попыток прижат вправо (спека 09, раздел 3.1).
   Сетка из трёх колонок, а не «flex + absolute»: боковые доли равны, поэтому
   логотип стоит в настоящем центре, и при этом счётчику некуда наехать на
   название — на узком окне колонка просто раздвигает соседнюю. */
.play-head {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: 20px;
  padding-top: 28px;
}

.brand {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-width: 0;
  text-align: center;
}

.brand-name {
  font-size: var(--t-brand);
  font-weight: 800;
  letter-spacing: 0.16em;
}

/* ⚠️ Ровно половина от --t-body, то есть НИЖЕ пола в 24 px. Решение человека,
   24 августа 2026, и оно третье в закрытом списке исключений (спека 09,
   раздел 6). Держится на том же, на чём метка «new»: подпись не несёт
   информации, которой больше неоткуда взяться, — что игра про смысл слова,
   видно по самой игре. Превратится после сжатия в серую полоску — не потеряно
   ничего. Слово, ник, ранг и название остаются 24 px и больше.
   Числа выписаны явно, а не calc(var(--t-body) / 2): сторож умеет читать
   только литералы, а размер, которого он не видит, однажды уедет молча. */
.brand-tag {
  font-size: clamp(12px, 0.85vw, 13px);
  font-weight: 500;
  color: var(--ink-soft);
}

.attempts {
  grid-column: 3;
  justify-self: end;
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding-top: 8px;
}

/* ── шкала теплоты ── */
.scale {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.scale-track {
  height: 12px;
  border-radius: 999px;
  background: var(--surface-hi);
  overflow: hidden;
}

/* Едет transform, а не width: изменение ширины пересчитывает разметку. */
.scale-fill {
  height: 100%;
  width: 100%;
  border-radius: 999px;
  transform: scaleX(0);
  transform-origin: left center;
  background: var(--cold);
  transition: transform 320ms cubic-bezier(0.22, 0.9, 0.3, 1);
}

.scale-fill[data-level='win'] {
  background: var(--win);
}
.scale-fill[data-level='hot'] {
  background: var(--hot);
}
.scale-fill[data-level='warm'] {
  background: var(--warm);
}
.scale-fill[data-level='cool'] {
  background: var(--cool);
}
.scale-fill[data-level='cold'] {
  background: var(--cold);
}

.scale-text {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.scale-label {
  font-size: 24px;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.scale-label[data-level='win'] {
  color: var(--win);
}
.scale-label[data-level='hot'] {
  color: var(--hot);
}
.scale-label[data-level='warm'] {
  color: var(--warm);
}
.scale-label[data-level='cool'] {
  color: var(--cool);
}
.scale-label[data-level='cold'] {
  color: var(--cold);
}

.scale-best {
  font-size: 24px;
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ── отсчёт до следующего слова ── */
.cool {
  height: 5px;
  border-radius: 0 0 999px 999px;
  background: var(--surface-hi);
  overflow: hidden;
}

.cool-fill {
  height: 100%;
  width: 100%;
  transform: scaleX(1);
  transform-origin: left center;
  background: var(--hot);
  transition: transform 1s linear;
}

/* ── экран итога ──
   Заменяет логотип и шкалу, список под ним остаётся на месте. */
.finish {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding-top: 20px;
  animation: rise 320ms cubic-bezier(0.22, 1.2, 0.5, 1) both;
}

.finish-title {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--ink-faint);
  /* Разрядка добавляет пустоту справа от последней буквы — компенсируем,
     иначе строка визуально уезжает влево. */
  padding-left: 0.3em;
}

.finish[data-win='true'] .finish-title {
  color: var(--hot);
}

.finish-word {
  font-size: var(--t-answer);
  font-weight: 800;
  letter-spacing: 0.14em;
  line-height: 1;
  text-transform: uppercase;
  text-align: center;
  color: #f4f7fa;
  padding-left: 0.14em;
  overflow-wrap: anywhere;
}

.finish-who {
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.finish-foot {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-top: 4px;
}

.finish-next {
  font-size: 24px;
  font-weight: 500;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* Кнопка поддержки живёт только на экране итога, на пике позитива.
   Формулировка — благодарность, а не оплата. */
.finish-da {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-size: 24px;
  font-weight: 600;
  text-decoration: none;
  padding: 7px 18px 8px;
  border-radius: 999px;
  border: 1px solid rgba(91, 217, 140, 0.45);
  color: var(--hot);
}

.finish-da::before {
  content: '';
  width: 20px;
  height: 20px;
  background-color: currentColor;
  /* Сердце нарисовано маской, а не буквой и не эмодзи: так оно наследует
     цвет и не зависит от того, что найдётся в системном шрифте. */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.8 5.6a5 5 0 0 0-7.1 0l-1.7 1.7-1.7-1.7a5 5 0 1 0-7.1 7.1l8.8 8.8 8.8-8.8a5 5 0 0 0 0-7.1z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.8 5.6a5 5 0 0 0-7.1 0l-1.7 1.7-1.7-1.7a5 5 0 1 0-7.1 7.1l8.8 8.8 8.8-8.8a5 5 0 0 0 0-7.1z'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
}

/* ── состояния без игры и служебные строки ── */
.message {
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink-soft);
  padding-top: 28px;
}

.notice {
  font-size: 24px;
  font-weight: 500;
  color: var(--ink-soft);
  white-space: pre-line;
  padding-top: 20px;
}

/* ── входящие ──
   Догадка появляется здесь сразу, как пришла из чата, получает ранг через
   100–300 мс, стоит с ним 1,8 секунды и только потом уезжает в список
   (спека 09, раздел 3.2).

   ⚠️ Секунда с лишним — не украшение. Ранг приезжает быстрее, чем зритель
   успевает перевести взгляд: без задержки плашка появляется и в ту же секунду
   улетает вниз, и он видит вспышку и движение, а прочитать ответ не успевает.

   Столбец, а не строка в перенос: плашка здесь та же, что в списке, во всю
   ширину. Новые сверху — очередь наполняется сверху и утекает вниз, в список,
   всё движение на экране идёт в одну сторону. */
.pending {
  position: relative;
  list-style: none;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px 0 0;
}

.pending:empty {
  display: none;
}

/* ── список догадок ──
   ВСЕ догадки раунда, по возрастанию ранга. Обрезки больше нет: страница
   листается, кадр её не режет. */
.top {
  list-style: none;
  margin: 0;
  padding: 20px 0 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Своя плашка у КАЖДОЙ догадки. Замкнутый контур читается как объект, и глаз
   ведёт его при перестановке мест; строка без границ на смене мест выглядит
   подменой текста, а не движением.

   ⚠️ Плашка ОДНА И ТА ЖЕ во входящих и в списке — один класс, одна ширина,
   одна сетка. Пока у входящих была своя мелкая плашка, узел на переезде менял
   форму прямо в полёте: глаз терял объект вместо того, чтобы вести его. Свести
   форму переходом нельзя — анимировать ширину запрещено, в переходах только
   transform и opacity. */
.row {
  position: relative;
  display: grid;
  grid-template-columns: var(--place-col) minmax(0, auto) minmax(0, 1fr) auto;
  gap: 18px;
  align-items: center;
  padding: 12px 20px 13px;
  border-radius: 16px;
  background: var(--surface);
  /* Едет только transform: раскладку двигает FLIP из render.js.
     Быстро и с торможением в конце: это перестановка мест, она обязана
     закончиться раньше, чем приедет прилетающая плашка (см. .row-land). */
  transition: transform 280ms cubic-bezier(0.2, 0.85, 0.3, 1);
  --heat: var(--cold);
}

.row[data-level='win'] {
  --heat: var(--win);
}
.row[data-level='hot'] {
  --heat: var(--hot);
}
.row[data-level='warm'] {
  --heat: var(--warm);
}
.row[data-level='cool'] {
  --heat: var(--cool);
}
.row[data-level='cold'] {
  --heat: var(--cold);
}

/* Плашка ещё не на своём месте — подложка чуть светлее. Отличие статическое,
   переходом его не ведём: фон анимировать нельзя. */
.pending .row {
  background: var(--surface-hi);
}

/* Лидер отличается не наличием подложки, а её силой и цветом: заливка берёт
   его собственную теплоту и связывает плашку со шкалой в шапке. */
.top .row:first-child {
  background: color-mix(in srgb, var(--heat) 14%, transparent);
}

.row-place {
  font-size: 24px;
  font-weight: 600;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.row-word {
  font-size: var(--t-word);
  font-weight: 800;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-who {
  font-size: 24px;
  font-weight: 500;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-rank {
  font-size: var(--t-word);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 104px;
  color: var(--heat);
}

/* Плашка «new» вместо номера места. Места у догадки во входящих ещё нет —
   оно появляется на приземлении, и пустой столбец там смотрелся дырой.

   Красный — тот же, что у строки «нет такого слова»: другого красного в
   палитре нет, и заводить второй незачем.

   ⚠️ 20 px — ЕДИНСТВЕННОЕ исключение из пола в 24 px для того, что видит
   зритель (спека 09, раздел 6), и оно осознанное. Метка не несёт информации,
   которой больше неоткуда взяться: «эта плашка новая» уже сказано её местом
   над списком и подложкой посветлее. Превратится в кашу после сжатия — прочтётся
   как красное пятно, и этого хватит. Слово, ник и ранг рядом — по-прежнему
   24 px и больше.

   justify-self прижимает плашку к правому краю столбца: подложка обязана
   облегать слово, а не растягиваться на всю ширину места. Правый край при этом
   совпадает с правым краем номеров в списке — плашка садится ровно туда, где
   через секунду встанет цифра. */
.pending .row-place {
  justify-self: end;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: 0.04em;
  line-height: 1.1;
  color: #ee6b60;
  background: rgba(238, 107, 96, 0.16);
  border: 1px solid rgba(238, 107, 96, 0.5);
  border-radius: 999px;
  padding: 4px 6px 5px;
}

/* Двойник повтора метку «new» не носит: слово уже названо и уже стоит в списке,
   а ник с рангом на двойнике — первого автора. «new» на такой плашке врёт.

   Гасится подложка, а не сам столбец: убери узел из сетки — и слово с ником
   уедут влево, разъехавшись с соседями по входящим. Текст в него не пишется
   (см. put() в render.js), так что от метки не остаётся ничего. */
.pending .row[data-dup='true'] .row-place {
  background: none;
  border-color: transparent;
  padding: 0;
}

/* Двойник красится ПОВТОРОМ, а не теплотой слова.

   ⚠️ Раньше он брал `--heat` названного слова и повтор близкого слова летел
   сверху зелёным — тем же цветом, каким экран говорит «почти». Событие при
   этом нулевое: слово уже в списке, кредит у первого автора, ранг не сдвинулся.
   Фиолетовый снимает это совпадение: цвета повтора нет больше нигде, и вопрос
   «а это сейчас что было» не возникает.

   Цвет здесь ОДИН на всю плашку — рамка, подложка и ранг, — потому что читать
   его придётся полсекунды и через сжатый видеопоток. Одна фиолетовая цифра
   в этих условиях теряется.

   Рамка не двигает раскладку: box-sizing: border-box стоит на всём. */
.pending .row[data-dup='true'] {
  background: color-mix(in srgb, var(--dup) 10%, var(--surface-hi));
  border: 1px solid color-mix(in srgb, var(--dup) 45%, transparent);
}

/* Ранг у двойника — чужой: он от ПЕРВОГО автора. Теплота на нём говорила бы
   про слово, а сказать надо про событие. */
.row[data-dup='true'] .row-rank {
  color: var(--dup);
}

/* Ранг ещё едет: вместо числа пульсирует точка. Сетка та же, что у остальных
   плашек: номер придёт в уже отведённый ему столбец, а не раздвинет строку. */
.row[data-wait='true'] .row-rank {
  color: var(--ink-faint);
  animation: blink 1.2s ease-in-out infinite;
}

/* Повтор уже названного слова: двойник рождается во входящих с ником ПЕРВОГО
   автора, уезжает на место существующей плашки и гаснет.

   ⚠️ Вне потока. Иначе двойник занимает место в столбце входящих, отталкивает
   настоящие плашки вниз и двигает за ними весь список — а он живёт полсекунды
   и права двигать раскладку не имеет. Ширину при этом держит сам: у
   абсолютного элемента её больше не от кого взять. */
.row[data-ghost='true'] {
  position: absolute;
  width: 100%;
  margin: 0;
  z-index: 2;
  transition: transform 520ms cubic-bezier(0.5, 0, 0.25, 1), opacity 200ms ease-in;
  opacity: 1;
}

/* Гасится классом, а не сменой data-ghost: смена атрибута выбила бы плашку
   из этого же правила, она потеряла бы position и transform и прыгнула бы
   обратно во входящие прямо на глазах. */
.row[data-ghost='true'].fading {
  opacity: 0;
}

/* Прилёт с верхней строки на своё место в списке.

   ⚠️ Дольше и мягче обычной перестановки мест, и это не украшательство, а
   очерёдность: соседи разъезжаются за 280 мс и открывают место, а плашка идёт
   те же пиксели 520 мс с медленным началом. Читается это как «сначала
   освободилось место, потом плашка туда приехала» — а не как «всё поехало
   разом». Задержкой того же не добиться: плашка на 200 мс замирала бы поверх
   строки, которая как раз в это время встаёт на её прежнее место.

   z-index — чтобы она летела НАД списком, а не ныряла под соседей: без него
   прилёт в середину списка читается как обрезка, а не как глубина. */
.row-land {
  z-index: 1;
  transition: transform 520ms cubic-bezier(0.5, 0, 0.25, 1);
}

/* Прилетевший повтор подсвечивает строку, на месте которой приземлился.
   Подсветка — отдельный слой с анимацией opacity: анимировать background на
   самой плашке нельзя, это перерисовка каждый кадр. */
.row::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* ⚠️ Не `--heat`. Слой зажигается ТОЛЬКО под приземлившийся двойник
     (data-hit ставит fly() и больше никто), и теплота тут врала дважды:
     говорила про слово вместо события и совпадала цветом с «почти угадали».
     Цвет тот же, что у летящей плашки, — иначе прилёт не связывается с тем,
     что прилетело. */
  background: var(--dup);
  opacity: 0;
  pointer-events: none;
}

.row[data-hit='true']::after {
  animation: hit 900ms ease-out both;
}

/* Появление плашки — ТОЛЬКО opacity, без подъёма.
   ⚠️ Кадры анимации сильнее инлайнового стиля: пока в этих кадрах стоял
   transform, плашка, родившаяся меньше секунды назад, переставала слушаться
   FLIP — а FLIP тут единственный способ доехать из входящих в список.
   Движение появления и так есть: новая плашка отталкивает соседей вниз. */
.row-enter {
  animation: appear 220ms ease-out backwards;
}

/* ── строка «нет такого слова» ──
   Отдельно и ненадолго: в список не попадает, иначе редкие слова забьют
   экран. */
.unknown {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-top: 14px;
  font-size: 24px;
  font-weight: 500;
  padding: 9px 20px 10px;
  border-radius: 999px;
  background: rgba(238, 107, 96, 0.1);
  border: 1px solid rgba(238, 107, 96, 0.45);
  color: #c9cfd6;
  animation: rise 260ms cubic-bezier(0.22, 1.2, 0.5, 1) both;
}

/* ── подвал ──
   Спека 09, раздел 3.8. Всё, что прибито к нижнему краю окна, лежит здесь и
   в обычном потоке: строка команд в самом низу во всю ширину, а над ней ряд
   с кнопками слева и плашками справа.

   ⚠️ Так сделано ради одного: магического числа «поднять кнопки на 68 px»
   в файле нет ни одного. Высоту строки команд задаёт её собственный текст, и
   когда на узком окне он переносится на вторую строку, ряд над ним уезжает
   выше сам. Пока кнопки и плашки висели каждая своим position: fixed, любая
   правка текста строки требовала пересчитывать это число руками — и никто бы
   не пересчитал. */
.foot {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  /* ⚠️ Подвал во всю ширину окна и лежит поверх списка. Без этого он ловит
     колесо и клики на пустом месте, и список под ним не пролистать. Право
     нажимать берут себе обратно только кнопки. */
  pointer-events: none;
}

.foot-row {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  /* Те же 24 px от края окна, что были у fixed-кнопок. Со строкой команд это
     поле превращается в зазор между ней и рядом. */
  padding: 0 24px 24px;
}

/* ── кнопки управления ──
   Спека 09, раздел 3.7. Левый нижний угол: правый нижний занят плашками,
   правый верхний — HUD.

   Подписей под кнопками нет: это был бы текст в кадре, который зритель обязан
   прочитать и который ему ничего не даёт. Название действия живёт в title и
   aria-label — они нужны стримеру и программе чтения с экрана, а в видеопоток
   не попадают. Поэтому и пол в 24 px панели не касается: читать тут нечего,
   font-size здесь не объявлен вовсе. */
.panel {
  display: flex;
  gap: 8px;
  pointer-events: auto;
}

.pbtn {
  width: 44px;
  height: 44px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 12px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--ink-dim);
  cursor: pointer;
  /* Только opacity и transform: панель висит в кадре, а процессор делится с
     кодировщиком. Цвет и фон меняются без перехода — скачком. */
  transition: opacity 140ms linear, transform 140ms cubic-bezier(0.22, 1.2, 0.5, 1);
}

.pbtn:hover:not(:disabled) {
  background: var(--surface-hi);
  color: var(--ink);
}

.pbtn:active:not(:disabled) {
  transform: scale(0.92);
}

.pbtn:focus-visible {
  outline: 2px solid var(--ink-soft);
  outline-offset: 2px;
}

/* Недоступное действие гасится, но НЕ исчезает. Панель, у которой кнопки
   пропадают и появляются, промахивается мимо пальца: соседняя кнопка встаёт
   на место той, куда стример целился. */
.pbtn:disabled {
  opacity: 0.28;
  cursor: default;
}

/* Вопрос «точно?». Кнопка, закрывающая живой раунд, выполняется только вторым
   нажатием: промах стоит раунда, и отменить его нечем — слово уже показано. */
.pbtn[data-confirm='true'] {
  color: var(--cold);
  border-color: var(--cold);
  animation: pbtn-ask 900ms ease-in-out infinite;
}

@keyframes pbtn-ask {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

.pico {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ── плашки в углу ──
   Мелко и в углу: техническое, зритель не читает. Единственное место, кроме
   HUD, где текст мельче 24 px. */
.banners {
  /* ⚠️ Правый край держится полем, а не justify-content: space-between у ряда.
     При panel=0 кнопок в ряду нет вовсе, и space-between увёл бы плашки
     к левому краю — туда, где их никто не ищет. */
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

.banner {
  font-size: 20px;
  font-weight: 500;
  padding: 7px 16px 8px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--ink-soft);
}

/* ── строка команд чата ──
   Спека 09, раздел 3.8. Единственное место, где зритель узнаёт, что у игры
   есть команды.

   ⚠️ Пол в 24 px на неё распространяется — в отличие от плашек и кнопок.
   Это текст, который зритель обязан прочитать, и под исключения раздела 6
   он не попадает. Размер общий с остальным текстом: var(--t-body). */
.cmds {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px 32px;
  padding: 12px 24px 14px;
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink-dim);
  /* Фон непрозрачен и совпадает с фоном страницы: список уезжает ПОД строку,
     а не просвечивает сквозь. Тот же приём, что у прилипающей шапки. */
  background: var(--page);
  border-top: 1px solid var(--hairline);
}

/* Команда переносится целиком: разрыв между «!правила» и «как играть» читается
   как две разные строчки, а их всего три. */
.cmd {
  white-space: nowrap;
}

.cmd-name {
  color: var(--ink);
  font-weight: 700;
}

/* ── HUD ──
   Отладочная панель, а не часть игры: тёмная и моноширинная, чтобы её нельзя
   было спутать с экраном и чтобы цифры не прыгали. */
.hud {
  position: fixed;
  top: 24px;
  right: 24px;
  z-index: 3;
  width: 380px;
  background: rgba(8, 10, 14, 0.92);
  border: 1px solid var(--hairline);
  border-radius: 12px;
  padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-soft);
  line-height: 1.45;
}

.hud-head {
  color: var(--hot);
  letter-spacing: 0.2em;
  margin-bottom: 6px;
}

.hud-line {
  display: flex;
  gap: 10px;
}

.hud-key {
  width: 62px;
  color: var(--ink-faint);
}

.hud-val {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hud-sep {
  margin: 6px 0 4px;
  color: var(--ink-faint);
}

.hud-ev {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── движение ──
   Только transform и opacity: всё остальное перерисовывает слой каждый кадр,
   а процессор делится с кодировщиком. */
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes appear {
  from {
    opacity: 0;
  }
}

@keyframes fade {
  to {
    opacity: 0;
  }
}

@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.3;
  }
}

/* ⚠️ 0,30, а не прежние 0,22: вспышка сменила цвет, а яркость у цветов разная.
   Зелёный держал 9,0 : 1 к подложке, фиолетовый — 6,1 : 1, и на прежней
   непрозрачности он читался бледнее там, где обязан читаться ровно так же. */
@keyframes hit {
  0% {
    opacity: 0;
  }
  25% {
    opacity: 0.3;
  }
  100% {
    opacity: 0;
  }
}

/* ── узкое окно ──
   Пол в 24 px запрещает ужимать текст, поэтому ужимается всё остальное, а
   потом ломается сама строка. Обрезать плашку нельзя ни на какой ширине:
   обрезанный ранг — это неверное число на экране, а не «мелкий шрифт». */
@media (max-width: 900px) {

  .row {
    gap: 12px;
    padding: 10px 14px 11px;
  }

  /* Столбец ранга перестаёт держать ширину: на узком окне лишние 104 px —
     это ровно то, чего не хватает нику. */
  .row-rank {
    min-width: 0;
  }

  .foot-row {
    padding: 0 12px 12px;
  }

  .hud {
    top: 12px;
    left: 12px;
    right: 12px;
    width: auto;
  }
}

@media (max-width: 620px) {
  /* Ник уходит на вторую строку. Вчетвером в 600 px слово, ник и ранг
     помещаются только обрезанными, а слово с рангом важнее ника. */
  .row {
    grid-template-columns: var(--place-col) minmax(0, 1fr) auto;
    row-gap: 4px;
  }

  .row-place {
    grid-row: 1 / span 2;
  }

  .row-word {
    grid-column: 2;
    grid-row: 1;
  }

  .row-rank {
    grid-column: 3;
    grid-row: 1;
  }

  .row-who {
    grid-column: 2 / -1;
    grid-row: 2;
  }

  .finish-foot {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .finish-da {
    justify-content: center;
  }

  /* Узкое окно: три колонки складываются в одну, всё по центру. */
  .play-head {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .brand,
  .attempts {
    grid-column: 1;
    justify-self: center;
  }

  .attempts {
    padding-top: 0;
  }
}

/* Движение выключается целиком, состояния остаются на месте. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}
