/* design.css — дизайн-код проекта: шкалы, палитра, движение и компоненты.
 *
 * Зачем он появился. Стили лежали в пяти местах и договорённостей между
 * собой не имели: пятнадцать разных скруглений в одном board.css,
 * двадцать шесть теней, восемь независимых наборов кнопок с отступами
 * 9, 10, 12 и 14 и кеглями 13, 14, 15. Каждый экран рисовался отдельно,
 * и глаз читает это как неряшливость, даже когда не может назвать
 * причину.
 *
 * Что здесь заведено. На каждую шкалу — по нескольку значений, и у
 * каждого значения есть роль. Роль важнее числа: когда значений много
 * и они безымянные, каждый следующий экран берёт своё.
 *
 * Образец — iOS, и взят он не за красоту, а за приём: там выбирать
 * почти не из чего. Один шрифт, шесть кеглей с именами, одна кривая
 * движения, три длительности, ни одной тени внутри экрана. Отсюда и
 * ощущение строгости — не от сухости, а от того, что разночтениям
 * неоткуда взяться.
 *
 * Чего отсюда НЕ брали: палитру. Синяя — своя, была в проекте с самого
 * начала. Тёплую, под дерево доски, пробовали и отвергли: выходила
 * копия chess.com. Системные цвета iOS не брали по той же причине.
 *
 * Где применяется. На всех экранах, которые мы рисуем сами: меню,
 * партия, разбор, задачи. Метка — `body class="cp"` в game.html
 * и index.html, правила пишутся как `body.cp X`.
 *
 * Что НЕ входит в код и не должно. Сама доска: клетки, фигуры,
 * координаты, подсветки ходов. Это не интерфейс вокруг игры, а игра;
 * рисованная графика со своей палитрой, и темам она не подчиняется
 * намеренно. Ещё — затемнение под окнами (`rgba(0,0,0,.45)`): оно
 * гасит то, что под ним, а не красит поверхность, и потому одинаково
 * в любой теме.
 *
 * Чего в коде пока нет. Телефон: раскладка ниже 980 пикселей живёт
 * в board.css и в шкалы не переведена.
 */

:root {
  /* ---------------------------------------------------------- шрифт */

  /* На технике Apple подхватится SF, на Windows 11 — Segoe UI Variable,
   * дальше по списку. Одно семейство на весь проект: разные шрифты для
   * заголовка и текста — это уже два голоса вместо одного. */
  --cp-font: -apple-system, BlinkMacSystemFont, "SF Pro Text",
             "Segoe UI Variable Text", "Segoe UI", system-ui,
             Roboto, Arial, sans-serif;

  /* Кегли — шкала iOS, урезанная до шести ступеней и опущенная на две.
   * У каждой ступени имя и своё дело, и между ними нет промежуточных:
   * не бывает «чуть крупнее подписи». Было тринадцать сочетаний кегля
   * и насыщенности на одном экране задач — включая 12.5, 13, 14, 15,
   * 16, 17 и 18 подряд.
   *
   * Про две ступени вниз. Сперва взяли шкалу как есть, от 17: это норма
   * чтения на телефоне, а приложение открывается и внутри Telegram.
   * Вживую вышло крупно — экран у нас не лист текста, а панель шириной
   * в 260 при доске на восемьсот, и текст в ней не читают, а сверяются
   * с ним. Роли и пропорции остались те же, значения на две ступени
   * мельче. */
  /* Ступень над заголовком: в iOS это Title 1, и берётся она там же,
   * где и здесь, — на шапку окна, открытого во весь экран. Заведена
   * 9 сентября 2026 по прямой просьбе: шкала кончалась на двадцати,
   * а шапки окон меню попросили крупнее. Выше неё в iOS только Large
   * Title (34), и он для страницы, а не для окна. */
  --cp-title-l:  700 28px/1.25 var(--cp-font);   /* шапка окна меню */
  --cp-title:    700 20px/1.30 var(--cp-font);   /* заголовок окна */
  --cp-headline: 600 15px/1.33 var(--cp-font);   /* кнопка, главная строка */
  --cp-body:     400 15px/1.33 var(--cp-font);   /* основной текст */
  --cp-subhead:  400 13px/1.38 var(--cp-font);   /* второстепенный */
  --cp-footnote: 400 12px/1.33 var(--cp-font);   /* подпись */
  --cp-caption:  400 11px/1.18 var(--cp-font);   /* мелкое: ярлыки, счётчики */

  /* Межбуквенное, одно на все ступени. SF поджимает крупный кегль сам,
   * Segoe UI — нет, поэтому подтягиваем вручную и понемногу. Второе
   * значение, потуже, было заведено под кегль 17; на пятнадцати оно
   * поджимало заметно, а ради одного заголовка держать вторую шкалу
   * незачем. */
  --cp-track: -.015em;

  /* -------------------------------------------------------- движение */

  /* Одна кривая на весь проект. В коде их было пять, и `ease` из них
   * стоял семьдесят один раз — то есть не выбранный, а доставшийся
   * по умолчанию.
   *
   * Эта — с сильным торможением и без отскока: быстро трогается,
   * долго успокаивается. Так у Apple ездят листы и переходы, и именно
   * это на глаз читается как «плавно», а не как «медленно». */
  --cp-ease: cubic-bezier(.32, .72, 0, 1);

  /* Второй кривой пришлось завести ровно одну: для того, что не едет,
   * а появляется — плашки, значки, всплывающие списки. Лёгкий перелёт
   * в конце показывает, что предмет прилетел и встал, а не проявился. */
  --cp-ease-pop: cubic-bezier(.2, 1.2, .4, 1);

  /* Четыре длительности. Было тринадцать, да ещё в двух единицах
   * сразу: .06, .08, .12, .14, .15, .16, .18, .2, .22, .28, .3
   * и рядом 120ms, 140ms, 160ms, 170ms.
   *
   * Четвёртая, самая короткая, заведена не сразу и по надобности.
   * Три остальные описывают движение, на которое смотрят: нажали —
   * ответило, открыли — раскрылось. А есть движение, на которое
   * не смотрят: карточка разбора догоняет свой новый рост, пока
   * человек листает ходы. Такому нельзя быть заметным вовсе — на
   * перемотке следующая смена приходит раньше, чем доедет прежняя, —
   * и даже быстрая ступень тут читается запаздыванием. */
  --cp-t-snap: .1s;    /* догоняющее: то, что обязано поспевать */
  --cp-t-fast: .2s;    /* отклик органа управления: нажатие, наведение */
  --cp-t:      .3s;    /* появление и раскрытие */
  --cp-t-slow: .5s;    /* большое перемещение: окно, лист */

  /* ====================================================================
   * ЧЕМ ДВИГАТЬ. Памятка на будущее, писана кровью четырёх заходов.
   * ====================================================================
   *
   * Первый вопрос перед любой анимацией: есть ли в том, что движется,
   * БУКВЫ. Если нет — бери что угодно, `opacity` и `transform` дёшевы
   * и правильны (доска, фигуры, точки подсказок, затемнение). Если
   * есть — дальше по таблице.
   *
   *   что делаем            чем                      чего нельзя
   *   ------------------------------------------------------------------
   *   едем вверх/вниз       transform: translateY    top, margin-top
   *   едем вбок             left                     (можно и transform)
   *   появляемся            visibility, без opacity  opacity
   *   исчезаем              opacity                  —
   *   гасим только текст    color                    opacity
   *   отклик на нажатие     фон (--cp-press),        transform, opacity
   *                         поджатие outline-ом
   *
   * ПОЧЕМУ. Механизма два, и они тянут в разные стороны.
   *
   * 1. Слой композитора. `opacity`, `transform`, `filter`,
   *    `backdrop-filter`, `will-change` уводят элемент в отдельный слой.
   *    Пока он там, текст сглаживается серым: чем заполнен фон под ним,
   *    ещё не решено. Кончилась анимация — слой схлопывается, и весь
   *    текст разом перерисовывается подпиксельно. Один кадр, но он
   *    виден: буквы меняют вес. Отсюда и правило «появляемся без
   *    прозрачности»: приход кончается видимым текстом, а уход — нулём,
   *    и там предъявить рывок некому.
   *
   * 2. Растеризация строк. Skia ставит строку по ЦЕЛЫМ пикселям
   *    по вертикали и с точностью до ЧЕТВЕРТИ пикселя по горизонтали.
   *    Поэтому движение отрисовкой (`top`, `margin-top`) по вертикали
   *    даёт расхождение: рамка кнопки едет дробно и гладко, подпись
   *    внутри стоит и прыгает через целый пиксель. Виднее всего в конце,
   *    где кривая с торможением ползёт медленнее пикселя за кадр.
   *    По горизонтали (`left`) шага в четверть глаз не видит, и там
   *    отрисовка годится.
   *
   * ЛОВУШКИ, на которых уже спотыкались.
   *
   *  — `opacity` на родителе утаскивает в слой всех детей. Затемнение
   *    должно быть СОСЕДОМ окна, а не его коробкой (`.go-dim` рядом
   *    с `.go-card`, `#friendBackdrop` рядом с `.menu-modal`).
   *  — Элемент, перекрывающий анимируемый слой, браузер уводит в слой
   *    за компанию. Поэтому затемнение под карточкой появляется сразу,
   *    а не проявляется.
   *  — `background-color` слоя не заводит, но кладётся на главном
   *    потоке: на весь экран это перерисовка каждого кадра вместе
   *    с доской. Для затемнения не годится.
   *  — `.999` вместо единицы плюс `will-change` рывок убирает, но
   *    оставляет серое сглаживание навсегда. Пробовали, отвергли:
   *    текст всё время чуть легче соседнего.
   *  — Наведение. Гасить или сдвигать что-то при `:hover` прозрачностью,
 *    `transform` или `filter` — тот же слой, только на каждом наведении:
 *    подпись рядом на миг мылится и резко становится чёткой. Так было
 *    с чертой-разделителем в переключателе цвета (13 сентября 2026).
 *    Гасить цветом (`background-color`, `color`) — слоя нет. Новые
 *    такие правила ловит `test_наведение_не_уводит_текст_в_слой`;
 *    в его списке исключений — только то, в чём нет букв.
 *    ОГОВОРКА (27 октября 2026): «слоя нет» верно не всегда. Переход
 *    `background-color` Chromium умеет вести на видеокарте и тогда
 *    на время перехода отрывает фон в слой — ник в плашке профиля
 *    мылился ~0,3 с. Случилось там, где элемент внутри уже отдельного
 *    слоя (угол «тема + профиль»); у кнопок меню — нет. По тексту CSS
 *    этого не отличить, поэтому ловит живой браузер: `test_hover_layers.py`
 *    обходит все экраны в Edge без окна и падает, если наведение или
 *    нажатие на чём-то с буквами добавляет слой. Лечится переходом
 *    зарегистрированной переменной цвета (`@property`, пример —
 *    `--chip-bg` у `.profile-chip` в index.html): он идёт в основном
 *    потоке, без слоя.
 *  — Прятать текст на время движения (`color: transparent`) — рабочий
   *    приём, но пустая карточка на треть секунды заметнее, чем один
   *    кадр смены сглаживания. Пробовали, отвергли.
   *
   * РОСТ ВМЕСТЕ С БУКВАМИ. Если должно вырасти то, в чём текст, и
   * текст должен расти вместе с ним (QR в приглашении, «Следующая
   * задача» в ожидании), масштаб не годится — это слой, см. выше.
   * Растим настоящим размером. Одна длина объявляется через
   * `@property` (syntax "<length>"), и только тогда браузер ведёт её
   * плавно: обычная переменная для него текст и переключается разом.
   * От этой длины считаются размер, кегль, колонка сетки — всё, что
   * растёт. Переход ставится на саму переменную. Образцы: `--iv-qr`
   * в index.html и `--pz-grow` в puzzle-ui.js.
   *
   *  — Соседи должны расступаться, а не стоять. Растёт в раскладке —
   *    всё рядом едет следом кадр в кадр. Пробовали держать соседей
   *    отрицательными полями (элемент растёт поверх) — выглядит
   *    нелепо: одно пухнет посреди застывшего, а на узкой панели ещё
   *    и вылезает за край (24 октября 2026).
   *  — Без перелёта. Кривая `--cp-ease-pop` на кегле — это буквы,
   *    которые на миг больше конечных и откатываются: читается
   *    дрожью, а не пружиной. Для роста с текстом — `--cp-ease`.
   *
   * ТИШИНА ВОКРУГ. Плавное движение рядом с мгновенным выглядит
   * рывком, как бы ни было сделано само движение. Если в тот же кадр,
   * что началась анимация, рядом что-то появилось, исчезло или
   * сменило высоту, — всё ниже прыгает разом, и глаз видит прыжок,
   * а не анимацию. Так было с «Следующей задачей»: в миг решения
   * строка «Ваш ход» пустела, мотивы появлялись, кнопка прыгала на
   * 9 пикселей — ровно когда начинала расти. Лечится не анимацией,
   * а раскладкой: то, что сменяет друг друга, ставить в одну клетку
   * сетки (`grid-area:1/1`) с постоянной высотой, а не друг под
   * другом. Проверять замером: координата соседа до и после события
   * должна совпадать.
   *
   * КАК ПРОВЕРЯТЬ. В скрытой вкладке переходы стоят на нулевой отметке,
   * и `getComputedStyle` отдаёт стартовое значение вместо конечного.
   * Мерить так: клонировать узел, поставить клону `transition: none`
   * и читать с клона — либо влить в страницу `*{transition:none}`.
   * Иначе половина замеров окажется выдумкой. Промежуточный кадр
   * снимается без кадров вовсе: `el.animate(...)`, пауза, `currentTime`
   * на середину — и читать размеры.
   */

  /* -------------------------------------------------------- отступы */

  /* Шаг 4: всё, что между, — повод спросить себя зачем. */
  --cp-s1: 4px;
  --cp-s2: 8px;
  --cp-s3: 12px;
  --cp-s4: 16px;
  --cp-s5: 24px;
  --cp-s6: 32px;

  /* ----------------------------------------------------- скругления */

  /* Три, и каждое привязано к роду предмета, а не к его размеру. */
  /* Восемь, а не десять: отметки в ленте решённых — квадратики 22×22,
   * и от десятки у них не остаётся прямой стороны, выходит кружок. */
  --cp-r-s: 8px;       /* строки списков, плитки, поля, отметки */
  --cp-r-m: 14px;      /* карточки, панели, окна */
  --cp-r-pill: 999px;  /* кнопки и ярлыки */

  /* ------------------------------------------------------------ тень */

  /* Тень в проекте одна, и та — только у того, что висит НАД экраном:
   * окно, всплывающий список, плашка. Всё, что лежит на экране,
   * отделяется от фона цветом.
   *
   * Так это устроено в iOS, и причина не в моде: тень под карточкой
   * говорит «этот прямоугольник приподнят», а приподнят он не бывает —
   * он часть страницы. Когда приподнято всё, приподнятого нет.
   * У нас теней было двадцать шесть; на одном экране задач — пять. */
  --cp-e-over: 0 20px 60px rgba(0, 0, 0, .45);

  /* ---------------------------------------------------------- цвета */

  /* Палитра своя, прежняя: тёмно-синяя. Поверхности разведены на три
   * различимые ступени (раньше панель и карточка почти совпадали),
   * приглушённый текст чуть светлее — он держал 4.6 к фону, впритык.
   *
   * Вся лестница опущена на тон: панель была #161d2d и читалась
   * серо-синей, пользователь попросил темнее — образцом дал #0e1621,
   * «можно даже на тон темнее». Взят этот тон ниже, ступени между
   * поверхностями сохранены. Графит за ней не опускали — его не просили. */
  --cp-bg: #060a12;
  --cp-surface: #0d1420;
  --cp-surface-2: #141c2a;
  --cp-surface-3: #1c2537;
  --cp-line: rgba(255, 255, 255, .08);
  --cp-line-strong: rgba(255, 255, 255, .16);

  --cp-text: #e8edff;
  --cp-muted: #8b99c4;

  /* Синий акцент — тот же, что был. Подпись на нём тёмная, а не белая:
   * белое на этом синем даёт контраст 2.9, то есть надпись на главной
   * кнопке читалась бы хуже, чем всё остальное на экране. */
  /* Две роли, а не одна.
   *
   * `--cp-accent` — цвет подсветок и пометок: подложки значков, текущий
   * ход, выбранная ячейка, «Подсказка» и «Решение», кольцо фокуса.
   * Его серая тема забирает себе и делает светлым: синий там оказался
   * единственным цветным пятном и читался обломком другой темы.
   *
   * `--cp-primary` — цвет главного действия, и он синий во всех тёмных
   * темах. Синий — признак проекта, и на главной кнопке он к месту:
   * это не мелочь, которая пестрит, а одно действие на экран.
   *
   * Различать их пришлось именно потому, что серая тема попросила
   * убрать синеву отовсюду, кроме главной кнопки. Пока цвет был один,
   * такой просьбы нельзя было выполнить, не потеряв половину смысла.
   */
  --cp-accent: #6aa7ff;
  --cp-accent-hover: #7fb4ff;
  --cp-accent-ink: #08131f;

  --cp-primary: #6aa7ff;
  --cp-primary-hover: #7fb4ff;
  --cp-primary-ink: #08131f;

  /* Фон призрачной кнопки. В синей теме его нет: там «Подсказку»
   * и «Решение» отличает от подписи рядом цвет, и этого довольно.
   * В серой теме акцент светлый, цвет сказать этого уже не может —
   * и тему подставляет едва заметная заливка. Роль объявлена здесь,
   * чтобы правило кнопки про темы не знало. */
  --cp-btn-ghost-bg: transparent;

  --cp-ok: #5fe28a;
  --cp-danger: #ff7a7a;
  /* Значок бота. Синий — тот же, что у акцента; на тёмном
   * светлее, на светлом темнее — как ok и danger. */
  --cp-bot: #6aa7ff;

  /* Подпись на сплошной красной заливке. Та же роль, что у
   * `--cp-primary-ink` на синей: сам `--cp-danger` — цвет текста на
   * тёмном, он для этого и подобран светлым, и текст на нём читать
   * нечем. Почти чёрный держит к нему 9.1. */
  --cp-danger-ink: #1a0b0b;

  /* Значок на цветной заливке — белый, и это не то же самое, что
   * подпись на ней.
   *
   * Подпись — буквы в пятнадцать пунктов, их читают, и им нужен запас
   * контраста: оттого `--cp-primary-ink` и `--cp-danger-ink` тёмные.
   * Значок — рисунок в две линии, его не читают, а узнают по силуэту,
   * и решает тут не контраст, а то, одного ли он цвета с соседями.
   *
   * Соседей у наших двух трое: шестерёнка, ползунки и стрелки полного
   * экрана. Они серые и набраны `--cp-text`, то есть почти белым. Пока
   * значки на синей и красной кнопке были тёмными, ряд читался двумя
   * наборами — «эти светлые, а эти почему-то нет». Белый собирает все
   * пять в один.
   *
   * Значение одно на все темы: заливка под значком своя в любой из них
   * (синяя, красная), и белое ложится на неё одинаково.
   */
  --cp-ink-on-fill: #ffffff;

  /* Отклик на нажатие. Плёнка, которой кнопка притемняется под пальцем:
   * кладётся поверх любой заливки, включая её отсутствие. Восемнадцать
   * процентов — столько же, сколько глаз замечал в прежней просадке
   * на три процента и в гашении на пятую долю. */
  --cp-press: rgba(0, 0, 0, .18);

  /* Сплошная заливка плиток над доской — зелёная и красная.
   *
   * Почему не `--cp-ok` и `--cp-danger`, которые уже есть. Те двое —
   * цвета текста на тёмном, светлые по назначению: `#5fe28a` и
   * `#ff7a7a`. Залив ими плитку целиком, получаешь мятную и розовую
   * плашки, а белый значок на них держит 1.6 и 2.3, то есть тонет.
   *
   * Порядок здесь обратный обычному: чернила заданы (белые — так
   * решено для всего ряда), значит подстраиваться должна заливка.
   *
   * Синий тут пробовали — тот же, что у кнопки «Реванш» в карточке
   * итога, чтобы одно действие в двух местах выглядело одинаково.
   * Отказались: `--cp-primary` светлый, белое на нём 2.45 при пороге
   * 3, и значок пришлось уводить в тёмный — а вместе с ним разъезжался
   * весь ряд, где у соседа белый флаг. Ряд дороже совпадения.
   *
   * Откуда числа. Взяты со шкалы качества хода (`--cp-q-best`
   * и `--cp-q-blunder`), где ровно эта задача уже решена: там это
   * подложка под кружком с белым знаком внутри, и она потому и темнее
   * своих чернил, что знак обязан читаться.
   *
   * И приглушены оттуда дважды, потому что первого раза не хватило.
   *
   * Дело в площади. Шкала качества ставит свой цвет кружком в 22
   * пикселя — точкой рядом с ходом. Тот же цвет, растянутый на плитку
   * 40 на 110, занимает в двадцать с лишним раз больше места, и то,
   * что точкой читалось пометкой, плиткой читается вывеской.
   *
   * Поэтому цвет уведён к самим поверхностям: светлота 27 и 26 против
   * 19 у `--cp-surface-3`, цветность 33 и 30. Это уже не «зелёная
   * кнопка», а «панель с зелёным в ней» — ступень между серой плиткой
   * соседа и краской. Цвет по-прежнему говорит, что будет, но говорит
   * вполголоса, как и всё остальное в проекте.
   *
   * Белое на них 7.7 и 9.2: чем темнее заливка, тем больше запаса
   * у значка. Порог для рисунка (не для текста) — 3.
   *
   * Значение одно на все темы, как и у `--cp-ink-on-fill`: заливка
   * несёт свой контраст с собой и от поверхности под ней не зависит.
   */
  --cp-good-fill: #2e5c43;
  --cp-good-fill-hover: #36694d;
  --cp-danger-fill: #6b3a3a;
  --cp-danger-fill-hover: #7a4444;

  /* Качество хода — четыре ступени: лучший, неточность, ошибка, зевок.
   *
   * И у каждой ступени два значения, потому что работы две.
   *
   * Заливка — под кружком в сводке и под значком на клетке доски.
   * Внутри там белый знак (✓, ?!, ??), и заливка обязана быть тёмной,
   * иначе знак не прочесть.
   *
   * Чернила — пометка возле хода в колонке и точка на графике оценок.
   * Это текст и метки на тёмной панели, и они обязаны быть светлыми.
   *
   * Разница между парами — не оттенок, а светлота: тон один и тот же.
   *
   * Заводя эту шкалу, я свёл обе работы в одно значение и привязал
   * концы к палитре: `--cp-ok` и `--cp-danger`. Но в палитре это цвета
   * текста на тёмном — они потому и светлые. Кружки от этого посветлели
   * на два десятка единиц L*, а белая галочка внутри «лучшего» стала
   * держать контраст 1.65 вместо 3.17, то есть перестала читаться.
   *
   * Старый код это различие знал, просто не проговаривал: там зевок был
   * `#d64545` в сводке и `#ff6a6a` на графике, и выглядело это как
   * небрежность. Небрежностью было отсутствие имён, а не сами числа.
   */
  --cp-q-best: #27a55a;
  --cp-q-inaccuracy: #d6ac1f;
  --cp-q-mistake: #e2701f;
  --cp-q-blunder: #d64545;

  --cp-q-best-ink: #5fe28a;
  --cp-q-inaccuracy-ink: #ecd04d;
  --cp-q-mistake-ink: #ef8330;
  --cp-q-blunder-ink: #ff6a6a;
}

/* ------------------------------------------------------- вторая тема

 * Графит. Та же тема, что была в проекте до дизайн-кода («как тёмная
 * у ChatGPT»), возвращённая и слегка перекрашенная.
 *
 * Почему её пришлось возвращать. Переключатель тем никуда не девался
 * и исправно писал `data-theme="warm"`, но менял он только старые
 * переменные board.css — `--bg`, `--panelSolid`. Экран к тому времени
 * читал уже одни `--cp-*`, а их для графита никто не объявил, и
 * переключение не меняло ровно ничего.
 *
 * Как подобраны значения. Не на глаз: взята лестница светлот синей
 * темы и повторена в нейтральном тоне. У синей ступени идут
 * L* 5.5 → 10.8 → 14.7 → 19.5, здесь 5.1 → 10.2 → 14.1 → 19.3 —
 * расхождение нигде не больше шести десятых. Именно одинаковые
 * ступени и делают две темы одной работой в двух цветах: меняется
 * оттенок, а не устройство.
 *
 * Тон: тёмно-серый, тепло на одну единицу. Красный и зелёный вровень,
 * синий на единицу ниже — меньше в восьми битах не бывает.
 *
 * Число искали четырьмя заходами, и теперь измерены все края.
 *
 *   синего больше   — «серый»: рядом с синей темой холодный серый
 *                     выглядит её выцветшей версией, отдельной темой
 *                     не звучит;
 *   +7…+10          — «слишком тёплый»: уголь начинает отдавать
 *                     деревом, а тёплую палитру под доску в проекте
 *                     уже пробовали и отвергли, выходила копия
 *                     chess.com;
 *   +3…+5           — всё ещё тёплый;
 *   +2              — тёплый;
 *   +1              — принято.
 *
 * Тут стоит остановиться и на слове «серый». Первый заход тоже был
 * серым, и его забраковали — но забраковали не серость, а холод.
 * Серый бывает тёплым и холодным, и разница между «выцветшей синей»
 * и «тёмно-серой» — одна единица в другую сторону.
 *
 * Строение оттенка тоже поменялось. Пока разница была большой, зелёный
 * шёл серединой между красным и синим: вровень с красным он даёт
 * оливковый, вровень с синим — розовый. На одной единице выбирать
 * приходится из двух: либо красный один над двумя (розоватый серый),
 * либо красный с зелёным вровень над синим (желтоватый). Тёплый серый
 * в природе желтоватый, поэтому второе.
 *
 * Лестница светлот держится: 5.4 / 10.7 / 14.6 / 19.4 против 5.5 /
 * 10.8 / 14.7 / 19.5 у синей. Контрасты: основной текст к панели 14.53
 * против 14.42, приглушённый 5.99 против 5.97, акцент 6.90 против 6.88.
 *
 * Акцент не трогали. Он тут не украшение темы, а признак проекта.
 */
body[data-theme="warm"] {
  --cp-bg: #121211;
  --cp-surface: #1d1d1c;
  --cp-surface-2: #252524;
  --cp-surface-3: #2f2f2e;

  --cp-text: #eeeeed;
  --cp-muted: #9a9a99;

  /* Акцент здесь светлый, а не синий.
   *
   * Синий — признак проекта, и в синей теме он им и остаётся. Но на
   * сером он оказался единственным цветным пятном на экране и читался
   * как обломок другой темы: подложки значков в меню, главные кнопки,
   * подсветка текущего хода, ячейка в сводке — всё это одна переменная,
   * и все они разом выбивались.
   *
   * Почти белый вместо него делает тему строго чёрно-белой. Именно так
   * устроена тёмная у ChatGPT, на которую эта тема и была похожа
   * с самого начала. Контраст к панели 14.1 — вдвое выше, чем был
   * у синего.
   *
   * Плата: цвет больше не может сказать «сюда можно нажать». Поэтому
   * призрачные кнопки получают здесь едва заметную заливку — восемь
   * процентов акцента. В синей теме её нет, там хватает цвета.
   */
  --cp-accent: #ebebea;
  --cp-accent-hover: #f7f7f6;

  /* `--cp-primary` здесь намеренно не переопределён: главная кнопка
   * остаётся синей и в серой теме. Это не забытая строка. */

  /* Чернила на акцентной кнопке — тот же тёмный, что у фона:
   * 15.7 к светлому акценту. */
  --cp-accent-ink: #121211;

  --cp-btn-ghost-bg: color-mix(in srgb, var(--cp-accent) 8%, transparent);
}

/* ------------------------------------------------- светлая (заморожена)

 * Значения оставлены целиком и рабочими, но из выбора тем светлая
 * убрана до отдельного захода: проверена она была только на экране
 * задач, а экранов теперь пять.
 *
 * Ниже — те же роли, другие значения; ни одно правило про это не знает.
 */
body[data-theme="light"] {
  --cp-bg: #f2f3f5;
  --cp-surface: #ffffff;
  --cp-surface-2: #f4f6f9;
  --cp-surface-3: #e8ecf3;
  --cp-line: rgba(17, 24, 39, .10);
  --cp-line-strong: rgba(17, 24, 39, .18);

  --cp-text: #111827;
  --cp-muted: #53627b;

  --cp-accent: #2563eb;
  --cp-accent-hover: #3b76f0;
  --cp-accent-ink: #ffffff;

  --cp-primary: #2563eb;
  --cp-primary-hover: #3b76f0;
  --cp-primary-ink: #ffffff;

  --cp-ok: #16a34a;
  --cp-danger: #dc2626;
  --cp-bot: #2563eb;
  /* Здесь красный тёмный, и подпись на нём светлая — как и на синей
   * кнопке этой темы. */
  --cp-danger-ink: #ffffff;

  /* Панель здесь белая, и текст на ней просит того же тёмного, что
   * и заливка: чернила совпадают с ней, а не светлеют. */
  --cp-q-inaccuracy: #b8860b;
  --cp-q-mistake: #c2560d;

  --cp-q-best-ink: #15803d;
  --cp-q-inaccuracy-ink: #b8860b;
  --cp-q-mistake-ink: #c2560d;
  --cp-q-blunder-ink: #b91c1c;

  --cp-e-over: 0 20px 60px rgba(17, 24, 39, .18);
}

/* ------------------------------------------------------------ шрифт: выбор */

/* «Шрифт» в настройках — три подачи текста, ключ `fontStyle`
 * (ui.js), метка `data-font-style` на <html>. Заведено 28 октября
 * 2026: искали шрифт «помягче системного», и каждый вариант кому-то
 * лучше, кому-то хуже, — пользователь решил дать выбор, а не выбирать
 * за всех.
 *
 *   plain — как было: системный, ничего не меняется (умолчание);
 *   soft  — тот же системный, но текст на ступень тише, без подтяжки
 *           букв, заголовки 600 вместо 700, кнопки 500 вместо 600;
 *   rubik — свой шрифт Rubik (ниже).
 *
 * Меняются только переменные: шрифт, межбуквенное, ступени кегля и
 * цвет текста. Правила компонентов о выборе не знают. */

/* Rubik. Лицензия SIL OFL 1.1 — можно всё, включая коммерческое, лишь
 * бы рядом с файлами лежал fonts/rubik/OFL.txt. Не удалять его.
 *
 * Файлы собраны сами. Google отдаёт Rubik переменным и без хинтинга, и
 * на Windows мелкий кегль был мыльным. Поэтому из переменного шрифта
 * нарезаны четыре статичных начертания — ровно наши ступени, 400, 450,
 * 600 и 700, — урезаны до латиницы и кириллицы и прогнаны через
 * ttfautohint. Остальные насыщенности браузер берёт у ближайшего.
 *
 * Скачиваются файлы только у того, кто выбрал Rubik: браузер грузит
 * @font-face, лишь когда на странице есть текст этим семейством.
 * Версия исходника (v31) стоит в имени — кэш вечный (web_backend.py). */
@font-face {
  font-family: "Rubik";
  font-weight: 400;
  font-display: swap;
  src: url("fonts/rubik/rubik-v31-hinted-400.woff2") format("woff2");
}
@font-face {
  font-family: "Rubik";
  font-weight: 450;
  font-display: swap;
  src: url("fonts/rubik/rubik-v31-hinted-450.woff2") format("woff2");
}
@font-face {
  font-family: "Rubik";
  font-weight: 600;
  font-display: swap;
  src: url("fonts/rubik/rubik-v31-hinted-600.woff2") format("woff2");
}
@font-face {
  font-family: "Rubik";
  font-weight: 700;
  font-display: swap;
  src: url("fonts/rubik/rubik-v31-hinted-700.woff2") format("woff2");
}

/* Про насыщенность на Windows. Chrome видит у Segoe UI только готовые
 * начертания — 300, 350, 400, 600, 700 — и промежуточные округляет:
 * 450 рисуется как 400, 500 — как 600. Так что `--cp-headline: 500`
 * облегчает кнопки только на Apple (SF переменный), на Windows они
 * остаются 600. Заголовки 600 вместо 700 работают везде. */
:root[data-font-style="soft"] {
  --cp-track: 0;
  --cp-title-l:  600 28px/1.25 var(--cp-font);
  --cp-title:    600 20px/1.30 var(--cp-font);
  --cp-headline: 500 15px/1.33 var(--cp-font);
}
/* «Чуть жирнее, чтобы был мягче» (28 октября 2026). Насыщенностью
 * этого не сделать — 450 на Windows тот же 400, а 600 уже полужирный.
 * Поэтому штрих утолщён обводкой на доли пикселя: тонкий яркий штрих
 * читается резко, чуть плотнее и чуть тише — спокойно. Обводка цвета
 * текста (по умолчанию currentColor) и наследуется, так что ложится на
 * весь текст. Подобрано на глаз: .15 почти незаметно, .25 уже жирно. */
:root[data-font-style="soft"] body.cp {
  -webkit-text-stroke: .2px;
}
/* Кроме эмодзи — у цветного знака обводка цвета текста ляжет контуром
 * поверх рисунка — и доски: координаты на ней — игра, а не интерфейс
 * (см. «Что НЕ входит в код» в шапке файла). */
:root[data-font-style="soft"] body.cp .emoji,
:root[data-font-style="soft"] body.cp .board-wrapper {
  -webkit-text-stroke: 0;
}
/* Цвет текста задаёт тема (на body), поэтому и тише его делаем у темы.
 * Контраст к фону: синяя 17 → 14.3, графит примерно так же. Светлая
 * заморожена и не трогается.
 *
 * Заодно старый `--text` из board.css и index.html: у двух десятков
 * правил (окна, настройки, плитки задач) цвет взят из него, и без
 * привязки они остались бы ярче остального. */
:root[data-font-style="soft"] body.cp[data-theme="dark"] {
  --cp-text: #d5dbeb;
  --text: var(--cp-text);
}
:root[data-font-style="soft"] body.cp[data-theme="warm"] {
  --cp-text: #dcdcdb;
  --text: var(--cp-text);
}

/* У Rubik межбуквенное ноль: он и так плотный, с подтяжкой под Segoe
 * мелкие буквы слипались. Мелкие ступени — 450: тонкий штрих в 11–13
 * пикселей на тёмном читался бледно. Системные в списке — на долю
 * секунды, пока файл не пришёл, и на знаки, которых в Rubik нет. */
:root[data-font-style="rubik"] {
  --cp-font: "Rubik", -apple-system, BlinkMacSystemFont, "SF Pro Text",
             "Segoe UI Variable Text", "Segoe UI", system-ui,
             Roboto, Arial, sans-serif;
  --cp-track: 0;
  --cp-subhead:  450 13px/1.38 var(--cp-font);
  --cp-footnote: 450 12px/1.33 var(--cp-font);
  --cp-caption:  450 11px/1.18 var(--cp-font);
}

/* ------------------------------------------------------------ кнопка */

/* Один компонент на все случаи: вид задаёт `--cp-btn-bg`, размер —
 * модификатор. Восемь наборов кнопок разъезжались именно потому, что
 * каждый следующий экран заводил себе свой.
 *
 * Кнопка плоская. Прежде у неё был нижний край в три пикселя, за
 * который её как бы берут пальцем, и нажатие проседало ровно на него.
 * Приём честный, но он из другого языка — из того, где предметы на
 * экране притворяются вещами. В iOS кнопка не притворяется ничем:
 * это заливка со словом, а что её нажали, показывают уменьшением
 * и прозрачностью. Отказ от края — самое заметное, что здесь сделано,
 * и без него всё остальное было бы косметикой.
 *
 * Высота 40, главное действие — 46. У Apple нижняя граница того, во
 * что уверенно попадают пальцем, — 44, и сперва держались её. На узкой
 * панели четыре кнопки такой высоты подряд оказались тяжелее всего
 * остального на экране, и высоту опустили: сорок — всё ещё уверенное
 * попадание, а не тридцать шесть, на которых начинают мазать.
 */
.cp-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--cp-s2);

  min-height: 40px;
  padding: 0 var(--cp-s4);
  border: 0;
  border-radius: var(--cp-r-pill);

  font: var(--cp-headline);
  color: var(--cp-btn-ink, var(--cp-text));
  background: var(--cp-btn-bg, var(--cp-tap));
  cursor: pointer;
  box-shadow: none;

  transition: background var(--cp-t-fast) var(--cp-ease),
              color var(--cp-t-fast) var(--cp-ease),
              opacity var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease);
}

/* Наведение подсвечивает саму заливку, а не всю кнопку целиком:
 * фильтр `brightness` берёт кнопку вместе с подписью и высветляет
 * и её тоже. В iOS наведения нет вовсе — оно здесь только потому,
 * что в это играют и мышью. */
.cp-btn:hover {
  background: var(--cp-btn-bg-hover,
              color-mix(in srgb, var(--cp-btn-bg, var(--cp-tap)) 90%, #fff));
}

/* Нажатие — краской, а не масштабом. Одно правило на все кнопки
 * проекта, и вот почему их пришлось собрать в одно место.
 *
 * Было: `transform: scale(.97)` плюс `opacity: .8`. Величины подобраны
 * верно — глаз замечает, рука не читает как «уехало», — но оба свойства
 * считаются на видеокарте. Браузер выносит кнопку в отдельный слой,
 * а в слое буквы сглаживаются серым; на отпускании слой схлопывается,
 * и подпись разом перерисовывается подпиксельно. Тот же рывок, что
 * ловили на окнах, только в момент, когда палец уже уходит.
 *
 * Стало: плёнка `--cp-press` поверх заливки. `background-image` —
 * свойство краски, перерисовывается на месте, слоя не заводит. Плёнка
 * кладётся на что угодно: на синюю кнопку, на серый значок, на строку
 * вовсе без фона (там она притемняет то, что под строкой).
 *
 * Мгновенно, без плавности: отклик на нажатие обязан совпадать
 * с нажатием. Задержка в две десятых, которую глаз прощает окну,
 * на кнопке читается тормозом.
 */
.cp-btn:active,
body.cp .top-actions .btn:active,
body.cp .review-btn:active,
body.cp .chat-compose button:active,
body.cp .ui-x:active,
body.cp .go-btns button:active,
body.cp .go-again:active,
body.cp .ui-actions .btn:active,
body.cp .menu-back:active {
  background-image: linear-gradient(var(--cp-press), var(--cp-press));
}

/* НАЖАТИЕ — четыре режима на весь проект, выбор в настройках интерфейса
 * («Нажатие кнопок»). Метку `data-press-fx` на <html> ставит press.js,
 * он же даёт цвет вокруг кнопки и плавное отпускание.
 *
 *   film — плёнка `--cp-press`, и теперь у всякой кнопки, а не только
 *          у перечисленных выше: часть кнопок не отвечала вовсе.
 *   ios  — плёнка и поджатие: заливка уходит внутрь на полтора-два с
 *          половиной пикселя. Не масштабом — это слой, буквы мылятся
 *          (памятка «ЧЕМ ДВИГАТЬ»). Сжимается только краска: по краю
 *          встаёт обводка цвета фона вокруг (`--bt-around`), толщиной
 *          `--bt-sq`. Обводка, а не внутренняя тень: тени у кнопок
 *          бывают свои, и общее правило стирало бы их на время нажатия.
 *          `outline` повторяет скругление, так что заливка остаётся той
 *          же формы, только меньше. При фокусе с клавиатуры не трогаем —
 *          там обводка занята кольцом фокуса.
 *   off  — плёнка прозрачная, и все правила выше гаснут разом.
 *   was  — «как было раньше»: общих правил нет, каждая кнопка отвечает
 *          своим (выше и по файлам страниц). Пользователь попросил
 *          оставить и его, чтобы можно было вернуться.
 *
 * Селектор `:is(…):not(…)` пишется слитно: перенос строки между ними —
 * это пробел, то есть «потомок кнопки», и правило молча не находит ничего.
 *
 * Выбор фигуры при превращении нарисован градиентом — плёнка его
 * заменила бы, и кружок под фигурой на миг пропадал. Он исключён. */
@property --bt-sq { syntax: "<length>"; inherits: false; initial-value: 0px; }

html[data-press-fx="off"] { --cp-press: transparent; }

html:is([data-press-fx="ios"], [data-press-fx="film"])
  :is(button, .btn, [role="button"]):not(:disabled, [aria-disabled="true"], .promo-option):active {
  background-image: linear-gradient(var(--cp-press), var(--cp-press));
}

html[data-press-fx="ios"]
  :is(button, .btn, [role="button"]):not(:disabled, [aria-disabled="true"], .promo-option, :focus-visible) {
  outline: var(--bt-sq) solid var(--bt-around, transparent);
  outline-offset: calc(var(--bt-sq) * -1);
}
html[data-press-fx="ios"]
  :is(button, .btn, [role="button"]):not(:disabled, [aria-disabled="true"], .promo-option):active {
  --bt-sq: var(--bt-sq-on, 1.5px);
}

.cp-btn:focus-visible {
  outline: 2px solid var(--cp-accent);
  outline-offset: 3px;
}

.cp-btn:disabled {
  opacity: .35;
  cursor: default;
  transform: none;
}

/* Главное действие: сплошная заливка акцентом. */
.cp-btn--primary {
  --cp-btn-bg: var(--cp-primary);
  --cp-btn-bg-hover: var(--cp-primary-hover);
  --cp-btn-ink: var(--cp-primary-ink);
}

/* Второе по важности: серая заливка, обычная подпись. */
.cp-btn--secondary {
  --cp-btn-bg: var(--cp-tap);
  --cp-btn-bg-hover: var(--cp-tap-hover);
  --cp-btn-ink: var(--cp-text);
}

/* Третьестепенное: одно слово, набранное акцентом.
 *
 * Раньше оно было приглушённо-серым и читалось как недоступное.
 * В iOS кнопка без заливки — синяя: цвет и есть единственный признак,
 * по которому слово отличают от подписи рядом.
 *
 * Боковые отступы здесь меньше: они нужны, чтобы отвести подпись от
 * края заливки, а заливки нет. Высоту в 44 кнопка держит по-прежнему —
 * попадать пальцем надо и в неё. */
.cp-btn--ghost {
  --cp-btn-bg: var(--cp-btn-ghost-bg);
  --cp-btn-ink: var(--cp-accent);
  padding: 0 var(--cp-s2);
}

.cp-btn--ghost:hover {
  background: var(--cp-accent-soft);
}

/* Во всю ширину и повыше — так в iOS выглядит главное действие листа. */
.cp-btn--lg {
  width: 100%;
  min-height: 46px;
}

/* ------------------------------------------------------------ карточка */

/* Карточка отделяется от фона цветом — ни рамки, ни тени. Светлый
 * контур в 1 пиксель обводил у нас всё подряд, и экран превращался
 * в набор наклеек; тень под каждой поверхностью говорила, что
 * приподнято всё сразу, то есть ничего. */
.cp-card {
  background: var(--cp-surface);
  border: 0;
  border-radius: var(--cp-r-m);
  box-shadow: none;
}

/* ----------------------------------------------- экраны приложения */

/* Ниже — замена прежнего оформления на новое. Всё под `body.cp`: метку
 * несут и меню, и партия, и разбор, и задачи — все страницы, которые мы
 * рисуем сами. Раньше здесь стоял `body.cp-puzzle`, и код жил на одном
 * экране: так и было задумано, чтобы было с чем сравнивать. Сравнили —
 * перевели остальные.
 *
 * Сперва обвязка, общая для всех экранов, потом то, что есть только
 * в одном месте.
 *
 * Доски это не касается вовсе: клетки, фигуры, координаты и подсветки
 * ходов остаются те же. Меняется рама вокруг них.
 */
body.cp {
  /* Производные от акцента считаются здесь, а не в `:root`.
   *
   * `var()` внутри значения переменной подставляется на том элементе,
   * где переменная объявлена. Объяви `--cp-accent-soft` в `:root` —
   * и он навсегда возьмёт корневой акцент, потому что тему ставит
   * `body`, элементом ниже. Пока акцент во всех темах был один, это
   * молчало; со светлым акцентом серой темы подложки значков в меню
   * остались бы синими.
   */
  --cp-accent-soft: color-mix(in srgb, var(--cp-accent) 16%, transparent);

  /* Заливка всего, что нажимается, — кроме главного действия (оно
   * `--cp-primary`) и крашеных кнопок партии.
   *
   * Заведена 13 сентября 2026: кнопка настроек, перемотка, кнопка-дата
   * в разборе, «← Меню» были залиты кто `surface-3`, кто `surface-2`,
   * кто цветом панели, а при наведении одни светлели, другие темнели.
   * Пользователь: нажимаемое должно быть одного цвета. Теперь всё
   * нажимаемое берёт заливку отсюда, а наведение одинаково светлит.
   *
   * Здесь, а не в `:root`, по той же причине, что `--cp-accent-soft`. */
  --cp-tap: var(--cp-surface-3);
  --cp-tap-hover: color-mix(in srgb, var(--cp-surface-3) 90%, #fff);

  background: var(--cp-bg);
  /* Цвет текста здесь, а не только у отдельных правил: всё, что его
   * не задаёт само, наследует именно отсюда. Без этой строки заголовки
   * меню, имя игрока и переключатель подсветки брали старый `--text`
   * из board.css — и в графите оказывались светлее остального текста
   * на экране. */
  color: var(--cp-text);
  font-family: var(--cp-font);
  letter-spacing: var(--cp-track);
}

body.cp::before { display: none; }   /* синее свечение из board.css */

/* ------------------------------------------------------ полосы прокрутки

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

   Красим темой. Ползунок — `--cp-line-strong`, тот же цвет, каким
   в палитре отмечены разделители: он и здесь отделяет, только вдоль.
   Жёлоб прозрачный: под ним уже есть поверхность, и вторая ему не
   нужна.

   Два написания, потому что правил два и оба живые. `scrollbar-*` —
   стандарт, его понимают Firefox и свежий Chromium; `::-webkit-*` —
   старое расширение, и только оно даёт ползунку скругление. Заданы
   оба, и они не спорят: где работает стандарт, расширение к нему
   не применяется.

   Поля вокруг ползунка сделаны прозрачной рамкой с `background-clip`,
   а не шириной: так дорожка остаётся во всю ширину и попасть в неё
   мышью по-прежнему легко, а видимая полоска выходит тонкой. */
body.cp,
body.cp * {
  scrollbar-width: thin;
  scrollbar-color: var(--cp-line-strong) transparent;
}

body.cp ::-webkit-scrollbar { width: 12px; height: 12px; }
body.cp ::-webkit-scrollbar-track { background: transparent; }
body.cp ::-webkit-scrollbar-thumb {
  background: var(--cp-line-strong);
  border-radius: var(--cp-r-pill);
  border: 3px solid transparent;
  background-clip: content-box;
}
body.cp ::-webkit-scrollbar-thumb:hover { background: var(--cp-muted); background-clip: content-box; }
body.cp ::-webkit-scrollbar-corner { background: transparent; }

/* СПИСОК В ОКНЕ. Как прокручивается всё, что длиннее окна: история игр,
   наборы задач в меню и на экране задач, список партий в разборе.

   1. Прокручивается список, а не окно. Шапка с заголовком и крестиком
      стоит на месте. Окно, прокрученное целиком, уносило крестик вверх,
      а его полоса прилипала к краю окна и выглядела чужой (пользователь,
      24 октября 2026: «скролл иной»).

   2. Полоса — в правом поле окна, а не вплотную к строкам. Список
      заходит в поле отрицательным правым отступом и отдаёт часть обратно
      полем: строки кончаются по краю шапки (под крестиком), полоса стоит
      за ними. Числа зависят от поля окна: при поле 18 — `margin-right:
      -14px; padding-right:4px`, при поле 24 — `-20px` и `8px`.

   3. Место под полосу держится всегда: `scrollbar-gutter:stable`.
      Иначе у короткого списка строки шире, чем у длинного.

   4. Потолок двойной: `max-height:min(60vh, calc(100vh - 200px))`.
      Шестьдесят процентов экрана — на обычном экране; второе — на
      низком, где окно само не выше экрана без 96, а шапке с полями
      нужно ещё 88. Без него на низком экране прокручивались и список,
      и окно: две полосы рядом.

   5. Сетка с потолком ужимает строки под доступную высоту, и плитки
      слипаются. Сетке, которая прокручивается, — `grid-auto-rows:
      max-content` (или `align-content:start`).

   6. Вид полосы — общий, выше: тонкая, цвета разделителя, жёлоб
      прозрачный. Своей раскраски у списков нет.

   Образцы: `.hs-list` и `#puzzlesPanel .pz-grid` в index.html,
   `.pzt-grid` в puzzle-ui.js, `.rv-drop-in` в review-ui.js. Следит
   `test_список_в_окне_прокручивается_по_правилам`. */

body.cp .brand-panel,
body.cp .chat-panel,
body.cp .moves-panel,
body.cp .pz-tape,
body.cp .menu-back,
body.cp .top-actions .btn {
  background: var(--cp-surface);
  border: 0;
  border-radius: var(--cp-r-m);
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  color: var(--cp-text);
}

/* Карточка слева — по содержимому, а не во всю колонку.
 *
 * `.chat-panel` заведена под переписку, и ей положено расти: в партии
 * внизу поле ввода, сообщения копятся вверх, и панель должна занимать
 * всё, что осталось от колонки (board.css: `flex: 1`).
 *
 * В задачах внутри неё стоит панель задачи — она ни от чего не зависит
 * и расти не собирается. Растянутая карточка давала под ней четыреста
 * пикселей пустой тёмной поверхности: коробку, в которой ничего нет.
 * Пустота сама по себе не мешает — мешает пустота, обведённая в рамку.
 */
body.cp-puzzle .chat-panel {
  flex: 0 0 auto;
  height: auto;
}

/* Доска. Своё скругление в 16 и своя тень — чёрная на семьдесят
 * процентов — были пятым радиусом и пятой тенью на экране; в светлой
 * теме тень к тому же оставалась чёрной. Доска и так самое контрастное
 * пятно, приподнимать её нечем и незачем. */
body.cp .board-wrapper {
  /* Фон рамы брался из старого `--panelSolid` — единственного, что
   * ещё переключалось само. В графите он уезжал на свой оттенок, пока
   * панели вокруг оставались синими. */
  background: var(--cp-surface);
  border-radius: var(--cp-r-m);
  box-shadow: none;
}

/* Значки-кнопки наверху правой колонки (интерфейс, полный экран).
 * Единственное, что оставалось на старом наборе `.btn`: своя тень,
 * свой кегль 18, свои отступы. Теперь круглые, ровно 44 — как всё,
 * во что надо попасть. */
body.cp .top-actions .btn {
  /* Четыре свойства ради одного квадрата — и каждое отбивает своё.
   * В board.css у `.btn.icon` стоит `height: 44px` и `min-width: 44px`.
   * Порог перебивает ширину, а высоту порогом не перебить: сначала
   * вышло 40 на 44, потом 44 на 40. Перекос виден сразу при любом
   * скруглении: с таблеткой выходил овал вместо круга, со скруглённым
   * квадратом — прямоугольник с разными углами. */
  width: 40px;
  min-width: 40px;
  height: 40px;
  min-height: 40px;
  padding: 0;

  /* Скруглённый квадрат, а не кружок.
   *
   * Таблетка (`--cp-r-pill`) заведена для кнопки с подписью: там
   * скругление равно половине высоты и просто продолжает форму строки.
   * На квадрате 40 на 40 та же таблетка превращает кнопку в круг, и
   * ряд из них читается не кнопками, а цепочкой кружков — тем более
   * теперь, когда два из них крашеные.
   *
   * `--cp-r-s` — ступень «плитки», и значок-кнопка это она и есть:
   * восемь от сорока — те же примерно двадцать процентов стороны, на
   * которых держатся значки приложений в iOS. Углы мягкие, но угол
   * виден. */
  border-radius: var(--cp-r-s);
  background: var(--cp-tap);
  font: var(--cp-body);
  transition: background var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease),
              opacity var(--cp-t-fast) var(--cp-ease);
}

/* Ряд занимает ширину колонки, а кнопки делят её поровну.
 *
 * Правая колонка — стопка плашек во всю ширину: часы, история ходов,
 * часы. Кнопки были единственным, что этому не подчинялось: три
 * квадратика 40 на 40, прижатых к левому краю, и двести пикселей
 * пустоты справа от них. Колонка от этого начиналась не строкой,
 * а обрывком строки.
 *
 * Тянутся сами кнопки, а не добавлена коробка вокруг них: коробка
 * дала бы четвёртую плашку в колонке, где их и так три, а нужна была
 * не плашка, а строка нужной длины.
 *
 * Высота остаётся 40 — та же, что у часов под ними. Ширина выходит
 * около 110, то есть плитка становится вытянутой; значок в ней
 * по-прежнему 18 и стоит посередине. Это ровно тот вид, каким в iOS
 * набран сегментированный переключатель, и читается ряд теперь одной
 * вещью из трёх частей, а не тремя вещами подряд.
 */
body.cp .top-actions.side-actions {
  gap: var(--cp-s2);
}

body.cp .top-actions.side-actions .btn {
  flex: 1 1 0;
  width: auto;
  min-width: 0;

  /* Сорок четыре, а не сорок. Четыре пикселя — плата за слово, которое
   * выезжает при наведении: значку надо куда-то подняться, а слову куда-то
   * встать. На сорока они делили бы одну строку и сходились впритык.
   *
   * Сорок четыре — то самое число, вокруг которого в проекте уже был спор
   * (см. выше про квадрат): в iOS это рост, на котором перестают мазать
   * пальцем. Пока кнопки были кружками, четыре лишних пикселя портили
   * круг; плитке они не мешают. */
  height: 44px;
  min-height: 44px;

  /* Слово лежит поверх плитки, а не в потоке: иначе значок при наведении
   * не поднимался бы, а его выталкивало бы вверх появившейся строкой —
   * это другое движение и выглядит оно рывком. */
  position: relative;

  /* `overflow: hidden` отсюда снят, и это не уборка, а лечение.
   *
   * Он стоял, пока слово выезжало снизу и его надо было обрезать за
   * краем плитки. Слово больше не едет, обрезать нечего — а вот вред
   * от него остался: скруглённая коробка с `overflow: hidden`, внутри
   * которой что-то движется на видеокарте, заставляет браузер завести
   * слой-маску на всю кнопку. В таком слое подпиксельное сглаживание
   * выключается для всего содержимого разом — включая слово, которое
   * мы только что от этого и уводили.
   *
   * То есть подпись чинилась, а кнопка вокруг неё продолжала ломать:
   * лечишь одно место, а слой заводится этажом выше.
   */
}

/* Значок и слово при наведении.
 *
 * В покое видно один значок, он посередине. Наводишь — значок уезжает
 * вверх, освобождая место, и снизу проявляется слово. Отпустил — всё
 * возвращается тем же путём.
 *
 * Зачем: три плитки со значками — ребус, и разгадывать его приходится
 * каждому, кто зашёл впервые. Раньше это решала подсказка браузера
 * (атрибут title): она всплывает через секунду, рисуется системой мимо
 * темы и встаёт где ей угодно — на снимке экрана она легла поверх
 * соседней плашки с часами. Теперь подпись своя, приходит сразу и
 * стоит там, где ей место. Атрибут title с этих трёх кнопок снят,
 * aria-label остался: он для тех, кто наведением не пользуется.
 *
 * Длительность — `--cp-t`, ступень «появление и раскрытие». Это ровно
 * те триста миллисекунд, о которых просили; своего числа заводить
 * не пришлось.
 */
/* Значок едет полем, а не сдвигом.
 *
 * `transform` — вторая половина той же беды. Он тоже считается на
 * видеокарте, тоже заводит слой, и слой этот живёт внутри кнопки рядом
 * со словом. Даже сняв обрезку, мы оставляли бы браузеру повод собрать
 * кнопку из кусков.
 *
 * `margin-bottom` — свойство раскладки. Оно пересчитывается на каждом
 * кадре обычным путём, слоёв не заводит, и текст рядом всё это время
 * остаётся ровно таким же, каким был. Плата — пересчёт раскладки
 * шестьдесят раз в секунду; для одного значка 22 на 22 внутри кнопки
 * это ничто. Там, где такой пересчёт стоил бы дорого — колонка ходов
 * на сто строк, — так делать нельзя, и там по-прежнему `transform`.
 *
 * Шестнадцать, а подъём выходит восемь: значок стоит в кнопке по
 * середине вместе со своими полями, и поле снизу поднимает его на
 * половину себя.
 */
body.cp .top-actions.side-actions .btn svg {
  margin-bottom: 0;
  transition: margin-bottom var(--cp-t) var(--cp-ease);
}

body.cp .top-actions.side-actions .btn:hover svg,
body.cp .top-actions.side-actions .btn:focus-visible svg {
  margin-bottom: 16px;
}

body.cp .btn-word {
  position: absolute;
  left: 0;
  right: 0;
  /* Четыре снизу — и ровно столько же остаётся между словом и поднятым
   * значком: 3 сверху, значок 22, 2 просвета, слово 13, 4 снизу.
   * Сорок четыре разошлись без остатка. */
  bottom: var(--cp-s1);

  font: var(--cp-caption);
  letter-spacing: var(--cp-track);
  text-align: center;
  pointer-events: none;

  /* Появляется краской, а не прозрачностью, — и в этом вся разница.
   *
   * Прозрачность и сдвиг браузер считает на видеокарте: он выносит
   * элемент в отдельный слой, а в слое, у которого есть прозрачность,
   * текст сглаживается серым — чем заполнен фон под ним, ещё не
   * решено. Дойдя до единицы, слой схлопывается в общий холст и
   * перерисовывается подпиксельно: буквы разом меняют вес в последнюю
   * миллисекунду движения. Это и видно как рывок в конце.
   *
   * Приём из index.html (`.999` плюс `will-change`) рывок убирает, но
   * ценой: слой остаётся навсегда, и серое сглаживание тоже навсегда —
   * текст всё время чуть мягче, чем соседний. Для окна меню, которое
   * видно секунду, это не плата; для подписи, на которую смотрят
   * в упор, — плата.
   *
   * Поэтому здесь третий путь: меняется не прозрачность элемента,
   * а прозрачность краски. `color` от `transparent` к цвету надписи —
   * это перерисовка на месте, без отдельного слоя. Текст всё время
   * лежит на непрозрачной плитке, подпиксельное сглаживание не
   * выключается ни на кадр, и переключаться в конце нечему.
   *
   * Сдвига у слова нет по той же причине: `transform` вынес бы его
   * в слой не хуже прозрачности. Движение в этой кнопке одно — вверх
   * едет значок, и оно на своём месте: у значка нет букв, портить
   * ему нечего.
   */
  color: transparent;
  transition: color var(--cp-t) var(--cp-ease);
}

body.cp .top-actions.side-actions .btn:hover .btn-word,
body.cp .top-actions.side-actions .btn:focus-visible .btn-word {
  color: inherit;
}

body.cp .top-actions .btn:hover {
  background: var(--cp-tap-hover);
  filter: none;
}



body.cp .menu-back {
  background: var(--cp-tap);
  font: var(--cp-subhead);
  /* Квадрат 40 со стрелкой 18 — рост кнопок партии справа вверху. */
  height: 40px;
  min-width: 40px;
  box-sizing: border-box;
  padding: 0 11px;
  gap: 0;
  transition: background var(--cp-t-fast) var(--cp-ease),
              opacity var(--cp-t-fast) var(--cp-ease);
}

/* «Меню» раскрывается словом по наведению (14 сентября 2026, попросил
 * пользователь — «по такому же принципу», что «Разобрать» в истории игр).
 *
 * В покое квадрат со стрелкой. Наводишь — кнопка плавно раздаётся
 * вправо, справа от стрелки краской проявляется «Меню»; стрелка стоит
 * где стояла. Кнопка висит поверх экрана (`position: fixed`), поэтому
 * раздаётся она, никого не толкая.
 *
 * ЧЕМ ДВИГАТЬ (памятка выше): в кнопке появляются буквы, значит
 * ширина — дорожкой сетки `0fr → 1fr` (раскладка, без слоя, и ровно
 * до ширины слова), слово — краской, не прозрачностью. Движение
 * горизонтальное: шаг растеризации в четверть пикселя не виден. */
body.cp .menu-back svg {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  display: block;
}

body.cp .menu-back-w {
  display: grid;
  grid-template-columns: 0fr;
  margin-left: 0;
  transition: grid-template-columns var(--cp-t) var(--cp-ease),
              margin-left var(--cp-t) var(--cp-ease);
}

body.cp .menu-back-w > span {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  color: transparent;
  transition: color var(--cp-t) var(--cp-ease);
}

body.cp .menu-back:hover .menu-back-w,
body.cp .menu-back:focus-visible .menu-back-w {
  grid-template-columns: 1fr;
  margin-left: var(--cp-s2);
}

body.cp .menu-back:hover .menu-back-w > span,
body.cp .menu-back:focus-visible .menu-back-w > span {
  color: inherit;
}

body.cp .menu-back:hover {
  background: var(--cp-tap-hover);
  filter: none;
}

/* Название по центру и самой крупной ступенью шкалы.
 *
 * В задачах шапка не показывает больше ничего: строки про режим и
 * контроль времени пусты, точка связи скрыта. Осталось одно слово,
 * прижатое к левому краю карточки, — а прижимать его было не к чему.
 *
 * Двадцать — верх шкалы. Раньше здесь стояло 34 с лишним (2.14em от
 * шестнадцати) и восьмисотое начертание: логотип кричал громче задачи,
 * ради которой открыли экран. Потом я увёл его в другую крайность, на
 * ступень «главная строка», и он сравнялся с «Ваш ход». Верх шкалы —
 * это ровно столько, сколько отсюда и просят: заметно, но не громче
 * всего остального. */
body.cp .brand-title {
  font: var(--cp-title);
  letter-spacing: var(--cp-track);
  justify-content: center;
}
/* Подписи о партии — два столбца, а не строки со сплошным текстом.
 *
 * Было: одна строка «Тип: X · Контроль: Y», собранная в JS склейкой.
 * В партии с ботом она выросла до «Тип: С ботом · уровень 1 · Контроль:
 * без часов», не влезла в ширину карточки и сломалась пополам там, где
 * пришлось, — «без» осталось на одной строке, «часов» уехало на вторую.
 *
 * Ширить карточку нельзя: её ширину задаёт колонка чата, её двигает
 * игрок ползунком. Значит строку надо не удлинять, а разложить.
 *
 * Названия столбцом слева, значения столбцом справа. `max-content`
 * равняет первый столбец по самому длинному названию — «Контроль», —
 * и второй начинается у всех трёх строк на одной вертикали. Это
 * та же раскладка, что у строк в окне настроек, и читается она
 * сверху вниз, а не сплошняком слева направо.
 */
/* Плашка одна, блока в ней два, между ними полоска.
 *
 * Сетка стоит на блоке, а не на всех подписях разом. Столбец названий
 * считается по `max-content` — по самому длинному названию В СВОЕЙ
 * сетке. Пока сетка была одна на пять строк, самым длинным было слово
 * «Контроль», и от его правого края начинались все значения без
 * разбора, включая имена игроков. Имя к контролю времени отношения
 * не имеет, а место, которое он у него забирал, в узкой карточке
 * последнее. Две сетки — у игроков названия равняются по «Чёрные».
 */
body.cp .brand-lines {
  display: flex;
  flex-direction: column;
  /* Ноль вслух, а не молчанием: у `.brand-lines` в board.css свой зазор
   * в 8, и без этой строки он всплывал бы поверх — воздух над полоской
   * выходил бы больше, чем под ней. Весь отступ считает блок ниже. */
  gap: 0;
  margin-top: var(--cp-s3);
}

body.cp .brand-group {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--cp-s3);
  row-gap: var(--cp-s1);
}

/* Полоска между блоками.
 *
 * В пределах полей карточки, а не во всю её ширину. Сперва была во всю
 * ширину, через боковые поля, — 13 сентября 2026 пользователь попросил
 * короче, от края текста до края текста.
 *
 * Воздух вокруг — общий шаг карточки, тот же, что между заголовком и
 * подписями и снизу у самой плашки: 12 сверху, 12 снизу.
 *
 * Контуров это не возвращает. Снимали обводки — рамки ВОКРУГ
 * поверхностей, которые превращали экран в набор наклеек. Здесь
 * поверхность одна, и полоска не обводит её, а делит надвое. Для этого
 * в палитре и лежит `--cp-line`.
 */
body.cp .brand-group + .brand-group {
  border-top: 1px solid var(--cp-line);
  margin-top: var(--cp-s3);
  padding-top: var(--cp-s3);
}

/* Пара «название — значение» обёрнута в строку, чтобы строку можно было
 * убрать целиком: в разборе партии контроля времени мы не знаем, и
 * пустое место рядом со словом «Контроль» выглядело бы потерей, а не
 * отсутствием. `display: contents` растворяет обёртку — в сетке остаются
 * те же две ячейки, столбцы равняются по всем строкам. */
body.cp .brand-row { display: contents; }

body.cp .brand-key { font: var(--cp-footnote); color: var(--cp-muted); }

/* Сторона игрока — кружок перед именем, как на Lichess (27 октября
 * 2026). Вид кружков общий с шапкой ходов (board.css, .legend-dot),
 * здесь только рост и то, что они видны всегда: другой подписи у
 * строки нет. Слово «Белые»/«Чёрные» оставлено читалке (.cp-sr).
 *
 * Столбец под кружок узкий, и зазор до имени — 6, а не общий 12
 * карточки: 12 после десятипиксельного кружка отрывали имя от него. */
body.cp #groupPlayers { column-gap: 6px; }
body.cp .brand-side { display: inline-flex; align-items: center; }
body.cp .brand-side .legend-dot { display: block; width: 12px; }

/* ------------------------------------------------ знак стороны

 * ЗНАК СТОРОНЫ — белые или чёрные одним знаком. Стоит в шапке ходов,
 * у имён в карточке партии, в «Истории игр» и в списке партий разбора;
 * класс .cp-side, рисунок один на все места.
 *
 * Пришли к нему 27 октября 2026 через долгий перебор: короли (чёрный
 * сливался с тёмной панелью), клетки доски, кружки — вышли копией
 * Lichess, квадраты с контуром. Пользователь: «крупнее и мягче,
 * желательно без контура, и чтобы не выглядело взятым с другого сайта».
 *
 * ФОРМА — настройка «Знак стороны»: кружок, квадрат, сквиркл, капсула,
 * овал; по умолчанию сквиркл. Сквиркл и овал скруглением углов не
 * нарисовать, поэтому форма — маска из SVG, а цвет даёт фон. Задаётся
 * переменными на ближайшем предке с data-side-shape: на <html> — для
 * страницы (в партии ставит ui.js, в меню — строчный скрипт в голове
 * index.html), на образце в окне настроек — для образца. Переменные
 * наследуются от ближнего, так что образцы показывают свою форму.
 * Без метки — сквиркл: он же на :root, но через :where — с нулевой
 * силой. Простой :root по силе равен [data-side-shape] и стоит в файле
 * ниже кружка и квадрата — перебивал их, и выбор этих двух на странице
 * ничего не менял (пользователь, 27 октября 2026; образцы в настройках
 * при этом были верны — :root их не касается).
 *
 * ОКРАСКА — настройка «Окраска знака» (27 октября 2026). Тоном без
 * контура белый и чёрный различались слабо: на тёмной поверхности
 * «чёрный» обязан быть светлее её, иначе пропадёт, и выходило «светлый
 * и чуть темнее». Пользователь попросил выбор из пяти:
 *   tone     — тоном: текст 85% и подписи 62% (3 к 1 к поверхности);
 *   contrast — контрастнее (умолчание): полный цвет текста и подписи
 *              на 30%. Было 40%; пользователь выбрал этот вариант и
 *              попросил ещё контрастнее. 30% на глаз почти не
 *              отличались от 40% (72 против 84 по яркости), 20% —
 *              почти сливались с поверхностью (1,4 к 1). Остались два
 *              пункта: этот, 30% (1,7 к 1), и следующий;
 *   crisp    — контрастнее с кромкой: тёмный 20% — разница с белым
 *              больше всего, — а по краю граница тоном подписей 60%,
 *              которая не даёт ему слиться; на 12 px она не читается
 *              обводкой, только держит форму;
 *   bold     — «Как фигуры»: внутри цвета самих фигур «Классики»
 *              (заливки wp.svg #f8f5ee и bp.svg #262320 — единственные
 *              свои цвета знака, и взяты они с доски), у чёрного кромка
 *              1,8 px цветом текста на 50% — 4,4 к 1 к строке: тёмная
 *              заливка почти равна поверхности, держит её кромка.
 *              Белому кромка не нужна — он к строке 15 к 1;
 *   edge     — чёрный темнее поверхности, по краю светлая кромка 1,1 px
 *              на 30% — читается именно чёрным;
 *   soft     — то же, кромка тоном подписей, мягче;
 *   outline  — чёрный контуром 1,5 px, белый залит.
 * Своих цветов нет: всё из ролей палитры и утоплено в поверхность,
 * на которой знак стоит (--side-ground, по умолчанию --cp-surface;
 * строки истории и списка разбора ставят свою, --cp-surface-2).
 *
 * Кромка — отдельным слоем (::after) с маской-кольцом: маска формы
 * срезала бы обычную тень у сквиркла на углах. Окраска выбирается
 * меткой data-side-paint, как форма: на <html> для страницы, на
 * образце — для образца; правило образца на ступень сильнее
 * (.ui-side), поэтому образец показывает свою окраску. Каждая окраска
 * задаёт все свойства сама, чтобы не унаследовать чужую маску.
 *
 * Ширину задаёт место (14 в шапке, 12 у имён и в списках), высота —
 * по пропорции формы. */
[data-side-shape="circle"]{ --side-ar: 12 / 12;
  --side-fill: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Ccircle cx='6' cy='6' r='6'/%3E%3C/svg%3E");
  --side-ring: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Ccircle cx='6' cy='6' r='6'/%3E%3C/clipPath%3E%3Ccircle cx='6' cy='6' r='6' fill='none' stroke='black' stroke-width='3' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-edge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Ccircle cx='6' cy='6' r='6'/%3E%3C/clipPath%3E%3Ccircle cx='6' cy='6' r='6' fill='none' stroke='black' stroke-width='2.2' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-bold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Ccircle cx='6' cy='6' r='6'/%3E%3C/clipPath%3E%3Ccircle cx='6' cy='6' r='6' fill='none' stroke='black' stroke-width='3.6' clip-path='url%28%23c%29'/%3E%3C/svg%3E"); }
[data-side-shape="square"]{ --side-ar: 12 / 12;
  --side-fill: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Crect width='12' height='12' rx='3.5'/%3E%3C/svg%3E");
  --side-ring: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Crect width='12' height='12' rx='3.5'/%3E%3C/clipPath%3E%3Crect width='12' height='12' rx='3.5' fill='none' stroke='black' stroke-width='3' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-edge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Crect width='12' height='12' rx='3.5'/%3E%3C/clipPath%3E%3Crect width='12' height='12' rx='3.5' fill='none' stroke='black' stroke-width='2.2' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-bold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Crect width='12' height='12' rx='3.5'/%3E%3C/clipPath%3E%3Crect width='12' height='12' rx='3.5' fill='none' stroke='black' stroke-width='3.6' clip-path='url%28%23c%29'/%3E%3C/svg%3E"); }
:where(:root), [data-side-shape="squircle"]{ --side-ar: 12 / 12;
  --side-fill: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z'/%3E%3C/svg%3E");
  --side-ring: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z'/%3E%3C/clipPath%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z' fill='none' stroke='black' stroke-width='3' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-edge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z'/%3E%3C/clipPath%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z' fill='none' stroke='black' stroke-width='2.2' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-bold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3CclipPath id='c'%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z'/%3E%3C/clipPath%3E%3Cpath d='M6 0C11 0 12 1 12 6S11 12 6 12 0 11 0 6 1 0 6 0Z' fill='none' stroke='black' stroke-width='3.6' clip-path='url%28%23c%29'/%3E%3C/svg%3E"); }
[data-side-shape="capsule"]{ --side-ar: 12 / 7;
  --side-fill: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'%3E%3Crect width='12' height='7' rx='3.5'/%3E%3C/svg%3E");
  --side-ring: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'%3E%3CclipPath id='c'%3E%3Crect width='12' height='7' rx='3.5'/%3E%3C/clipPath%3E%3Crect width='12' height='7' rx='3.5' fill='none' stroke='black' stroke-width='3' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-edge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'%3E%3CclipPath id='c'%3E%3Crect width='12' height='7' rx='3.5'/%3E%3C/clipPath%3E%3Crect width='12' height='7' rx='3.5' fill='none' stroke='black' stroke-width='2.2' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-bold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'%3E%3CclipPath id='c'%3E%3Crect width='12' height='7' rx='3.5'/%3E%3C/clipPath%3E%3Crect width='12' height='7' rx='3.5' fill='none' stroke='black' stroke-width='3.6' clip-path='url%28%23c%29'/%3E%3C/svg%3E"); }
[data-side-shape="oval"]{ --side-ar: 12 / 9.6;
  --side-fill: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 9.6'%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8'/%3E%3C/svg%3E");
  --side-ring: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 9.6'%3E%3CclipPath id='c'%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8'/%3E%3C/clipPath%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8' fill='none' stroke='black' stroke-width='3' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-edge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 9.6'%3E%3CclipPath id='c'%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8'/%3E%3C/clipPath%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8' fill='none' stroke='black' stroke-width='2.2' clip-path='url%28%23c%29'/%3E%3C/svg%3E");
  --side-bold: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 9.6'%3E%3CclipPath id='c'%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8'/%3E%3C/clipPath%3E%3Cellipse cx='6' cy='4.8' rx='6' ry='4.8' fill='none' stroke='black' stroke-width='3.6' clip-path='url%28%23c%29'/%3E%3C/svg%3E"); }
/* Без body.cp — намеренно: рисунок по умолчанию должен уступать
 * окраскам, а у тех специфичность — метка и класс. */
.cp-side {
  position: relative;
  aspect-ratio: var(--side-ar, 1);
  -webkit-mask: var(--side-fill) center / contain no-repeat;
          mask: var(--side-fill) center / contain no-repeat;
}
.cp-side.black::after {
  content: ""; position: absolute; inset: 0;
  -webkit-mask: var(--side-edge) center / contain no-repeat;
          mask: var(--side-edge) center / contain no-repeat;
}
/* Без метки — «контрастнее», как умолчание настройки. */
.cp-side.white { background: var(--cp-text); }
.cp-side.black { background: color-mix(in srgb, var(--cp-muted) 30%, var(--side-ground, var(--cp-surface))); }
[data-side-paint="tone"] .cp-side.white, .ui-side[data-side-paint="tone"] .cp-side.white { background: color-mix(in srgb, var(--cp-text) 85%, var(--side-ground, var(--cp-surface))); }
[data-side-paint="tone"] .cp-side.black, .ui-side[data-side-paint="tone"] .cp-side.black { background: color-mix(in srgb, var(--cp-muted) 62%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-fill); mask-image: var(--side-fill); }
[data-side-paint="tone"] .cp-side.black::after, .ui-side[data-side-paint="tone"] .cp-side.black::after { background: transparent;
  -webkit-mask-image: var(--side-edge); mask-image: var(--side-edge); }
[data-side-paint="contrast"] .cp-side.white, .ui-side[data-side-paint="contrast"] .cp-side.white { background: var(--cp-text); }
[data-side-paint="contrast"] .cp-side.black, .ui-side[data-side-paint="contrast"] .cp-side.black { background: color-mix(in srgb, var(--cp-muted) 30%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-fill); mask-image: var(--side-fill); }
[data-side-paint="contrast"] .cp-side.black::after, .ui-side[data-side-paint="contrast"] .cp-side.black::after { background: transparent;
  -webkit-mask-image: var(--side-edge); mask-image: var(--side-edge); }
[data-side-paint="crisp"] .cp-side.white, .ui-side[data-side-paint="crisp"] .cp-side.white { background: var(--cp-text); }
[data-side-paint="crisp"] .cp-side.black, .ui-side[data-side-paint="crisp"] .cp-side.black { background: color-mix(in srgb, var(--cp-muted) 20%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-fill); mask-image: var(--side-fill); }
[data-side-paint="crisp"] .cp-side.black::after, .ui-side[data-side-paint="crisp"] .cp-side.black::after { background: color-mix(in srgb, var(--cp-muted) 60%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-edge); mask-image: var(--side-edge); }
[data-side-paint="bold"] .cp-side.white, .ui-side[data-side-paint="bold"] .cp-side.white { background: #f8f5ee; }
[data-side-paint="bold"] .cp-side.black, .ui-side[data-side-paint="bold"] .cp-side.black { background: #262320;
  -webkit-mask-image: var(--side-fill); mask-image: var(--side-fill); }
[data-side-paint="bold"] .cp-side.black::after, .ui-side[data-side-paint="bold"] .cp-side.black::after { background: color-mix(in srgb, var(--cp-text) 50%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-bold); mask-image: var(--side-bold); }
[data-side-paint="edge"] .cp-side.white, .ui-side[data-side-paint="edge"] .cp-side.white { background: color-mix(in srgb, var(--cp-text) 90%, var(--side-ground, var(--cp-surface))); }
[data-side-paint="edge"] .cp-side.black, .ui-side[data-side-paint="edge"] .cp-side.black { background: color-mix(in srgb, #000 55%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-fill); mask-image: var(--side-fill); }
[data-side-paint="edge"] .cp-side.black::after, .ui-side[data-side-paint="edge"] .cp-side.black::after { background: rgba(255, 255, 255, .3);
  -webkit-mask-image: var(--side-edge); mask-image: var(--side-edge); }
[data-side-paint="soft"] .cp-side.white, .ui-side[data-side-paint="soft"] .cp-side.white { background: color-mix(in srgb, var(--cp-text) 90%, var(--side-ground, var(--cp-surface))); }
[data-side-paint="soft"] .cp-side.black, .ui-side[data-side-paint="soft"] .cp-side.black { background: color-mix(in srgb, #000 45%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-fill); mask-image: var(--side-fill); }
[data-side-paint="soft"] .cp-side.black::after, .ui-side[data-side-paint="soft"] .cp-side.black::after { background: color-mix(in srgb, var(--cp-muted) 45%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-edge); mask-image: var(--side-edge); }
[data-side-paint="outline"] .cp-side.white, .ui-side[data-side-paint="outline"] .cp-side.white { background: color-mix(in srgb, var(--cp-text) 90%, var(--side-ground, var(--cp-surface))); }
[data-side-paint="outline"] .cp-side.black, .ui-side[data-side-paint="outline"] .cp-side.black { background: color-mix(in srgb, var(--cp-text) 80%, var(--side-ground, var(--cp-surface)));
  -webkit-mask-image: var(--side-ring); mask-image: var(--side-ring); }
[data-side-paint="outline"] .cp-side.black::after, .ui-side[data-side-paint="outline"] .cp-side.black::after { background: transparent;
  -webkit-mask-image: var(--side-edge); mask-image: var(--side-edge); }

/* Окно настроек в двух видах: игрок и Dev (ui.js, uiMode). Строки
 * помечены в game.html: data-dev — только у разработчика, data-user —
 * только у игрока, без метки — у обоих. */
body.cp #uiModal:not([data-mode="dev"]) [data-dev],
body.cp #uiModal[data-mode="dev"] [data-user] { display: none !important; }

/* Переключатель «Игрок / Dev» в шапке окна — сегментами, как в iOS:
 * подложка --cp-surface-3, выбранный сегмент светлее. */
body.cp .ui-mode {
  display: inline-flex; gap: 2px; padding: 2px; margin-left: auto; margin-right: var(--cp-s2);
  border-radius: var(--cp-r-s); background: var(--cp-surface-3);
}
body.cp .ui-mode[hidden] { display: none; }
body.cp .ui-mode button {
  border: 0; border-radius: calc(var(--cp-r-s) - 2px); padding: 4px 10px;
  font: var(--cp-footnote); color: var(--cp-muted); background: transparent; cursor: pointer;
}
body.cp .ui-mode button[aria-pressed="true"] { color: var(--cp-text); background: var(--cp-surface); }

/* Образец формы рядом с подписью в настройке «Знак стороны»: белый
 * и чёрный знак парой, в рост шапки ходов. */
body.cp .ui-side { display: inline-flex; align-items: center; gap: 4px; }
body.cp .ui-side .legend-dot { display: block; }

/* Текст только для читалки экрана: на странице его нет. */
body.cp .cp-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* Значение в полную силу, название приглушено: так видно, что читать,
 * а что подпись к прочитанному. Раньше приглушены были обе половины
 * строки разом — и «Контроль», и «без часов». */
body.cp .brand-info-line { font: var(--cp-footnote); color: var(--cp-text); }

/* Имя игрока — в одну строку, сколько поместилось.
 *
 * Остальные значения в карточке короткие и известны заранее: «Блиц»,
 * «5+3», «Идёт игра». Имя не такое — его пишет не программа, а человек,
 * и длина у него любая. Телеграмное `@` плюс двадцать знаков, «Бот ·
 * уровень 8», «Player 482913057» — в столбец шириной с половину узкой
 * карточки это не влезает.
 *
 * Перенос здесь хуже обрезки: перенесённое имя занимает две строки и
 * читается как две разные записи, а карточка из-за него меняет высоту
 * при каждой смене соперника. Многоточие говорит честно: имя длиннее,
 * чем видно. Целиком оно лежит в `title` (game.js) — под курсором.
 *
 * `min-width: 0` — без него ячейка `1fr` не уступит ниже ширины своего
 * содержимого, и обрезать будет нечего: столбец просто раздвинет
 * карточку. */
body.cp .brand-player {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
body.cp #betaBadge { font: var(--cp-caption); color: var(--cp-muted); }

/* ------------------------------------------------ список ходов */

/* Подсветка в списке ходов.
 *
 * Наведение было белым: заливка в 5% белого плюс светлая рамка в два
 * пикселя — то есть ровно тот контур, который мы отовсюду сняли, только
 * появляющийся под курсором. Теперь и наведение, и текущий ход красятся
 * акцентом, разной силой: под курсором едва заметно, у текущего хода
 * втрое сильнее. Рамок нет ни у того, ни у другого.
 */
body.cp {
  --moveHoverBg: color-mix(in srgb, var(--cp-accent) 10%, transparent);
  --moveHoverBorder: transparent;
  --moveActiveBorder: transparent;
}

/* Скругления в колонке ходов не слушались шкал: у строки было 10,
 * а у ячейки внутри неё — 12. Внутренний угол круче внешнего — глаз
 * читает это как брак, даже не умея назвать причину. */
body.cp .move-row { border-radius: var(--cp-r-s); }
body.cp .move-cell { border-radius: var(--cp-r-s); font: var(--cp-subhead); }
/* Текущий ход — сплошная заливка акцентом с тёмной подписью, а не
 * акцент в треть силы.
 *
 * Треть работала, пока акцент был синим: синий в 30% поверх тёмной
 * панели даёт тёмно-синее пятно, и светлая подпись на нём читается.
 * В серой теме акцент светлый, и та же треть даёт средне-серое —
 * подпись на нём держит контраст 2.3, то есть не читается вовсе.
 *
 * Сплошная заливка снимает вопрос в обеих темах.
 *
 * Цвет здесь — главного действия, а не подсветок, и потому синий в обеих
 * темах. «Где ты сейчас в партии» — не украшение и не пометка вроде
 * подложки значка: это единственное место на экране, отвечающее на
 * вопрос, ради которого разбор и открыли. Тем же синим и по той же
 * причине отмечен курсор на графике оценок. */
body.cp {
  --moveActiveBg: var(--cp-primary);
}

/* Селектор длиннее соседнего намеренно: `body.cp .moves-panel
 * .move-cell` ниже красит ячейки в обычный цвет и весит ровно столько
 * же — при равном весе побеждает то, что записано позже. Добавленная
 * `.moves-panel` разрешает спор в пользу подсветки. */
body.cp .moves-panel .move-cell.active {
  color: var(--cp-primary-ink);
  font-weight: 600;
}
body.cp .moves-panel .move-cell { color: var(--cp-text); }
/* Класс здесь `.move-no`, а не `.move-num`: я написал правило
 * на несуществующий селектор, и номер хода единственный на экране
 * остался на старом `--muted` — заметно это стало только в графите,
 * когда всё остальное поменяло оттенок, а он нет. */
body.cp .moves-panel .move-no { color: var(--cp-muted); font: var(--cp-footnote); }

/* Вертикальная черта посреди колонки ходов — последний контур,
 * оставшийся от прежнего оформления. Делить белые и чёрные ходы
 * ей не нужно: они и так стоят двумя ровными столбцами. */
body.cp .moves-panel::after { display: none; }

/* Кнопки перемотки под списком ходов — те же правила, что у прочих:
 * без контура и без края, садятся при нажатии.
 *
 * Но не таблетки и не в общий рост. Их пять в ряд поперёк колонки
 * в 260, и они не действия, а орган управления: пролистать список.
 * Скруглением-таблеткой каждая читалась как отдельная кнопка размером
 * с «Подсказку», и ряд перевешивал всё, ради чего он стоит.
 *
 * Кегль стрелок вернулся на 12: общая ступень «подпись». Я поднял его
 * до пятнадцати заодно со всем остальным — стрелки выросли, и это было
 * первым, что бросилось в глаза. Высота с 42 до 36 — по той же причине,
 * по которой весь экран поехал вниз.
 */
body.cp .review-btn {
  min-width: 36px;
  height: 36px;
  padding: 0 var(--cp-s2);
  background: var(--cp-tap);
  border: 0;
  border-radius: var(--cp-r-s);
  color: var(--cp-text);
  font: var(--cp-footnote);
  box-shadow: none;
  transition: background var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease),
              opacity var(--cp-t-fast) var(--cp-ease);
}

body.cp .review-btn:hover { background: var(--cp-tap-hover); }



body.cp .review-step,
body.cp #reviewStep { color: var(--cp-muted); font: var(--cp-footnote); }

/* Значки-кнопки наверху правой колонки помечены в board.css по
 * идентификатору — а идентификатор перебивает любой класс, сколько бы
 * их ни было. Поэтому здесь тоже по идентификатору: иначе своя тень
 * у этих двух так и осталась бы единственной внутри экрана. */
body.cp #uiBtn,
body.cp #pageFsBtn {
  box-shadow: none;
}

/* Ячейки в колонке ходов — последнее место на экране, где движение
 * шло по своей длительности и своей кривой (.12s ease, семьдесят
 * с лишним ячеек сразу). */
body.cp .move-cell {
  transition: background var(--cp-t-fast) var(--cp-ease),
              color var(--cp-t-fast) var(--cp-ease);
}

/* ============================================================ партия */

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

/* ------------------------------------------------------------- часы */

/* Часы — единственное на экране, что читают на ходу, поэтому шкала
 * у них та же, а роль — «главная строка».
 *
 * Чей сейчас ход, показывали три вещи сразу: зелёная рамка у своих
 * часов, жёлтая у активных и жёлтое кольцо вокруг. Три цвета на одну
 * мысль, и ни один из них в палитре проекта не значится. Осталась одна
 * отметка — заливка часов того, кто думает.
 *
 * Синим, а не акцентом. Акцент в графите почти белый, и отметка выходила
 * белой — ровно тем, от чего экран отучали. `--cp-primary` синий во всех
 * тёмных темах, это цвет главных кнопок в меню.
 *
 * Заливка целиком, а не кольцо в два пикселя по краю. Кольцо — приём
 * для того, что выбрано из нескольких равных: так помечен набор фигур
 * в окне тем. Здесь выбирают не часы, здесь ими показывают, чей ход,
 * и отметка должна быть видна боковым зрением, пока смотришь на доску.
 * Залитая плашка — единственная синяя вещь в колонке, промахнуться
 * мимо неё нельзя; обводка на тёмном фоне читалась рамкой поля ввода.
 *
 * Подпись на синем — тёмная (`--cp-primary-ink`), как и на главной
 * кнопке меню: белое на этом синем даёт контраст 2.9.
 */
body.cp .clock {
  padding: var(--cp-s2) var(--cp-s3);
  border: 0;
  border-radius: var(--cp-r-s);
  background: var(--cp-surface);
  font: var(--cp-headline);
  color: var(--cp-text);
  box-shadow: none;
  transition: background var(--cp-t-fast) var(--cp-ease),
              color var(--cp-t-fast) var(--cp-ease);
}

/* Иконка игрока перед подписью часов (game.js, `подписьЧасов`). Строчным
 * кружком, а не флексом у плашки: раскладку плашки на телефоне ведут
 * свои правила (board.css), и менять её ради кружка незачем. Размер —
 * чуть выше строки кегля плашки, чтобы не раздвигать её по высоте
 * заметно. Буква — когда иконки нет. */
body.cp .clock .clock-av {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  margin-right: var(--cp-s2);
  vertical-align: middle;
  border-radius: var(--cp-r-pill);
  overflow: hidden;
  background: var(--cp-surface-3);
  color: var(--cp-text);
  font: var(--cp-caption);
  font-weight: 700;
}
body.cp .clock .clock-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
body.cp .clock .clock-label { vertical-align: middle; }
/* Свой display перебил бы `hidden` — в партии с ботом и в разборе кружка нет. */
body.cp .clock .clock-av[hidden] { display: none; }

/* Зрителю кнопки «Реванш» и «Сдаться» не нужны (game.js ставит класс
 * `cp-spectator`, когда оба места заняты другими). */
body.cp-spectator .top-actions .btn.good2,
body.cp-spectator .top-actions .btn.bad { display: none; }

/* «Глаз» зрителей — в правом верхнем углу карточки с названием, напротив
 * стрелки «Меню»; окошко со списком — поверх страницы (game.js,
 * `окошкоЗрителей`). Без переходов: в обоих буквы, а наведение не должно
 * их мылить (памятка «ЧЕМ ДВИГАТЬ», test_hover_layers). */
body.cp .brand-panel { position: relative; }
body.cp .spec-eye {
  position: absolute;
  top: 12px;
  right: 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 10px;
  border: 0;
  border-radius: var(--cp-r-pill);
  background: var(--cp-tap);
  color: var(--cp-muted);
  font: var(--cp-subhead);
  font-weight: 600;
  cursor: pointer;
}
body.cp .spec-eye[hidden] { display: none; }
body.cp .spec-eye:hover,
body.cp .spec-eye[aria-expanded="true"] { background: var(--cp-tap-hover); color: var(--cp-text); }
body.cp .spec-eye svg { width: 18px; height: 18px; display: block; }

body.cp .spec-pop {
  position: fixed;
  z-index: 9000;
  width: 240px;
  max-height: 320px;
  overflow: auto;
  padding: var(--cp-s3);
  box-sizing: border-box;
  border-radius: var(--cp-r-m);
  background: var(--cp-surface-2);
  box-shadow: var(--cp-e-over);
  visibility: hidden;
}
body.cp .spec-pop.open { visibility: visible; }
body.cp .spec-pop-head { margin-bottom: var(--cp-s2); font: var(--cp-footnote); color: var(--cp-muted); }
body.cp .spec-row { display: flex; align-items: center; gap: var(--cp-s2); padding: var(--cp-s1) 0; }
body.cp .spec-row .clock-av {
  flex: none; display: inline-grid; place-items: center; width: 24px; height: 24px;
  border-radius: var(--cp-r-pill); overflow: hidden; background: var(--cp-surface-3);
  color: var(--cp-text); font: var(--cp-caption); font-weight: 700;
}
body.cp .spec-row .clock-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
body.cp .spec-name { flex: 1; min-width: 0; font: var(--cp-subhead); color: var(--cp-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
body.cp .spec-you { font: var(--cp-caption); color: var(--cp-muted); }
body.cp .spec-more { padding-top: var(--cp-s1); font: var(--cp-footnote); color: var(--cp-muted); }

body.cp .clock.me { border: 0; }
body.cp .clock.enemy { opacity: 1; color: var(--cp-muted); }

body.cp .clock.active {
  border: 0;
  background: var(--cp-primary);
  color: var(--cp-primary-ink);
  box-shadow: none;
}

/* --------------------------------------------------------- переписка */

/* Поле ввода стало полем, а не полосой.
 *
 * Было: строка во всю ширину панели, отделённая сверху светлой чертой
 * в пиксель, и вторая такая же черта между вводом и кнопкой отправки.
 * Те самые контуры, которые сняты со всего остального, — просто они
 * прятались в чате, куда на экране задач не заходят.
 *
 * Стало: скруглённое поле на ступень светлее панели, кнопка отправки
 * внутри него и акцентом. Отделяет поле от переписки его собственный
 * фон, как и всё прочее в проекте.
 */
body.cp .chat-messages { font: var(--cp-subhead); padding: var(--cp-s3); }

body.cp .chat-input {
  border-top: 0;
  padding: 0 var(--cp-s3) var(--cp-s3);
}

body.cp .chat-compose {
  height: 40px;
  padding: 0 var(--cp-s1) 0 var(--cp-s3);
  border-radius: var(--cp-r-pill);
  background: var(--cp-surface-2);
}

body.cp .chat-compose input {
  height: 40px;
  padding: 0;
  font: var(--cp-subhead);
}

/* Черта между полем и кнопкой отправки. Кнопка теперь внутри поля
 * и покрашена акцентом — разделять их нечем и незачем. */
body.cp .chat-compose button::before { display: none; }

body.cp .chat-compose button {
  width: 32px;
  flex: 0 0 32px;
  height: 32px;
  color: var(--cp-accent);
  border-radius: var(--cp-r-pill);
  transition: opacity var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease);
}



body.cp .chat-msg { font: var(--cp-subhead); border-radius: var(--cp-r-m); }
/* Подпись автора над текстом сообщения (chat.js): зритель пишет или
 * зритель читает. Мелко и приглушённо — главное в строке текст. */
body.cp .chat-msg .chat-who { display: block; margin-bottom: 2px; font: var(--cp-caption);
  font-weight: 600; color: var(--cp-muted); }
/* Полоска зрителя — нейтральная: цвет у полосок только у двоих за доской. */
body.cp .chat-row.spec .chat-stripe { background: var(--cp-surface-3); box-shadow: none; }

/* ------------------------------------------- действия в партии */

/* «Реванш» и «Сдаться» — заливка своим цветом в шестую долю силы
 * и подпись тем же цветом в полную. Это тот же приём, каким набраны
 * отметки в ленте решённых задач: цвет говорит, что будет, а кричать
 * ему незачем.
 *
 * Общее правило значков красит их в серое, и после перевода партии на
 * общий код они оба стали одинаково серыми — разница между «сыграть
 * ещё раз» и «сдаться» пропала. Возвращаем, но уже цветами палитры,
 * а не своими: было `rgba(38,208,124,.18)` и `rgba(255,77,77,.16)`,
 * ни то, ни другое в палитре не значилось.
 *
 * Цвета с тех пор поменялись дважды.
 *
 * «Реванш» был зелёным, побывал синим и вернулся к зелёному.
 *
 * Синим он стал из опасения, что зелёный в проекте занят: им помечен
 * лучший ход в разборе. Опасение не подтвердилось — эти два зелёных
 * не встречаются на одном экране, в разборе кнопок партии нет вовсе
 * (board.css, блок `body.mode-review`). Зато синий занят по-настоящему
 * и в двух шагах отсюда: им залиты часы того, чей сейчас ход. Две синие
 * плашки в одной колонке, и одна из них про время, а другая про кнопку.
 *
 * Теперь на колонку три цвета и три разных дела: синий — чей ход,
 * зелёный — сыграть ещё, красный — сдаться.
 *
 * А до того обе перестали быть подкрашенными и стали крашеными.
 *
 * Заливка в шестую долю силы — приём для отметки: столько берут
 * квадратики в ленте решённых задач, где цвет только сообщает, чем
 * кончилось. Но это не отметки, это две единственные кнопки действия
 * на всём экране партии, и в четверть голоса они выцветали до серых
 * соседей — трёх служебных значков, которым как раз положено молчать.
 *
 * Теперь цвет в полную силу. В ряду из пяти значков две крашеные
 * кнопки — это и есть его лицо: два дела партии и три инструмента,
 * которые к партии отношения не имеют.
 *
 * А сами значки внутри — белые, как и у трёх серых соседей. Тёмные
 * чернила остаются подписям (см. `--cp-ink-on-fill`): рисунок в две
 * линии узнают по силуэту, и важнее, чтобы весь ряд был одного цвета.
 */
body.cp .top-actions .btn.good2 {
  background: var(--cp-good-fill);
  color: var(--cp-ink-on-fill);
}

body.cp .top-actions .btn.good2:hover {
  background: var(--cp-good-fill-hover);
}

body.cp .top-actions .btn.bad {
  background: var(--cp-danger-fill);
  color: var(--cp-ink-on-fill);
}

body.cp .top-actions .btn.bad:hover {
  background: var(--cp-danger-fill-hover);
}

/* --------------------------------------- всплывающая подсказка пинга */

body.cp .connTip {
  /* Фон брался из старого `--panelSolid`: в графите подсказка уходила
   * на свой оттенок, пока панель под ней была уже на токене. */
  background: var(--cp-surface);
  border-radius: var(--cp-r-s);
  border: 0;
  box-shadow: var(--cp-e-over);
  font: var(--cp-footnote);

  /* Приходит полем, уходит растворением — как окна и как всё прочее
   * в проекте, что появляется поверх экрана. Своё время было объявлено
   * дважды, в самой подсказке и в правиле наведения; перебить надо оба,
   * иначе приход и уход идут по разным часам.
   *
   * `translateY(-50%)` остаётся: он держит подсказку по середине точки
   * и неподвижен, а неподвижный сдвиг слоя не заводит. Двигается
   * `margin-left`. */
  transform: translateY(-50%);
  margin-left: -6px;
  transition: opacity var(--cp-t-fast) var(--cp-ease),
              margin-left var(--cp-t-fast) var(--cp-ease),
              visibility 0s linear var(--cp-t-fast);
}

/* ------------------------------------------ окно настроек интерфейса */

body.cp .ui-modal__panel {
  padding: var(--cp-s4);
  border: 0;
  border-radius: var(--cp-r-m);
  background: var(--cp-surface);
  /* Цвет тоже отсюда: заголовок окна своего не задаёт и наследует
   * от панели — а панель брала старый `--text`. */
  color: var(--cp-text);
  box-shadow: var(--cp-e-over);
}

body.cp .ui-modal__backdrop { background: rgba(0, 0, 0, .45); }

/* Открытие и закрытие — плавно (28 октября 2026, по просьбе: окно
 * возникало и пропадало кадром, `display: none` → `block`). Механика
 * та же, что у окон меню (`.menu-modal` в index.html) и у карточки
 * конца партии, разбор — памятка ЧЕМ ДВИГАТЬ в начале файла:
 *
 *   приход — окно едет сверху вниз на 24 сдвигом, прозрачности нет
 *            вовсе (иначе текст в конце меняет вес);
 *   уход   — растворением, короче; окно при этом отъезжает
 *            обратно вверх, но гаснет раньше, чем доедет;
 *   подложка — появляется сразу (проявляясь, она утащила бы окно в
 *            слой), гаснет прозрачностью: букв в ней нет.
 *
 * Едет свойство `translate`, а не `transform`: на узком экране окно
 * центруется `transform: translateX(50%)` (board.css), и одно
 * затёрло бы другое. Отдельные свойства складываются.
 *
 * Обёртка прячется `visibility`, а не `display`: анимировать `display`
 * нельзя. При уходе она держится видимой до конца растворения, при
 * приходе включается сразу — и закрытое окно не ловит фокус табом. */
body.cp .ui-modal {
  display: block;
  visibility: hidden;
  transition: visibility 0s linear var(--cp-t-fast);
}
body.cp .ui-modal.open {
  visibility: visible;
  transition: visibility 0s;
}
body.cp .ui-modal__backdrop {
  opacity: 0;
  transition: opacity var(--cp-t-fast) var(--cp-ease);
}
body.cp .ui-modal.open .ui-modal__backdrop {
  opacity: 1;
  transition: opacity 0s;
}
body.cp .ui-modal__panel {
  opacity: 0;
  translate: 0 -24px;
  transition: opacity var(--cp-t-fast) var(--cp-ease),
              translate var(--cp-t) var(--cp-ease);
}
body.cp .ui-modal.open .ui-modal__panel {
  opacity: 1;
  translate: 0 0;
  transition: opacity 0s,
              translate var(--cp-t) var(--cp-ease);
}

/* Черты между строками сняты — строки разделяет расстояние между ними.
 * Их было по одной на каждую строку настроек, то есть полтора десятка
 * светлых линий в одном окне. */
body.cp .ui-modal__head {
  border-bottom: 0;
  padding-bottom: var(--cp-s3);
  margin-bottom: var(--cp-s2);
}

body.cp .ui-modal__title {
  font: var(--cp-headline);
  letter-spacing: var(--cp-track);
}

/* Квадрат с мягкими углами, 36 на 36 — как крестик в окнах меню
 * (`.close-btn` в index.html) и на карточке конца партии.
 *
 * 28 октября 2026 доведён до того же крестика целиком: по размеру он
 * совпадал, но иконка стояла левее середины — у кнопки была блочная
 * раскладка и боковые поля браузера (1px 6px), а SVG блочный, и его
 * прижимало влево. Плюс свой цвет и никакого наведения. Теперь —
 * центровка сеткой, тихая заливка на ступень светлее окна, крестик
 * приглушённый и светлеет при наведении, как у `.close-btn`. */
body.cp .ui-x {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--cp-r-s);
  background: var(--cp-surface-2);
  color: var(--cp-muted);
  cursor: pointer;
  transition: background var(--cp-t-fast) var(--cp-ease),
              color var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease);
}
body.cp .ui-x:hover { background: var(--cp-surface-3); color: var(--cp-text); }

/* Переключатель сегментами — для выбора из двух-трёх слов: «Шрифт».
 * Тот же приём, что «Кратко / Подробно» в сводке задач (puzzle-ui.js,
 * `.pz-view`): слова в одной дорожке, под выбранным — ползунок.
 *
 * Внутри — обычные радиокнопки, спрятанные, но живые: ui.js читает
 * их как прежде, стрелки клавиатуры переключают, читалка видит группу.
 * Положение ползунка берётся из того, какая из них отмечена
 * (`:has`), — без скрипта.
 *
 * Ползунок едет `left`, а не `transform`: слова лежат поверх него, и
 * сдвиг слоем увёл бы в слой и их (памятка ЧЕМ ДВИГАТЬ, по горизонтали
 * `left` годится). Колонки — сеткой поровну, чтобы ползунок ровно
 * в треть совпадал со словом. */
body.cp .cp-seg {
  --seg-n: 2;
  --seg-i: 0;
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  padding: 2px;
  border-radius: var(--cp-r-s);
  background: var(--cp-surface-2);
}
/* Слов два или три — ширина ползунка по числу слов. */
body.cp .cp-seg:has(> label:nth-of-type(3)) { --seg-n: 3; }
body.cp .cp-seg:has(> label:nth-of-type(2) input:checked) { --seg-i: 1; }
body.cp .cp-seg:has(> label:nth-of-type(3) input:checked) { --seg-i: 2; }
body.cp .cp-seg-knob {
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: calc(2px + (100% - 4px) / var(--seg-n) * var(--seg-i));
  width: calc((100% - 4px) / var(--seg-n));
  border-radius: 6px;
  background: var(--cp-tap);
  transition: left var(--cp-t) var(--cp-ease);
}
body.cp .cp-seg label {
  position: relative;
  display: grid;
  place-items: center;
  padding: 6px var(--cp-s2);
  border-radius: 6px;
  cursor: pointer;
  font: var(--cp-footnote);
  font-weight: 600;
  color: var(--cp-muted);
  transition: color var(--cp-t-fast) var(--cp-ease);
}
body.cp .cp-seg label:hover,
body.cp .cp-seg label:has(input:checked) { color: var(--cp-text); }

/* Ползунок можно тащить (ui.js, «сегменты»): за выбранное слово —
 * рука, пока тянешь — сжатая рука, а светлое слово то, что под
 * ползунком (`is-near`), а не прежнее. Выделение текста при протяжке
 * выключено; по вертикали палец по-прежнему листает окно. */
body.cp .cp-seg { user-select: none; touch-action: pan-y; }
body.cp .cp-seg label:has(input:checked) { cursor: grab; }
body.cp .cp-seg.is-dragging,
body.cp .cp-seg.is-dragging label { cursor: grabbing; }
body.cp .cp-seg.is-dragging label:has(input:checked) { color: var(--cp-muted); }
body.cp .cp-seg.is-dragging label.is-near { color: var(--cp-text); }
body.cp .cp-seg label:has(input:focus-visible) {
  outline: 2px solid var(--cp-accent);
  outline-offset: -2px;
}
body.cp .cp-seg input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}



body.cp .ui-row {
  border-bottom: 0;
  padding: var(--cp-s2) 0;
  gap: var(--cp-s2);
}

body.cp .ui-label { font: var(--cp-subhead); color: var(--cp-text); }
body.cp .ui-val { font: var(--cp-footnote); color: var(--cp-accent); }
body.cp .ui-note { font: var(--cp-footnote); color: var(--cp-muted); }
body.cp .ui-radio label { font: var(--cp-subhead); }

/* Робот рядом с подписью в настройке «Робот» — картинка того, что
 * выбираешь. 20 × 20, цветом бота, как в меню; эмодзи — цветным
 * шрифтом, как значки меню. Рисунки берутся из icons.js. */
body.cp .ui-bot {
  width: 20px; height: 20px; display: grid; place-items: center;
  color: var(--cp-bot); font: var(--cp-body); line-height: 1;
  font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif;
}
body.cp .ui-bot .cp-ic { width: 20px; height: 20px; display: block; }

/* Конь набора рядом с подписью в настройке «Фигуры». Чуть крупнее
 * робота: у SVG «Классики» поля внутри холста. */
body.cp .ui-set { width: 24px; height: 24px; object-fit: contain; display: block; }

/* Выпадающие поля в окне настроек. Оформление у них стояло прямо
 * в разметке, атрибутом style, — а инлайновое правило перебивает любой
 * файл стилей, кроме как через !important. Поэтому style из game.html
 * снят, а вид описан здесь, как у всего остального. */
body.cp .ui-row select {
  width: 100%;
  padding: 0 var(--cp-s3);
  height: 40px;
  border: 0;
  border-radius: var(--cp-r-s);
  background: var(--cp-surface-2);
  color: var(--cp-text);
  font: var(--cp-subhead);
  cursor: pointer;
}

/* ---------------------------------------------- окно конца партии */

body.cp .go-card {
  padding: var(--cp-s5);
  border-radius: var(--cp-r-m);
  box-shadow: var(--cp-e-over);
}

body.cp .go-title {
  margin: var(--cp-s2) 0 2px;
  font: var(--cp-title);
  letter-spacing: var(--cp-track);
}

/* Крест поражения. Цвет — тот же, каким на экране задач набрано
 * «Плохой ход»; семейство — тоже оттуда, и по той же причине: знака ✗
 * в Segoe UI нет, система подставляет для него Segoe UI Symbol сама.
 * Раз уж подставляет — назовём его вслух, чтобы крест в карточке и
 * крест на задачах были нарисованы одной рукой. Хвост списка — на
 * случай, если шрифта нет (не Windows). */
body.cp .go-mark--bad {
  color: var(--cp-danger);
  font-family: "Segoe UI Symbol", "Segoe UI", system-ui, sans-serif;
}

body.cp .go-why { font: var(--cp-subhead); color: var(--cp-muted); }
body.cp .go-players { font: var(--cp-subhead); margin: var(--cp-s4) 0 var(--cp-s1); }
body.cp .go-players .go-side { border-radius: var(--cp-r-s); }
body.cp .go-players .go-vs { font: var(--cp-footnote); }
body.cp .go-note { font: var(--cp-footnote); color: var(--cp-muted); }
body.cp .go-hint { font: var(--cp-caption); color: var(--cp-muted); }

/* «Реванш» — в полтора раза выше прочих: 60 против 40.
 *
 * Столбец из трёх одинаковых кнопок заставляет читать все три, чтобы
 * выбрать одну, — а выбор тут почти всегда один и тот же. Цветом он
 * уже отмечен; рост добавляет второй признак, который виден раньше,
 * чем прочитана подпись.
 *
 * Кегль при этом прежний. Крупная кнопка с крупной подписью читалась
 * бы заголовком, а это кнопка; расти должно поле вокруг слова, а не
 * само слово. */
body.cp .go-btns button.primary {
  min-height: 60px;
  /* Кегль вырос вместе с кнопкой: 20 против 15, та же ступень шкалы,
   * что у заголовка окна. Слово в кнопке 60 ростом, набранное
   * пятнадцатым, тонуло в поле вокруг себя. */
  font: var(--cp-title);
  letter-spacing: var(--cp-track);
}

/* Кнопки в окне рисует gameover-ui.js своей разметкой, класс на них
 * не повесить. Правила те же, что у общего компонента: рост 40,
 * таблетка, ни рамки, ни тени. */
body.cp .go-btns button,
body.cp .go-again {
  min-height: 40px;
  padding: 0 var(--cp-s4);
  border: 0;
  border-radius: var(--cp-r-pill);
  box-shadow: none;
  font: var(--cp-headline);
  letter-spacing: var(--cp-track);
  transition: background var(--cp-t-fast) var(--cp-ease),
              opacity var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease);
}



/* ------------------------------------------------- признак связи */

/* Точка у названия и таблетка с пингом. Кольцо вокруг них — не тень,
 * а отметка состояния: тем же приёмом помечены выбранный набор в окне
 * тем и активные часы. Но цвета у обеих стояли свои — `#26d07c` и
 * `#ff4d4d`, ни одного из них в палитре нет. Теперь берут из неё.
 */
body.cp .logoDot {
  background: var(--cp-ok);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cp-ok) 18%, transparent);
}

body.cp .logoDot.is-bad {
  background: var(--cp-danger);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--cp-danger) 18%, transparent);
}

body.cp .pingBadge {
  padding: 2px var(--cp-s2);
  background: color-mix(in srgb, var(--cp-ok) 16%, transparent);
  border: 0;
  color: var(--cp-ok);
  font: var(--cp-caption);
  font-weight: 700;
  box-shadow: none;
}

body.cp .pingBadge.bad {
  background: color-mix(in srgb, var(--cp-danger) 16%, transparent);
  color: var(--cp-danger);
  box-shadow: none;
}

body.cp .connWrap:hover .connTip {
  /* Прозрачности на приходе нет: она уводит подсказку в слой, и «Пинг:
   * 45 мс» меняет вес букв в конце перехода. Едет только поле. */
  opacity: 1;
  margin-left: 0;
  transition: opacity 0s,
              margin-left var(--cp-t-fast) var(--cp-ease);
}

/* ------------------------------------- кнопки в окне настроек */

/* «Сброс» и «Готово» стоят на старом наборе `.btn`: свой кегль 14,
 * своё начертание 650, своя тень. Общий компонент до них не достаёт —
 * класс на них не повесить, разметка в game.html. */
body.cp .ui-actions .btn {
  min-height: 40px;
  padding: 0 var(--cp-s4);
  border: 0;
  border-radius: var(--cp-r-pill);
  box-shadow: none;
  font: var(--cp-headline);
  letter-spacing: var(--cp-track);
  transition: background var(--cp-t-fast) var(--cp-ease),
              opacity var(--cp-t-fast) var(--cp-ease),
              transform var(--cp-t-fast) var(--cp-ease);
}

body.cp .ui-actions .btn:hover { filter: none; }


body.cp .ui-actions .btn.good2 {
  background: color-mix(in srgb, var(--cp-ok) 18%, transparent);
  color: var(--cp-ok);
}

body.cp .ui-actions .btn.dev {
  background: var(--cp-primary);
  color: var(--cp-primary-ink);
}

/* --------------------------- появление окна конца партии */

/* Свои .18 и .22 с кривой-пружиной жили тут дольше всего остального:
 * это было единственное движение в проекте, считавшее время само.
 * Теперь и длительности, и способ движения объявлены там же, где сама
 * карточка (gameover-ui.js), — и перебивать отсюда нечего.
 *
 * Кривой-пружины у карточки больше нет вовсе: она перескакивала
 * конечное значение, а окно от этого подрагивало. Ровно это уже
 * выяснили на окнах меню. */
