# План фронта — как пишется интерфейс TextMachine > Этап **S0** сессии фронта: план до первой строки кода (`FRONTEND_SESSION_PROMPT.md` §7.2). > Ландится отдельным коммитом. Правят следующие фронт-сессии; при конфликте с > `STACK_DECISIONS.md` побеждает STACK_DECISIONS (там ратифицированы пины), при конфликте > с промтом сессии — промт (там ратифицирован продукт). ## 0. Границы Эта сессия делает **S0 (план) и S1 (инструменты, скриншот-цикл, `tokens.css`, витрина)** и останавливается на витрине. Экраны — S2–S7, по одной сессии на этап (`BACKLOG.md` Ф-1). Зона записи — только `frontend/`. `backend/`, `platform/`, `docs/`, `eval/` — read-only. --- ## 1. Пины Все версии сверены с npm-регистри **02.08.2026** командой `npm view version`. Ставятся **точными** версиями, без `^` и `~`. Колонка «дата» — дата публикации именно этой версии. Строка «= latest» означает: на 02.08.2026 эта версия и есть последняя опубликованная. Единственное намеренное отставание от latest — TypeScript (см. сноску). ### Рантайм | Пакет | Пин | Дата | Зачем именно нам | |---|---|---|---| | `react` | `19.2.8` | 2026-07-21 | ядро; = latest, React 20 не существует | | `react-dom` | `19.2.8` | 2026-07-21 | обязан совпадать с `react` версия-в-версию | | `react-router` | `8.3.0` | 2026-07-22 | библиотечный (data) режим: маршрут на экран — несущая правила «каждый экран открывается в изоляции» (§3.10). Требует Node ≥ 22.22 | | `@tanstack/react-query` | `5.101.4` | 2026-07-21 | всё серверное состояние: прогон, книги, банк. Мажор 5 актуален, «v6» на npm — svelte-адаптер | | `zustand` | `5.0.14` | 2026-05-28 | состояние интерфейса (свёрнутость панелей, активные вкладки). Только с селектором или `useShallow` | | `react-aria-components` | `1.20.0` | 2026-07-31 | ЕДИНСТВЕННАЯ библиотека примитивов: `Table`+`Virtualizer` (банк), `Tree` (главы), `Tabs/Menu/Dialog/Popover/Tooltip` (оболочка), `I18nProvider` (русские служебные строки). Стилей не навязывает. Импорт разрешён только внутри `src/ui/` | | `react-resizable-panels` | `4.12.2` | 2026-07-12 | три панели Fleet. API v4: `Group`/`Panel`/`Separator` — старые имена `PanelGroup`/`PanelResizeHandle` мертвы | | `lucide-react` | `1.28.0` | 2026-07-30 | тонкие линейные монохромные иконки как во Fleet: `size 16`, `strokeWidth 1.5`, без `absoluteStrokeWidth` | | `@fontsource-variable/inter` | `5.3.0` | 2026-07-19 | интерфейсный гротеск; локальный woff2, а не CDN — вид не зависит от сети и одинаков в скриншот-цикле | | `@fontsource-variable/jetbrains-mono` | `5.3.0` | 2026-07-19 | моноширинный для содержимого | CJK-шрифт не ставим: иероглифы отдаём системному стеку, `lang` на элементе приходит из данных языковой пары (`STACK_DECISIONS.md` §2). ### Сборка и типы | Пакет | Пин | Дата | Зачем именно нам | |---|---|---|---| | `vite` | `8.2.0` | 2026-07-30 | внутри Rolldown вместо Rollup+esbuild; = latest | | `@vitejs/plugin-react` | `6.0.5` | 2026-07-30 | без Babel; старый рецепт подключения React Compiler в нём удалён | | `typescript` | `6.0.3` | 2026-04-16 | **намеренно не latest** (latest = 7.0.2): у TS 7 нет программного API до 7.1, значит нет `typescript-eslint` и плагинов языкового сервера. Пересмотр — `BACKLOG.md` Ф-3 | | `@types/react` | `19.2.18` | 2026-07-30 | | | `@types/react-dom` | `19.2.4` | 2026-07-30 | | | `@types/node` | `22.20.1` | | мажор типов держим на мажоре рантайма (Node 22), а не на latest 26 — иначе тайпчек разрешит API, которых на стенде нет | | `lightningcss` | `1.33.0` | 2026-07-20 | минификация CSS в бою; в dev не участвует | ### Контроль качества | Пакет | Пин | Дата | Зачем именно нам | |---|---|---|---| | `eslint` | `10.8.0` | 2026-07-24 | ради `no-restricted-syntax` — на нём стоит половина гейта «одно место для цвета»; в oxlint такого правила нет вовсе | | `@eslint/js` | `10.0.1` | 2026-02-06 | базовый набор для flat-config | | `typescript-eslint` | `8.65.0` | 2026-07-20 | единый пакет (парсер+плагин+`config`) | | `eslint-plugin-react-hooks` | `7.1.1` | 2026-04-17 | держит совместимость с React Compiler, пока сам компилятор выключен (`BACKLOG.md` Ф-2) | | `eslint-plugin-react-refresh` | `0.5.3` | | ловит экспорты, ломающие HMR | | `eslint-config-prettier` | `10.1.8` | 2025-07-18 | гасит форматные правила ESLint, чтобы формат был ровно один | | `stylelint` | `17.14.1` | 2026-07-20 | вторая половина гейта: `declaration-property-value-allowed-list` | | `stylelint-config-standard` | `40.0.0` | 2026-01-15 | база | | `prettier` | `3.9.6` | 2026-07-21 | точный пин: разные патчи форматируют по-разному и создают шум в диффах | | `vitest` | `4.1.10` | 2026-07-06 | тесты на том же конфиге Vite | | `happy-dom` | `20.11.1` | 2026-07-22 | DOM для контракт-теста токенов: в отличие от jsdom считает каскад CSS-переменных | | `@testing-library/react` | `16.3.2` | 2026-01-19 | рендер компонентов в тестах | | `@playwright/test` | `1.62.1` | 2026-07-30 | скриншот-цикл и сверка вычисленных токенов в настоящем Chromium | | `msw` | `2.15.0` | 2026-07-08 | моки сети; пин ставим сейчас, включает S3 | | `globals` | `17.8.0` | 2026-07-26 | наборы глобалов для flat-config | ### Стенд `node -v` = **22.22.3** — проходит нижнюю границу `react-router` (`engines.node: >=22.22.0`), она самая жёсткая из всех. Проверять первым действием на любой новой машине. Пакетный менеджер — npm из поставки Node, один `package.json` в `frontend/`, воркспейсов нет. --- ## 2. Карта `src/` | Папка | Что внутри | Что НЕ внутри | |---|---|---| | `tokens/` | `tokens.css` — единственный источник цвета, размера, радиуса, шрифта. `reset.css` | ничего кроме двух глобальных файлов | | `ui/` | глупые примитивы на токенах: кнопка, поле, вкладки, строка дерева, таблица, выноска. Единственное место, где разрешён импорт `react-aria-components` | запросы, знание о доменных сущностях | | `shell/` | оболочка: три панели, верхняя полоса, статус-полоса, вкладки | доменная логика экранов | | `features/` | `books`, `chapters`, `bank`, `reader`, `settings` — по экрану на папку; запрос живёт здесь, на уровне экрана | цвета и размеры мимо токенов | | `api/` | единственный вход к данным: типы контракта + функции. Сейчас внутри моки, потом HTTP | React-компоненты | | `mock/` | фикстуры: книга, главы, пары оригинал/перевод, банк, состояния прогона | логика; фикстуры — данные | | `showcase/` | витрина примитивов: маршрут `/showcase`, живой каталог для сверки с `references/fleet.png` | продуктовые экраны | Правило зависимостей — сверху вниз, без обратных рёбер: `features/` → `ui/` + `api/`; `shell/` → `ui/`; `ui/` → `tokens/`. `api/` не знает про React. ### 2.1. Слои каскада Ловушка `STACK_DECISIONS.md` §7.4 — тихие расхождения из-за порядка каскада между стилями сторонних примитивов, токенами и CSS Modules. Порядок объявлен один раз, в шапке `reset.css` (он импортируется первым, а заявление слоёв обязано стоять раньше любого слоя): ```css @layer reset, vendor; ``` - **сброс** — самый низ, перебивается чем угодно; - **vendor** — стили сторонних примитивов; подключаются только так: `@import 'пакет/styles.css' layer(vendor)`; - **токены и CSS Modules — намеренно ВНЕ слоёв.** Неслойное правило выигрывает у любого слоя, поэтому ни сброс, ни чужие стили наши перебить не могут, а модуль экрана при нужде может локально переопределить токен обычным каскадом. Токены сознательно не завёрнуты в слой: заворачивать нечего (это только объявления переменных на `:root`), а лишний слой делает их слабее собственных стилей приложения без всякой выгоды. --- ## 3. Правила поддерживаемости — в проверяемой форме Из `FRONTEND_SESSION_PROMPT.md` §5.1. Слева — правило, справа — чем оно проверяется. Правило без проверки — лозунг, поэтому у каждого либо машинный гейт, либо ревью-вопрос с однозначным ответом. | # | Правило | Проверка | |---|---|---| | 1 | Цвет и размер только из `tokens.css` | **машинно**: stylelint `declaration-property-value-allowed-list` (`/^var\(--/` на `color`, `background-color`, `border-color`, `fill`, `stroke`, `font-size`, `z-index`) + ESLint `no-restricted-syntax` на `#hex`/`rgb(`/`hsl(`/`oklch(` в TSX. Исключение по пути — только `tokens/` | | 2 | Отключить гейт комментарием нельзя | **машинно**: `reportDisables: true` в конфиге stylelint — `/* stylelint-disable */` сам становится ошибкой | | 3 | Данные только через `src/api/` | ревью-вопрос: есть ли `fetch`/`axios`/импорт из `mock/` вне `src/api/`? Должно быть «нет» | | 4 | Файл = один компонент + свой `.module.css`, больше ~150 строк — делить | ревью глазами при лендинге пакета | | 5 | Состояние ровно в двух местах: TanStack Query (серверное), Zustand (интерфейсное) | ревью-вопрос: есть ли `useState` с копией серверных данных? Должно быть «нет» | | 6 | Глобальных стилей два файла | **машинно**: любой `.css` вне `src/tokens/`, не являющийся `*.module.css`, — ошибка сборки правилом ESLint на импорт | | 7 | Никаких UI-китов | ревью-вопрос: в `package.json` кроме `react-aria-components` библиотек компонентов нет | | 8 | Компоненты глупые: данные пропсами, запросы на уровне экрана | ревью-вопрос: есть ли `useQuery` внутри `src/ui/`? Должно быть «нет» | | 9 | Комментарии — одна-две строки «почему» | ревью глазами; проектная норма | | 10 | Каждый экран открывается в изоляции: свой маршрут, своя фикстура | **машинно** косвенно: скриншот-скрипт снимает экран по URL. Не открывается по прямой ссылке — не снимется | | 11 | `styles.имяКласса` ссылается на существующий класс | **машинно**: `src/cssModules.test.ts`. Vite типизирует модуль как `{ [key: string]: string }`, поэтому опечатка даёт `className="undefined"` тихо — тайпчек и линт её пропускают. Правило заведено не впрок: на витрине такая ссылка уже нашлась | **Контрольный вопрос владельца** (`§5.1`, применять к каждому пакету работ): добавление нового состояния главы или нового вида замечания правит **один** файл. Правит три — структура неверна, переделать до движения дальше. Ответ этой структуры: состояние главы живёт как строка типа в `src/api/types.ts` и как одна запись в карте отображения `src/features/chapters/`. Цвет индикатора — токен. Новый вид замечания — запись в карте видов, компонент выноски не трогается. --- ## 4. Протокол скриншот-цикла Цель — не «код валиден», а «вид совпал». Смотреть на построенное обязательно, а не предполагать. **Команда:** `npm run shot [маршрут ...] [--size ШxВ]`. Без аргументов снимает `/showcase` при 1440×900. **Что делает:** 1. собирает и поднимает `vite preview` (сборка, а не dev — dev-оверлеи не должны попадать в кадр); 2. запускает Chromium из Playwright, вьюпорт **1440×900** (базовая ширина из §4.5 промта), `deviceScaleFactor: 2` — чтобы снимок был сравним с референсом, снятым на macOS при 2x; `--size 1280x764` даёт вьюпорт самого референса, когда нужно прямое наложение; 3. ждёт `networkidle` и загрузки шрифтов (`document.fonts.ready`) — иначе в кадр попадает фолбэк-шрифт; 4. кладёт PNG в `frontend/.shots/<маршрут>.png`. **Что делаю я после команды — обязательная часть цикла, а не опция:** открываю PNG инструментом чтения файлов, смотрю на него, кладу рядом `references/fleet.png`, называю расхождения словами, правлю, снимаю снова. **Стенд без root.** `.tooling/` собирается один раз и в репозиторий не едет (вместе с `.shots/` он в `.gitignore`). Chromium не стартует, пока не найдёт `libnss3`, `libnssutil3`, `libnspr4`, `libasound.so.2`; в системе их нет, а sudo недоступен, поэтому пакеты выкачиваются и распаковываются локально. Тем же способом кладётся шрифт CJK: на стенде не установлено ни одного (`fc-list :lang=zh` пуст), и без него весь китайский текст в кадре — квадраты, а плотность колонок проверить нечем. `scripts/shot.mjs` сам подставляет `LD_LIBRARY_PATH` и `XDG_DATA_HOME`, когда видит `.tooling/root` — думать об этом больше не нужно. ```bash mkdir -p .tooling && cd .tooling apt-get download libnss3 libnspr4 libasound2t64 fonts-noto-cjk for d in *.deb; do dpkg -x "$d" root; done rm -f *.deb && cd .. ``` Шрифт нужен только стенду: продукт отдаёт CJK системному стеку (`STACK_DECISIONS.md` §2), на Windows у пользователя он есть. --- ## 5. Критерий сходимости с референсом **Растровое совпадение не является целью и недостижимо:** Fleet закрыт (скачивание прекращено 22.12.2025), референс снят на macOS при 2x, цель — Chromium с другим хинтингом шрифтов. Эталонных скриншотов в тестах нет — вместо них контракт-тест токенов (`STACK_DECISIONS.md` §3). **Сходиться обязаны:** цвета · промежутки 8px · радиусы 6px · плотность строк · общее впечатление. **Подбираются на глаз:** кегли, интерлиньяж, внутренние отступы — померить их больше негде. ### 5.1. Замеры, перепроверенные в этой сессии Значения §1.1 промта перепроверены заново по `references/fleet.png` (PIL: гистограмма всего изображения, срезы строк и столбцов, детект края скругления по порогу яркости). **Все числа совпали, одна роль разошлась:** §1.1 отдаёт `#353739` и «выбранной строке», и «активной вкладке», а в кадре это разные вещи — вкладка открытого документа залита `#142f4c`, вкладка панели `#27292b`, и `#353739` под вкладками не встречается вовсе. В токенах роли разведены. Ниже — сводка с тем, что удалось доснять; это и есть входные числа `tokens.css`. Масштаб снимка 2x подтверждён независимо: все четыре промежутка между панелями равны ровно 16 физическим пикселям, что даёт целые 8 CSS-пикселей. **Поверхности** (доля площади — по гистограмме всего кадра 2560×1528): | Роль | Значение | Подтверждение | |---|---|---| | фон оболочки | `#090909` | 10.45% площади | | заливка панели | `#17191a` | 80.48% площади | | приподнятая поверхность, наведение, **активная вкладка панели** | `#27292b` | 18 196 px; вкладки `Files`, `Terminal`, `AI Assistant` | | выбранная строка дерева, клавиша-чип | `#353739` | 37 374 px; ровно три пятна в кадре, и все три — строка дерева и чипы клавиш | | **активная вкладка открытого документа** | `#142f4c` | **поправка**: заливка вкладки `rpc-node.ts` — 7 183 px синеватого, а не серого | | подсветка текущей строки | `#152945` | 36 800 px | | выделение | `#164e8d` | 476 px | | текст основной | `#dfe1e3` | 0.40% площади | | текст вторичный | `#8a8e91` | **доснято**: статус-полоса | | текст приглушённый | `#707479` | **доснято**: колонка номеров строк | | акцент | `#746deb` | 1 258 px, рамка поля ввода | | красный индикатора | `#b82e45` | 829 px | **Геометрия** (физические пиксели / 2): | Величина | CSS | Как замерено | |---|---|---| | промежуток между панелями | **8** | четыре среза по 16 физ. без исключений | | поле оболочки по краям | **8** | тот же срез: слева и справа те же 16 физ. | | радиус панели | **6** | угол выходит на прямую за 12 физ. | | верхняя полоса | **28** | верхняя полоса фона 72 физ. = 8 поля + 28 полосы | | статус-полоса | **20** | нижняя полоса фона 56 физ. = 20 полосы + 8 поля | | шапка панели (вкладки) | **26** | заливка вкладки `Files` 86..137 физ. | | шаг строки дерева | **26** | шаг текстовых полос: 52 физ., 15 строк подряд | | подложка выбранной строки | **24** | заливка `#353739` — 48 физ. при шаге 52 | | межстрочный интервал содержимого | **21** | подсветка текущей строки 42 физ. | | ширина боковой панели при 1280 | **320** (25%) | границы панелей: 8‥328 · 336‥944 · 952‥1272 | | внутренний отступ панели | **6** | подложка строки 14‥321.5 при панели 8‥328 | Модель раскладки, вытекающая из замеров и сходящаяся точно (764 = 8+28 + 700 + 20+8): поле оболочки 8 · промежуток 8 · верхняя полоса 28 · статус-полоса 20. **Типографика** (оценка по высоте глифов, точнее не измеряется): интерфейс ~13px (высота выносных элементов 10px), статус-полоса ~12px, содержимое ~13px моноширинным при интерлиньяже 21px. **Из `antigravity_chat.png` — модель выноски замечания** (§3.8 промта): левая граница 3px, синяя `#2964ad` (заметка) / зелёная `#3c7d4f` (подсказка), заливки текста нет. Палитра Antigravity (нейтральный серый, без холодного оттенка): `#101010` фон · `#161616` боковая панель · `#1c1c1c` приподнятая карточка · `#252525` кнопка основного действия · `#323232` разделитель · `#cecece` текст. ⚠ Две палитры в одном приложении не смешиваются. Базовая — Fleet; из Antigravity берётся **форма** пустого состояния и выноски, не цвета. Токенов Antigravity в `tokens.css` не заводим. ### 5.2. Витрина сведена с референсом — что сошлось и что нет Витрина снята тем же способом, что и референс (`--size 1280x764`, 2x), и промерена тем же кодом. **Сошлось до пикселя:** промежутки — четыре по 16 физ.; верхний край панелей — 72 физ.; радиус — угол выходит на прямую за 12 физ.; шаг строки дерева — 26 CSS; нижняя полоса — 56 физ.; доли площади поверхностей (панель 83.5% против 80.5%, фон 9.4% против 10.5% — разница от того, что у нас другое наполнение). Высота глифов статус-полосы совпала точно (23 физ.), вкладки — 19 против 20 физ. **Расхождения, названные вслух:** 1. **Субпиксельное сглаживание.** У Fleet текст сглажен в серую шкалу (macOS): в статус-полосе ровно 0% цветных пикселей. У нас в том же месте 1.29%, в дереве 1.35% против 0.37% — это цветная бахрома Chromium. `-webkit-font-smoothing: antialiased` в сбросе действует только на macOS. Расхождение принято: §8 промта прямо снимает совпадение по хинтингу, а на Windows субпиксельное сглаживание — норма платформы. Гасить его флагом в скриншот-цикле не стали: снимок должен показывать то, что рисует настоящий браузер. 2. **Колонка оригинала приглушена** (`--color-text-secondary`) — это решение, а не замер: во Fleet на этом месте подсвеченный код. Пересмотреть на S6, когда читалка будет настоящей. 3. **`+` в конце рядов вкладок** у Fleet есть, у нас нет — добавляется вместе с действием, которое он будет запускать (S2). 4. **Наведение и прочие интерактивные состояния** витриной не проверены: снимок статичен. Токен `--color-raised` подтверждён только на активной вкладке. 5. **Боковые панели фиксированы 320px** (замеренная абсолютная ширина при 1280). Тянущиеся панели на `react-resizable-panels` — S2; тогда же решится, тянуть их долей или пикселями. 6. **Скроллбар в кадр не попадает.** Chromium на стенде отдаёт оверлейные полосы (`offsetWidth - clientWidth = 0`), поэтому в статичном снимке их нет. Что стилизация применяется — проверено вычисленными значениями: `scrollbar-width: thin`, `scrollbar-color: rgb(53, 55, 57) rgba(0, 0, 0, 0)`. На Windows полосы займут место и получат эти цвета. ### 5.3. Что выяснилось про инструменты — не переоткрывать - **`eslint-plugin-react-hooks`**: flat-конфиг лежит в `configs.flat['recommended-latest']`. Одноимённый ключ верхнего уровня — старого формата, ESLint 10 на нём падает с ошибкой про «plugins as array». - **Vitest по умолчанию подменяет CSS пустой заглушкой.** Без `test.css: true` контракт-тест токенов сверял бы пустоту и был бы вечно зелёным. Проверено: до включения он падал на пустой строке, а не проходил. - **happy-dom не понимает заявление `@layer a, b;`** — проглатывает весь остаток файла, и `getComputedStyle` перестаёт видеть переменные. Поэтому заявление слоёв живёт в `reset.css`, а `tokens.css` остаётся чистым (§2.1). Блочную форму `@layer x { }` он тоже не поддерживает. - **`@types/node` держим на мажоре 22**, а не на latest 26: иначе тайпчек разрешает API, которых на стенде нет. - **`import.meta.glob` по `*.module.css` брать с `?raw`, а не `?inline`:** `?inline` отдаёт уже скомпилированный CSS с хешированными именами (`._shell_1abc_1`), и сверять с ним имена из TSX бессмысленно. - **Гейты и оба структурных теста проверены живым нарушением, а не заявлением:** литерал цвета в модуле · литерал в сокращённой записи `border` · `font-size` числом · попытка отключить правило комментарием · hex в TSX · инлайновый стиль с обычным свойством · импорт глобального CSS мимо `main.tsx` · опечатка в имени класса. Каждый раз проверка падала, после отката — зелено. Разрешённое исключение `style={{ '--dot': … }}` проходит. --- ## 6. Что уже известно про движок — учтено в форме данных Из `FRONTEND_SESSION_PROMPT.md` §9 и `STACK_DECISIONS.md` §8. Влияет на типы в `src/api/` уже сейчас, поэтому записано в плане, а не откладывается на S3. 1. **Прогресс — пофазный, не «готово N из M».** Юнит становится `done` только когда есть и черновые строки всех членов, и строка редактуры: во время черновой волны сквозной счётчик стоит на нуле почти всё время. Тип прогресса — две пары `{ done, total }` (черновик ∥ редактура), а не одно число. Пользователю всё равно показывается одна полоса без названий фаз (§4.1). 2. **Подпись термина — не UPDATE строки.** Банк пересобирается из файлов на каждом прогоне, прямая запись стирается следующим прогоном. Контракт подписи — отправка набора решений, а не правка записи; фикстуры S3 моделируют именно это. 3. **Байтовых смещений у замечаний нет.** Замечание адресует блок или главу — и только. Тип замечания не имеет полей `offset`/`length`; подчёркиваний внутри текста не будет. 4. **Единица пары — edit-unit, ~1.9 на главу.** Местами вся глава окажется одним блоком. Читалка обязана нормально выглядеть в этом случае; выравнивание грубое, по единицам экспорта. 5. **Одна вертикальная прокрутка на читалку.** Список строк-пар, внутри строки `grid-template-columns: 1fr 1fr`. Синхронизации двух прокруток нет и не будет — колонки физически не могут разъехаться. Денежных полей в интерфейсе нет нигде (§4.8): ни сумм, ни потолков, ни оценки, ни остатка. --- ## 7. Порядок S1 и что в него НЕ входит Порядок работ S1: каркас → `npm run check` → скриншот-цикл и проверка, что он живой → `tokens.css` → витрина → контракт-тест токенов → сверка витрины с референсом. `npm run check` = `prettier --check` → `eslint` → `stylelint` → `tsc --noEmit` → `vitest run`. Шагов пять, а не четыре: `STACK_DECISIONS.md` §3 перечисляет четыре, но там же требует, чтобы обе половины гейта цвета жили в одной команде, а CSS-половину гоняет именно stylelint. `npm run check:full` = `check` → `vite build` → `shot`. Отдельного e2e-набора в S1 нет: единственная браузерная проверка — скриншот-цикл, он и стоит в `check:full`. Git-хуков нет. **Не входит в S1** (и не должно появиться раньше срока): экраны, оболочка трёх панелей, слой данных и MSW, React Compiler (Ф-2), токен-гейт на отступы (Ф-4 — включается, когда шкала отступов устоится, иначе мешает подбору), эталонные скриншоты в тестах (отложены решением `STACK_DECISIONS.md` §3), светлая тема, мобильная раскладка.