/*
 * Палитра страницы живёт в переменных, а не в конкретных значениях: одно и то же правило красится
 * переменной, поэтому палитру видно в одном месте и правка не разносится по десяткам правил. Inline-
 * стили запрещены политикой style-src 'self', так что перекрашивать страницу на лету было бы нечем.
 * Темы, между которыми посетитель выбирает, на странице нет: палитра одна, и второй набор правил
 * под атрибут держали бы в головах двух читателей, из которых один всегда устаревает.
 */
:root {
    /*
     * Страница сообщает браузеру, какие у него собственные элементы — полосы прокрутки, курсор,
     * раскрывающийся список (подшаг P29.3). Без этой строки они остаются нейтральными: замер перед
     * той правкой показал `color-scheme: normal` и светлую полосу в 15 px поверх тёмной тогда
     * страницы. С подшага P41.1 оформления на странице одно, поэтому и строй один.
     *
     * Слово `only` добавлено не для порядка (подшаг P45.1): без него браузер вправе перекрасить
     * страницу под свой тёмный режим. Замер прогоном живого Chrome по DevTools-протоколу с
     * вооружённым конвейером перекрашивания (`--force-dark-mode --enable-features=WebContentsForceDark`,
     * отчёт `gap4402-repaint-headed-fdw.txt`): панель кода становится rgb(30,29,25) при 96.6%
     * тёмных пикселей вместо бумажных rgb(255,253,248) при 93.6%, а лист целиком — 95.2% тёмных.
     * С `light only` те же аргументы Chrome красят страницу как прежде: снимок панели совпадает
     * байт в байт со снимком без тёмного режима, отчёт `gap4402-repaint-guarded-fdw.txt`, хотя
     * медиалог `prefers-color-scheme` в значении `dark` странице по-прежнему соответствует. Тот же
     * прогон с объявлением, добавленным к стилям уже после загрузки страницы, перекрашивания НЕ
     * снимает (отчёт `gap4402-repaint-headed-fdw-guard.txt`: панель остаётся тёмной на 94.1%) —
     * браузер решает на загрузке, поэтому объявление продублировано meta-тегом в шаблонах и охраняется
     * тестом `BrowserRepaintOptOutTest`.
     */
    color-scheme: light only;

    /*
     * Типографика бумажной вёрстки (подшаг P41.2): текст страницы набран серифным, всё, что
     * показывает код и имена объектов, — моноширинным. Стеки вынесены переменными потому, что
     * моноширинный нужен не только панели кода: три её слоя, `textarea` формы, `pre` справочника и
     * подписи карточек графа обязаны получить один и тот же набор шрифтов, иначе метрики разъедутся.
     */
    --serif: Georgia, "Palatino Linotype", "Times New Roman", serif;
    --mono: "Cascadia Mono", Consolas, monospace;
    /* Ширина печатной колонки: текст и граф стоят в одной колонке, и длинная строка набранного
       текста читается так же, как в макете (70rem ≈ 1120 px). */
    --page-w: 70rem;
    /*
     * Мера текста короткой формы (фаза P50): вход, регистрация и личный кабинет набираются своей
     * шириной, а не шириной всей страницы. 46rem ≈ 736 px — строка подписи остаётся читаемой, а
     * таблицы тянутся на всю колонку. Страницам разбора и правил эта мера не нужна: у них таблицы
     * с четырьмя-пятью столбцами, и им нужна полная ширина колонки.
     */
    --measure-w: 46rem;

    /*
     * Полоса высоты шапки страницы (подшаг P53.7): заголовок 2rem + нижнее поле .5rem + строки
     * подписи по 1.25rem. Две строки = 5rem, три = 6.25rem, четыре = 7.5rem, пять = 8.75rem, шесть =
     * 10rem, семь = 11.25rem. Переносы самой длинной подписи страницы начинаются с 816, 520, 412,
     * 350 и 300 px (замеры `nav5430-sweep-bounds.txt` и `nav5430-sweep-bounds2.txt`), а ступени
     * стоят чуть раньше этих переносов, чтобы полоса заранее была не меньше нужной.
     */
    --head-band: 5rem;

    /*
     * Бумажная палитра (макет 07 «Бумажная вёрстка», выбран посетителем): тёплый фон листа, цвет
     * текста — тёмные чернила, один акцент на всё оформление, линейки вместо рамок-коробок.
     */
    --page-bg: #f6f3ec;
    --page-fg: #221f1a;
    --muted-fg: #6d6558;
    --border: #d8d2c4;
    --notice-fg: #7c2d12;

    --badge-border: #b9b0a0;
    --badge-error-border: #a4462f;
    --badge-error-fg: #7c2d12;
    --badge-info-border: #4f7a5f;
    --badge-info-fg: #2f5138;
    --badge-unknown-border: #a2988a;
    --badge-unknown-fg: #4a453b;

    --panel-bg: #fffdf8;
    /* Краски главной кнопки панели ввода (подшаг P38.2): акцент бумажной палитры говорит, какое
       действие главное, и не отвечает за состояние — отказ или успех по-прежнему подписаны словами. */
    --go-bg: #7c3a1d;
    --go-fg: #fff8ef;
    --go-border: #5f2b14;
    --go-bg-hover: #5f2b14;
    /* Краски плавающей панели кнопок холста (подшаг P31.3): полупрозрачный фон, тонкая рамка и
       мягкая тень — чтобы панель лежала ПОВЕРХ карточек, а не вырезала из них кусок. */
    --glass-bg: rgba(255, 253, 248, .86);
    --glass-border: rgba(34, 31, 26, .18);
    --glass-shadow: 0 1px 6px rgba(34, 31, 26, .16);
    --node-fill: #fffdf8;
    --node-stroke: #b8b0a0;
    --node-subtitle: #6d6558;
    /* Оттенки слоёв той же тёплой семьи, что и лист: слой по-прежнему различается заливкой карточки
       (это объясняет легенда), но карточка не спорит с фоном страницы кислотным цветом. */
    --layer-raw: #eef1f5;
    --layer-staging: #f6ecd9;
    --layer-mart: #e9efe2;
    --layer-temp: #efe6ef;
    /* Проводки графа — печатная линия: цвет тот же, что у тёмной линейки, а не отдельный «графовый»
       цвет. Выбранные ребро и карточка получают акцент — он же у главной кнопки. */
    --edge-stroke: #4a4438;
    --arrow-fill: #4a4438;
    --selected-stroke: #7c3a1d;
    --port-fill: #fffdf8;
    --port-stroke: #8a8272;
    --row-hover: #f2eee4;
    /*
     * Цепочка колонок (подшаг P25.2): свой цвет, а не «выделенное жирнее». Выделение — про одну
     * связь, цепочка — про путь значения через несколько связей, и различать их надо по цвету.
     * В бумажной палитре это печатная зелень, она отделяется от рыжеватого акцента выделения.
     */
    --chain-stroke: #3f6b3a;
    --chain-row: #e4ecdf;

    --gap-error: #8a1f1f;
    --gap-unknown: #8a6b1f;
    --selection-bg: rgba(124, 58, 29, .24);

    --code-bg: #fffdf8;
    --code-fg: #221f1a;
    --line-number-fg: #9a9081;

    /*
     * Полосы прокрутки — часть палитры, а не настройка браузера (подшаг P29.3). Размер вынесен в
     * переменную потому, что он нужен не только правилам полос: слой окраски и колонка номеров
     * резервируют под полосу столько же (подшаг P29.4).
     */
    --scrollbar-size: .65rem;
    --scrollbar-track: #ece7db;
    --scrollbar-thumb: #c6bdae;

    /*
     * Краски лекс — те же оттенки, что были у страницы до бумажной вёрстки (значения палитры коммита
     * 8ca8126: зарезервированное слово фиолетовое, функция пурпурная, строка зелёная, число синее).
     * Посетитель попросил вернуть именно их; лист остался бумажным. Прогон по DevTools-протоколу меряет
     * фактический контраст каждой краски к фону панели кода rgb(255, 253, 248): до правки это
     * gap4107-tokens-tokens.txt, после — gap4107-tokens-p421.txt, где наихудшая пара — незарезервированное
     * ключевое слово 5.56:1 и комментарий 5.61:1, затем переменная 5.83:1, оператор 6.39:1, строка 6.57:1
     * и выше до имени 18.58:1: знакомый строй читается на светлом листе не хуже, чем читался на белом.
     */
    --tok-comment: #5a6b5a;
    --tok-string: #0b6a3d;
    --tok-number: #1f4b9b;
    --tok-keyword: #5b21b6;
    /*
     * Цвет «не зарезервированного ключевого слова» нарочно не новый случайный оттенок, а тот же
     * акцент, что у зарезервированного, только светлее: цвет говорит «слово из перечня ключевых
     * слов диалекта», а степенью различает «зарезервировано» и «может быть именем».
     */
    --tok-keyword-non-reserved: #7c4dc4;
    --tok-function: #a11e76;
    --tok-quoted: #0f5f7e;
    --tok-variable: #8a5a00;
    --tok-name: #111111;
    --tok-operator: #aa3333;
    --tok-punctuation: #555555;
    --tok-unknown: #8a1f1f;
}

/*
 * Колонка страницы стоит по центру окна (фаза P50). Живой замер до правки
 * (`nav5010-geom-before.txt`, `nav5010-geom-before-1440.txt`): при внутреннем окне 1249 px
 * коробка страницы лежала в 32..1152 — то есть 32 px слева и 97 px справа, а в окне 1424 px
 * разрыв был 32 против 272: всё содержимое жало к левому краю. Боковые отступы `auto` делают
 * поля одинаковыми.
 * <p>
 * Ширина колонки взята через `min()`, и это тоже результат замера, а не осторожность: одними
 * только `margin: 2rem auto` на узком окне (внутренние 844 px) авто-отступы схлопываются в ноль,
 * и страница упирается текстом в самый край окна — прежний боковой отступ в 2rem при этом
 * пропадал. Запись `100% - 4rem` оставляет эти 2rem с каждой стороны при любом окне, а
 * центрирование работает там, где колонка влезает целиком.
 */
body {
    font-family: var(--serif);
    margin: 2rem auto;
    background: var(--page-bg);
    color: var(--page-fg);
}

/*
 * Колонка текста страницы (фаза P53, подшаг P53.4): та же колонка, что была у `body`, переехала на
 * элемент `.column`, а `body` растянут на всю ширину окна. Причина переезда — замер верхней панели
 * (`nav5300-geom-before.txt`): панель стояла внутри `body`, и на страницах с короткой формой
 * (`form-page`, мера 736 px) та же панель начиналась с 352 px вместо 155 px при окне 1440 px — меню
 * перескакивало на 197 px при переходе со страницы на страницу. Теперь колонка принадлежит тексту, а
 * панель занимает полосу полной ширины окна и на всех страницах стоит в одном и том же месте.
 * <p>
 * Значения ширины при этом прежние: `min()` с `100% - 4rem` оставляет 2rem с каждой стороны на узком
 * окне, и живой замер P50 (`nav5010-geom-before.txt`, коробка страницы 32 px слева против 97 px
 * справа при окне 1249 px) остаётся закрытым той же записью, только на другом элементе.
 */
.column {
    max-width: min(var(--page-w), 100% - 4rem);
    margin-inline: auto;
}

/*
 * Страницы с короткой формой — вход, регистрация и личный кабинет — набираются своей мерой текста
 * (фаза P50). Та же правка, снятая тем же замером: в коробке страницы шириной 1120 px таблица
 * учётной записи занимала 314 px, сводка — 320 px, а три таблицы статистики — 682, 636 и 659 px, и
 * все они начинались в 32 px от левого края. Даже отцентрированная колонка оставляла текст левее
 * центра, а абзац подписи растягивался на все 1120 px. Мера 736 px держит строку читаемой; таблицы
 * тянутся на всю колонку, и числа встают под своими заголовками у правого края.
 */
body.form-page .column {
    max-width: min(var(--measure-w), 100% - 4rem);
}

/*
 * Таблицы короткой формы идут на всю колонку: линейка строки читается как строка набора, а не как
 * короткая черточка под подписью.
 */
body.form-page section table {
    width: 100%;
}

/*
 * Два числовых столбца статистики прижаты вправо вместе со своими заголовками: подпись показателя
 * читается слева, числа — справа, и строку сводят по линейке, а не ищут числа в середине строки.
 */
.stats-facts th[scope="col"]:nth-child(2),
.stats-facts th[scope="col"]:nth-child(3),
.stats-facts td {
    text-align: right;
    white-space: nowrap;
}

/*
 * Цвет выделения полупрозрачен намеренно. В панели кода цветные глифы рисует слой под полем, а само
 * поле, когда текст совпал с разобранным, красит свои глифы прозрачно (правило is-analyzed), так что
 * непрозрачный фон выделения лёг бы поверх слоя и закрыл слова: замер перед правкой дал блок
 * `#2a3c5c` на всю выделенную ширину, под которым текст не читался. Полупрозрачный фон оставляет
 * слова видимыми и в панели кода, и в таблицах графа.
 */
::selection {
    background: var(--selection-bg);
}

/*
 * Полосы прокрутки красятся палитрой страницы (подшаг P29.3). До правки в стилях не было ни одного
 * правила о полосах, и прогон Chrome показал светлую полосу шириной 15 px поверх тёмной страницы.
 * Каналов два, и оба обязательны, но не вместе. `color-scheme` в палитре сообщает браузеру его
 * строй — это собственные элементы браузера (курсор, раскрывающийся список, полосы в Firefox, где их
 * не расписывают). Толщину и цвета полос в этом же блоке задают псевдоэлементы `::-webkit-scrollbar*`.
 * Стандартные `scrollbar-color` и `scrollbar-width` здесь не работают и удалены: первая попытка
 * была измерена живым Chrome — при них поле продолжало рисовать полосу в 15 px, хотя переменная
 * толщины говорила `.65rem`, то есть псевдоэлементы переставали действовать.
 */
::-webkit-scrollbar {
    width: var(--scrollbar-size);
    height: var(--scrollbar-size);
}

::-webkit-scrollbar-track {
    background: var(--scrollbar-track);
}

::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: calc(var(--scrollbar-size) / 2);
}

::-webkit-scrollbar-corner {
    background: var(--scrollbar-track);
}

/*
 * Желобок под полосу прокрутки сохраняется на всех страницах (фаза P53, подшаг P53.4). Замер
 * после переезда панели (`nav5300-geom-after.txt`, окно 860 px) оставил последний скачок меню: на
 * страницах, которые выше окна (главная, граф, справочник), браузер рисовал полосу в 10 px, и правый
 * край панели вставал в 818 px, а на коротких страницах входа и регистрации — в 828 px. Ширина при
 * этом не менялась: двигался только край. `scrollbar-gutter: stable` оставляет этот желобок постоянным,
 * поэтому и панель, и колонка набора стоят в одних координатах независимо от высоты страницы.
 */
html {
    scrollbar-gutter: stable;
}

textarea {
    width: 100%;
    min-height: 14rem;
    font-family: var(--mono);
    background: var(--page-bg);
    color: var(--page-fg);
    border: 1px solid var(--border);
}

/*
 * Надписи на элементах формы набираются тем же шрифтом, что и текст страницы (подшаг P41.2): без
 * этой строки браузер рисует на кнопках и полях свой шрифт, и на бумажной странице они выглядят
 * чужими вкладками. Метрики панели кода этим не задеваются: у трёх её слоёв моноширинный стек
 * задан своим правилом.
 */
button,
input,
select {
    background: var(--page-bg);
    color: var(--page-fg);
    border: 1px solid var(--border);
    border-radius: 2px;
    font-family: inherit;
}

button {
    padding: .3rem .7rem;
    border-radius: 2px;
    letter-spacing: .04em;
}

a {
    color: inherit;
}

label {
    display: block;
    margin-bottom: 1rem;
}

section {
    margin-top: 2rem;
}

/*
 * Таблицы печатаются линейками, а не клетками (подшаг P41.2): у строки только нижняя линейка, у
 * шапки — линейка цветом текста, вертикальных линеек нет. Смысл тот же, что у линейки под шапкой
 * панели: граница таблицы уже есть у самой панели, и клетка поверх неё только дробит текст.
 */
table {
    border-collapse: collapse;
    margin-top: .5rem;
}

th {
    border: 0;
    border-bottom: 1px solid var(--page-fg);
    padding: .35rem .6rem .3rem;
    text-align: left;
    font-size: .78rem;
    font-weight: 600;
    letter-spacing: .12em;
    color: var(--muted-fg);
}

td {
    border: 0;
    border-bottom: 1px solid var(--border);
    padding: .4rem .6rem;
    text-align: left;
}

.status,
.summary {
    font-weight: 600;
}

.badge {
    display: inline-block;
    border: 1px solid var(--badge-border);
    border-radius: 999px;
    padding: .1rem .45rem;
    font-size: .82rem;
    letter-spacing: .02em;
}

.badge-error,
.badge-warn {
    border-color: var(--badge-error-border);
    color: var(--badge-error-fg);
}

.badge-info {
    border-color: var(--badge-info-border);
    color: var(--badge-info-fg);
}

.badge-unknown {
    border-color: var(--badge-unknown-border);
    color: var(--badge-unknown-fg);
}

.notice {
    color: var(--notice-fg);
}

/*
 * Пояснение под холстом графа (подшаг P26.2) нужно только в один момент: когда кликнуто поле без
 * колоночных связей. Пустой абзац стоял бы пустой строкой под холстом и съедал место, отданное
 * карточкам, поэтому пустой элемент не участвует в разметке.
 */
.chain-notice:empty {
    display: none;
}

.empty-state {
    font-weight: 600;
}

select,
button {
    margin-bottom: 1rem;
}

/*
 * Шапка страницы — одна и та же полоса на всех страницах (подшаг P53.7, обращение посетителя от
 * 2026-11-04: «шапку страницы не исправил, высота разная на страницах, проверь css»). До правки
 * замер в Chrome (отчёт `nav5430-sweep-before.txt`, окна 1440/1180/1024/900/820/768/700/640/560/
 * 480/420/380) показал шесть разных шапок: 51 px на входе и в кабинете (только заголовок), 74 px на
 * регистрации, 86 px на справочнике правил, 105 px на главной, 74 px на странице графа при окне
 * 1440 и 91 px при 1024; набор страницы начинался с 115 px по 195 px. Причины три: на странице
 * графа заголовок и подпись стояли одной строкой (`display: flex`), на остальных — столбиком;
 * подписи на разных страницах имели разный шрифт и разные поля; и у блока, который идёт под шапкой,
 * верхнее поле было своё на каждой странице (зазор выходил 0, 6, 12, 16 и 32 px).
 * <p>
 * Теперь шапка на всех страницах набрана одной обёрткой `header.page-head` с заголовком и подписью,
 * полоса её высоты задана числом `--head-band`, а зазор до набора страницы — один (`margin-bottom`
 * шапки при нулевом верхнем поле следующего блока). Высота полосы выбирается по самой длинной
 * подписи страницы на этом окне: с учётом переносов это от двух до семи строк подписи по 1.25rem,
 * ступеней шесть (замеры `nav5430-sweep-after.txt`, `-bounds.txt` и `-bounds2.txt`).
 * Оговорка названа прямо: полоса — `min-height`, а не жёсткая высота. Если подпись на каком-то окне
 * перенесётся в больше строк, чем держит ступень, шапка на этой странице вырастет выше полосы, и
 * равенство на этом окне пропадёт; ступени потому и поставлены с запасом, что прогонами проверены
 * ширины от 1440 до 300 px, а не все подряд.
 * <p>
 * Что при этом стало хуже и это решение посетителя, а не моё: страница графа раньше экономила
 * вертикальное место одной строкой шапки (подшаг P41.2), а теперь её шапка стоит столбиком и
 * занимает ту же полосу, что и везде. Набор страницы графа при окне 1440 px начинается с 132 px
 * до правки и с 178 px после — на 46 px ниже (замеры `nav5430-sweep-before.txt` и
 * `nav5430-sweep-after.txt`). Холст графа при этом остаётся своим 70vh: высота холста считается от
 * окна, а не от шапки.
 */
.page-head {
    min-height: var(--head-band);
    margin: 0 0 1.25rem;
}

/* Верхнее поле первого блока под шапкой снято: зазор между шапкой и набором задаёт одна шапка.
   Иначе на разных страницах к зазору прибавлялось своё поле блока (замеры 0, 6, 12, 16 и 32 px). */
main.column > .page-head + * {
    margin-top: 0;
}

/*
 * Ступени полосы шапки (подшаг P53.7). Самая длинная подпись страницы — подпись главной — переносится
 * на большую строку на измеренных ширинах: две строки ещё при 820 px, три начиная с 816 px, четыре
 * начиная с 520 px, пять начиная с 412 px, шесть начиная с 350 px, семь начиная с 300 px (прогоны
 * `nav5430-sweep-bounds.txt` и `nav5430-sweep-bounds2.txt`). Ступени поставлены чуть раньше переноса,
 * чем нужно: при 820 px полоса уже три строки, при 532 - уже четыре, при 424 - уже пять, при 364 -
 * уже шесть, при 324 - уже семь. Это стоит лишнего воздуха на нескольких узких интервалах ширины, но
 * держит равенство на любых ширинах между замеренными: полоса заранее не меньше, чем потребуется.
 * Шаг ступени - одна строка подписи (1.25rem).
 */
@media (max-width: 51.25rem) {
    :root {
        --head-band: 6.25rem;
    }
}

@media (max-width: 33.25rem) {
    :root {
        --head-band: 7.5rem;
    }
}

@media (max-width: 26.5rem) {
    :root {
        --head-band: 8.75rem;
    }
}

@media (max-width: 22.75rem) {
    :root {
        --head-band: 10rem;
    }
}

@media (max-width: 20.25rem) {
    :root {
        --head-band: 11.25rem;
    }
}

/*
 * Заголовок страницы набирается одинаково на всех страницах (подшаг P41.2, полоса и высота строки
 * добавлены подшагом P53.7): без явной высоты строки полоса шапки зависела бы от браузерного
 * «normal», и две одинаковые подписи давали бы разную высоту полосы.
 */
h1 {
    font-size: 1.7rem;
    line-height: 2rem;
    font-weight: 400;
    letter-spacing: -.01em;
    margin: 0 0 .5rem;
}

.page-note {
    margin: 0;
    color: var(--muted-fg);
    font-size: .95rem;
    line-height: 1.25rem;
}

/*
 * Верхняя панель навигации (подшаг P48.4): слева имя проекта и переходы по страницам анализатора,
 * справа вход или личный кабинет. Панель — строка с переносом, а правая группа прижата
 * `margin-left: auto`: на узком окне правая группа переносится целиком на вторую строку, а не рвётся
 * по одной ссылке. Кнопка выхода набрана тем же шрифтом, что ссылки рядом, и подчёркнута так же:
 * переход и действие стоят в одном ряду и не должны выглядеть элементами разной природы.
 * <p>
 * Ширина полосы панели не зависит от страницы (фаза P53, подшаг P53.4): панель стоит в `body`,
 * растянутом на всю ширину окна, и её собственная полоса ограничена той же переменной `--page-w`, что
 * и колонка обычных страниц. До переезда панель наследовала ширину колонки страницы, и на страницах с
 * короткой формой левый край меню стоял на 197 px правее, чем на главной (замер
 * `nav5300-geom-before.txt`, окно 1440 px: 155 px против 352 px).
 */
.topbar {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .4rem 1rem;
    max-width: min(var(--page-w), 100% - 4rem);
    margin: 0 auto 1.25rem;
    padding-bottom: .5rem;
    border-bottom: 1px solid var(--border);
    font-size: .95rem;
}

.topbar-group {
    display: inline-flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .4rem 1rem;
}

/*
 * Имя проекта слева в панели (фаза P53, подшаг P53.2). Начертание здесь одно и то же на всех
 * страницах: живой замер панели показал, что у подписи открытой страницы жирное начертание, и из-за
 * него следующая ссылка уезжала на 7-10 px при переходе между страницами. Имя же не меняется, поэтому
 * оно жирным не двигает ничего; `white-space: nowrap` держит «SQL Pointer» в одной строке, чтобы имя
 * не переносилось на узком окне и не меняло высоту панели.
 */
.topbar-brand {
    font-weight: 600;
    letter-spacing: .04em;
    white-space: nowrap;
}

.topbar-right {
    margin-left: auto;
}

.topbar-name {
    color: var(--muted-fg);
}

.topbar form {
    margin: 0;
}

.topbar button {
    font: inherit;
    color: inherit;
    background: none;
    border: none;
    padding: 0;
    /*
     * Внешний нижний отступ кнопки выхода снят (фаза P53, подшаг P53.4). Общий набор `select, button`
     * даёт элементам `margin-bottom: 1rem`, а в строке флексов поля не схлопываются: коробка формы
     * выхода вырастала до 33 px против 17 px у соседних ссылок и поднимала всю панель с 26 px до
     * 42 px, когда посетитель входил (замер `nav5300-height.txt`: button h=17 mb=16px, form h=33).
     * Первая догадка была про базовую линию, и замер её опроверг: выравнивание формы по середине
     * строки height панели не меняло.
     */
    margin: 0;
    cursor: pointer;
}

/*
 * Открытая страница не притворяется переходом: на её месте стоит подпись тем же текстом, тем же
 * кеглем и тем же начертанием, что у соседних переходов; отличаются только подчёркивание и цвет
 * (фаза P53, подшаг P53.4). Живой замер до правки (`nav5300-geom-before.txt`, окно 1180 px): подпись
 * открытой страницы была набрана жирным, и следующая за ней ссылка уезжала на 7 px, а при окне
 * 1440 px — на 10 px. Метка, от которой едут соседние переходы, — это то же прыгающее меню, только
 * мелкими шажками, поэтому у подписи открытой страницы здесь нет ни жирного начертания, ни полей.
 */
.topbar-current {
    color: var(--muted-fg);
    text-decoration: none;
}

.status-row {
    display: flex;
    align-items: baseline;
    gap: .3rem 1rem;
    flex-wrap: wrap;
    margin: 0 0 1rem;
}

.status-row p {
    margin: 0;
    font-size: .92rem;
}

.graph-caption {
    display: flex;
    align-items: baseline;
    gap: .4rem 1.25rem;
    flex-wrap: wrap;
    margin-bottom: .4rem;
}

.graph-caption h2 {
    margin: 0;
    font-size: .95rem;
    font-weight: 600;
    letter-spacing: .16em;
}

/*
 * Полоса графа (подшаг P44.1, семнадцатое обращение посетителя): слева дерево отношений и их колонок,
 * справа холст. Просьба — «а на странице графа можно сделать чтобы дерево слева от графа рисовалось?»,
 * содержание подтверждено тем же обращением — «Отношения → колонки этого запроса».
 *
 * Ширину мерили до правки (отчёт `gap4401-geom-before.txt`, окно 1440 px, фикстура разобрана, граф
 * вписан): холст 1120 px при внутренней ширине 1118 px, карточки занимают 1080 px из них, а слева от
 * холста нет ничего — он начинается у левого края колонки («elements that sit in the canvas band and
 * end at its left edge: NONE»). Пробная колонка 16rem, вставленная в живую страницу той же разметкой,
 * оставляет холсту 844 px. Если после этого граф не вписать заново, карточки вылезают за правый край
 * холста на 269 px; после вписывания кнопкой «Весь граф» приближение падает с 82% до 61%, и карточка
 * шириной 212 px становится 158 px. Это и есть цена дерева слева: у картины отнимается четверть
 * ширины, и ничем другим за неё не платят. Поэтому колонка 14rem, а ниже 60rem (960 px) она встаёт над
 * холстом и ширины ему не отнимает вовсе.
 */
.graph-band {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 1rem;
}

/*
 * Холст забирает остальное место, но не выталкивает дерево: `min-width: 0` обязателен, потому что у
 * flex-элемента минимум по умолчанию равен ширине содержимого, а содержимое холста — svg с шириной,
 * которую выставил скрипт.
 */
.graph-band > .graph-stage {
    flex: 1 1 auto;
    min-width: 0;
}

.tree-column {
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    max-height: 40vh;
    /* Прокрутка дерева не должна уезжать в прокрутку страницы: у колонки свой край. */
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    background: var(--panel-bg);
    padding: .45rem .6rem .55rem;
}

/*
 * Прокрутку принимает сам список, а не колонка целиком (замер после первой правки: 22 строки колонок
 * просят 1245 px при доступных 581 px). Если прокручивать колонку целиком, её подпись и пояснение
 * уезжают вместе со списком: пояснение о том, как пользоваться деревом, лежало бы под последним
 * элементом прокрутки, то есть там, где его не видно. `min-height: 0` обязателен: без него flex-элемент
 * отказывается становиться меньше своего содержимого, и список переполнил бы колонку вместо прокрутки.
 */
.tree-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Прокрутка списка не должна уезжать в прокрутку страницы: у списка свой край. */
    overscroll-behavior: contain;
}

@media (min-width: 60rem) {
    .graph-band {
        flex-direction: row;
        gap: 1.25rem;
    }

    /*
     * В широком окне дерево стоит слева и принимает высоту холста целиком (`align-items: stretch` у
     * полосы), а не растёт до своих строк: иначе полоса разъезжалась бы выше карточек. Замер после
     * первой правки (отчёт `gap4401-geom-overshoot.txt`): без предела 22 строки дерева сделали полосу
     * 1247 px высотой, холст растянулся следом, и под карточками осталось 666 px пустой обёртки.
     * Предел взят равным высоте холста: правило `.graph.graph-interactive` даёт холсту `height: 70vh`,
     * и у холста, и у колонки дерева свои 18 px полей и рамок (у холста `.5rem` и рамка 1 px, у
     * колонки те же 18 px), поэтому те же 70vh означают, что колонка вырастает ровно до обёртки
     * холста — 582 px при окне 900 px. Строк больше, чем места, — они уходят внутрь своей прокрутки.
     */
    .tree-column {
        flex: 0 1 14rem;
        min-width: 9rem;
        max-height: 70vh;
    }
}

.tree-caption {
    flex: 0 0 auto;
    margin: 0 0 .4rem;
    font-size: .84rem;
    font-weight: 600;
    letter-spacing: .16em;
}

.tree-list,
.tree-columns {
    margin: 0;
    padding: 0;
    list-style: none;
}

.tree-relation + .tree-relation {
    margin-top: .35rem;
}

/*
 * Имена отношений и колонок — это код, поэтому они набраны тем же моноширинным стеком, что подписи
 * карточек и поле ввода (подшаг P41.3). Кнопка, а не ссылка: действие относится к текущей картине,
 * страницы она не меняет, и без скрипта она остаётся тем, чем выглядит, — списком имён.
 */
.tree-relation-name,
.tree-column-name {
    display: block;
    width: 100%;
    margin: 0;
    padding: .1rem .3rem;
    border: 1px solid transparent;
    border-radius: 2px;
    background: none;
    color: inherit;
    font-family: var(--mono);
    font-size: .8rem;
    line-height: 1.35;
    text-align: left;
    /* Длинное имя переносится, а не обрезается: обрезанное имя было бы именем без части. */
    overflow-wrap: anywhere;
    cursor: pointer;
}

.tree-relation-name {
    font-weight: 600;
}

.tree-column-name {
    font-size: .76rem;
}

.tree-relation-name:hover,
.tree-column-name:hover {
    background: var(--row-hover);
}

/* Колонки относятся к своему отношению и визуально стоят внутри него, а не отдельным списком. */
.tree-columns {
    margin: .1rem 0 0 .45rem;
    padding-left: .4rem;
    border-left: 1px solid var(--border);
}

.tree-column-note {
    display: block;
    margin: 0 .3rem .15rem;
    color: var(--muted-fg);
    font-size: .7rem;
}

.tree-note {
    flex: 0 0 auto;
    margin: .15rem 0 0;
    color: var(--muted-fg);
    font-size: .74rem;
}

.tree-hint {
    flex: 0 0 auto;
    margin: .6rem 0 0;
    color: var(--muted-fg);
    font-size: .72rem;
}

/*
 * Строки дерева помечаются теми же классами, что карточки, и решает их один раз скрипт
 * (`updateSelection`): второго решения у дерева нет, поэтому «что выбрано» не может разойтись с тем,
 * что нарисовано на холсте. Коричневая рамка — выбранный узел, зелёная — узел на пути значения,
 * зелёная подложка — строка колонки на этом пути, серая — поле без колоночных связей (подшаг P26.2),
 * приглушение — общее правило `.dimmed`. Порядок правил тот же, что у карточек: цепочка перекрывает
 * выделение.
 */
.tree-relation.selected > .tree-relation-name {
    border-color: var(--selected-stroke);
}

.tree-relation.chain-node > .tree-relation-name {
    border-color: var(--chain-stroke);
}

.tree-column.chain-row > .tree-column-name {
    background: var(--chain-row);
}

.tree-column.solo-row > .tree-column-name {
    background: var(--row-hover);
    border-color: var(--border);
}

/*
 * Панель таблиц (подшаг P41.2): тонкая линейка по краю вместо рамки-коробки и внутренняя прокрутка
 * по горизонтали.
 *
 * Прокрутка по горизонтали нужна затем, что строки таблиц с кодом и длинными причинами не влезают в
 * узкое окно. Замер до правки: при окне 860 px страница уходила в горизонтальную прокрутку на 100 px.
 * Принадлежность починки мерили отдельно (отчёт `gap4103-overflow.txt`, окно 860 px, фикстура разобрана,
 * панели открыты, правила меняли средствами DevTools-протокола, потому что политика страницы запрещает
 * встроенные стили): без собственной прокрутки панели та же сборка снова вылезает за край на 18 px, а если
 * вместе с тем вернуть колонке сетки прежнюю ширину `1fr` - на 27 px. То есть полосу у панели держит
 * правило `overflow-x`, а `minmax(0, 1fr)` добавляет к этому 9 px: колонка перестаёт держать минимальную
 * ширину своего содержимого. Прежние 100 px - другой опыт: у прежней страницы были другие поля и другая
 * ширина, поэтому эти числа не одно и то же, и сравнивать их напрямую нельзя.
 */
details.data-panel {
    margin-top: 1.25rem;
    border: 1px solid var(--border);
    padding: .75rem 1rem 1rem;
    overflow-x: auto;
}

details.data-panel summary {
    cursor: pointer;
    font-weight: 600;
    font-size: .9rem;
    letter-spacing: .16em;
}

/*
 * Заголовки секций внутри панели таблиц набираются разряженно и мелко, как колонтитул печатной
 * полосы (подшаг P41.2): важнее порядок разделов, чем их крупные слова — таблицы под ними и так
 * открываются по клику.
 */
details.data-panel h2 {
    margin: 0 0 .25rem;
    font-size: .84rem;
    font-weight: 600;
    letter-spacing: .18em;
}

/* Ссылки верхней панели подчёркнуты с отступом подчёркивания (подшаг P41.2, перенесено в панель
   подшагом P48.4): на бумажной странице ссылка отличается чертой, а не цветом — акцент на странице
   один, и он принадлежит действиям. */
.topbar a {
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* Внутри сворачиваемой панели секции стоят плотнее, чем обычные секции страницы: таблицы и так
 * открываются по клику, и большие отступы внутри панели только растягивают её. */
details.data-panel section {
    margin-top: 1.25rem;
}

pre {
    font-family: var(--mono);
    overflow: auto;
}

/*
 * Две панели страницы наследования складываются в одну печатную колонку (подшаг P41.2): код сверху,
 * граф под ним. Это тот самый выбор, который показывает макет: граф перестаёт стоять рядом с полем и
 * уходит вниз, зато каждая полоса набора остаётся широкой, а колонки не делят ширину окна пополам.
 * Каркас разметки при этом не тронут: те же `editor-column` и `graph-column`, просто сетка одноколоночная.
 */
/* Верхнее поле страницы графа стало общим для всех страниц (подшаг P53.7): раньше оно было 1.25rem
   против 2rem у остальных, и полоса панели на странице графа оканчивалась на 46 px вместо 58 px —
   меню прыгало по вертикали при переходе на граф (замер `nav5430-sweep-before.txt`, все ширины окна).
   Нижнее поле осталось своим: под графом пустого места больше, чем под текстовыми страницами. */
body.lineage-page {
    margin: 2rem auto 2.5rem;
}

/* Колонка страницы наследования остаётся полной ширины без оконного clamp: каркас рабочей области и
   границы переноса шапки панели ввода посчитаны по этой ширине (подшаг P42.2). */
body.lineage-page .column {
    max-width: var(--page-w);
}

.workspace {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 2.5rem;
    align-items: start;
}

@media (max-width: 40rem) {
    .workspace {
        gap: 1.75rem;
    }
}

/*
 * Строка действий под панелью ввода переносится (подшаг P38.2): замер до правки показал, что в колонке
 * шириной 501 px три кнопки без переноса уезжают за её правый край на 155 px, а средняя из них сидит на
 * 7 px ниже соседних. Перенос оставляет кнопки внутри колонки, а `row-gap` не даёт строкам слипнуться.
 */
.form-row {
    display: flex;
    gap: .5rem;
    row-gap: .4rem;
    flex-wrap: wrap;
    align-items: center;
}

.form-row label {
    margin-bottom: 0;
}

.form-row select,
.form-row button {
    margin-bottom: 0;
}

/*
 * Холст графа работает на высоту окна: без скрипта у него свой скролл, поэтому страница целиком
 * не разъезжается, когда карточек много по ширине. Верхняя граница — 80% высоты окна: холст обязан
 * оставлять видимыми и статус разбора, и панель кода слева, иначе страница превращается в одну
 * картинку без подписей. Когда скрипт взял холст на себя, правило .graph-interactive ниже меняет
 * способ двигать картинку: прокрутка уходит, приходят панорама и приближение.
 */
.graph {
    border: 1px solid var(--border);
    padding: .5rem;
    overflow: auto;
    min-height: 60vh;
    max-height: 80vh;
    background: var(--panel-bg);
}

/*
 * Обёртка холста (подшаг P31.3): нужна относительная координата, чтобы панель кнопок вставала
 * ВНУТРИ области холста, а не отдельной строкой над ним. Замер прежней раскладки
 * (graph31-before.txt): строка кнопок 27 px + зазор 5 px сверху холста, и пока холст прокручивается
 * или уходит ниже сгиба, кнопки уезжают вместе с ним — посетитель девятого обращения на это и
 * жаловался. Обёртка не входит в холст: скрипт перед каждой отрисовкой расчищает содержимое холста
 * (clearNode), и кнопки внутри холста первая же перерисовка стёрла бы вместе с обработчиками.
 */
.graph-stage {
    position: relative;
}

/*
 * Кнопки холста (подшаги P23.2 и P31.3): панель висит внутри области холста в правом верхнем углу
 * и остаётся на месте, пока карточки панорамируются, — она относится к одному холсту и обязана быть
 * под руками, когда холст ушёл из поля зрения. Полупрозрачный фон и размытие под ним нужны затем,
 * чтобы панель не вырезала из карточек кусок: сквозь неё видно, что лежит под ней. Кнопки выключены,
 * пока графа нет: скрипт включает их только тогда, когда есть что приближать и переносить.
 */
.graph-controls {
    position: absolute;
    top: .55rem;
    right: .55rem;
    z-index: 2;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .3rem;
    max-width: calc(100% - 1.1rem);
    padding: .35rem .45rem;
    border: 1px solid var(--glass-border);
    border-radius: 999px;
    background: var(--glass-bg);
    box-shadow: var(--glass-shadow);
    backdrop-filter: blur(.25rem);
    -webkit-backdrop-filter: blur(.25rem);
}

/*
 * В серверном ответе без скрипта холст прокручивается своими полосами прокрутки (правило .graph
 * выше), и панель не должна ложиться на правую полосу: иначе за верхний конец полосы не за что
 * схватиться. Когда скрипт взял холст на себя, полос нет и панель встаёт в самый угол.
 */
.graph:not(.graph-interactive) ~ .graph-controls {
    right: calc(.55rem + var(--scrollbar-size));
}

.graph-controls button {
    margin-bottom: 0;
    padding: .25rem;
    min-width: 1.75rem;
    min-height: 1.75rem;
    border-radius: 2px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/*
 * Значки панели (десятое замечание посетителя): подписи заменены контурными значками, имя
 * действия осталось в aria-label и title. Значок рисуется вклеенным svg, а не картинкой в CSS:
 * страница отдаёт политику `Content-Security-Policy: default-src 'self'` (замер заголовков ответа
 * /lineage), и она запрещает картинки в data-URI — маска принималась браузером и молча рисовала
 * мусор, svg не загрузился ни в одном из девяти видов кодирования (промер svg2-31.ps1, у всех
 * loads=false). Политику безопасности ради декорации не ослабляют, поэтому значки лежат в
 * разметке, а цвет и толщину им задаёт это правило: currentColor держит их тем же цветом, что и
 * надпись рядом, а 2.2 в системе 24 — это около 1.5 экранных пикселя при 16 px, поэтому значок
 * читается как линия, а не как заливка.
 */
.graph-controls .graph-control-icon {
    width: 1rem;
    height: 1rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/*
 * Подсветка поверхности: у кнопки без подписи нет другого способа показать, что она живая.
 * Полупрозрачная серая заливка работает и над полем кода, и над холстом, и не спорит с цветом значка.
 */
.graph-controls button:not(:disabled):hover {
    background: rgba(127, 127, 127, .18);
}

.graph-controls button:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 1px;
}

.graph-controls button:disabled {
    opacity: .45;
}

/* Граница между группами панели: приближение, вид, экран. */
.graph-controls-divider {
    width: 1px;
    align-self: stretch;
    margin: .1rem .15rem;
    background: var(--glass-border);
}

/*
 * Показание приближения: колесо мыши не имеет видимого результата, и число объясняет, что оно
 * сделало. Стоит между «−» и «+» и состоит из одного числа: ширина задана в rem, а цифры —
 * табличные, чтобы панель не дёргалась при переходе со 100% на 20% и обратно.
 */
.graph-zoom-level {
    min-width: 2.75rem;
    padding: 0 .1rem;
    text-align: center;
    font-size: .8rem;
    font-variant-numeric: tabular-nums;
    color: var(--muted-fg);
}

/* Пока граф не нарисован, показания нет, и пустое место на панели было бы загадкой. */
.graph-zoom-level:empty {
    display: none;
}

/*
 * Холст, взятый скриптом на себя: его высота перестаёт зависеть от размера графа, потому что вместо
 * полос прокрутки работают панорама и приближение. Полосы прокрутки остались бы вторым способом
 * двигать картинку и стали бы спорить с перетаскиванием, поэтому переполнение скрыто. Без скрипта
 * этого класса на холсте нет, и серверный fallback продолжает прокручиваться правилом .graph выше.
 *
 * touch-action: none обязателен: без него палец на холсте прокручивает страницу вместо переноса
 * карточки, а щипок перехватывается браузером раньше, чем доходит до приближения графа.
 */
.graph.graph-interactive {
    overflow: hidden;
    height: 70vh;
    touch-action: none;
}

.graph.graph-interactive > svg {
    display: block;
    touch-action: none;
}

/*
 * Курсор объясняет, что именно двигается: карточка переносится, пустая область панорамируется.
 * Перенос и приближение сделаны мышью, а выбор с клавиатуры остался прежним — Tab по карточке и
 * Enter, — поэтому курсор ничего у клавиатуры не отнимает.
 */
.graph.graph-interactive .lineage-node {
    cursor: grab;
}

.graph.graph-interactive .lineage-node.dragging,
.graph.graph-interactive > svg.panning {
    cursor: grabbing;
}

/*
 * Во весь экран уходит обёртка холста вместе с панелью кнопок (подшаг P31.3), а не страница: нужно
 * место для графа, а не для шапки и меню, и кнопки обязаны остаться под руками в полноэкранном
 * режиме — вынесенные из холста, они бы из него исчезли. Границы высоты снимаются с холста внутри
 * обёртки, иначе он остался бы в своих 70vh внутри полноэкранного ящика. Правило для холста без
 * обёртки остаётся запасом: если обёртки в разметке нет, скрипт просит экран у самого холста.
 */
.graph-stage:fullscreen {
    background: var(--page-bg);
}

.graph-stage:fullscreen .graph.graph-interactive,
.graph.graph-interactive:fullscreen {
    height: 100vh;
    min-height: 0;
    max-height: none;
    border: none;
}

/*
 * Панель ввода: textarea остаётся полем формы и носителем текста, а номера строк и окраска лежат
 * слоем под ней. Все три столбца обязаны иметь одинаковые метрики шрифта, межстрочный интервал,
 * поля и переносы, иначе окраска поедет относительно текста — поэтому они в одном правиле.
 */
.pane-caption {
    display: flex;
    gap: 1rem;
    align-items: baseline;
    flex-wrap: wrap;
}

.pane-caption label {
    margin-bottom: .3rem;
}

/*
 * Шапка панели ввода (четырнадцатое обращение посетителя, подшаг P38.1; состав шапки пересмотрело
 * шестнадцатое обращение, подшаг P40.1): слева значок и имя «SQL Редактор», справа — «Очистить» и
 * «Загрузить». Больше в шапке ничего нет, и это не вкус, а ширина колонки: по замеру двухколоночной
 * раскладки колонка панели была 501 px (с подшага P41.2 колонка одна, и шапка стала во всю ширину
 * страницы). Прежняя вторая
 * строка (поясняющая подпись слева и счётчик позиции справа) по замеру стоила панели 21 px высоты
 * (шапка 49 px, без неё 28 px), а перенести счётчик в первую строку нельзя: с узким счётчиком
 * («Строка 1, Столбец 1», 142 px) кнопки ещё стоят у края колонки (правый край кнопок 525 px при крае
 * колонки 525 px), но стоит в тексте появиться выделению («Строка 2, Столбец 1 · Выбрано 14», 239 px),
 * как кнопки вылезают за правый край на 76 px, а с самым широким счётчиком, который умеет печатать
 * страница («Строка 99999, Столбец 99999 · Выбрано 99999», 322 px), — на 158 px. Это опыт со стилями на
 * живой странице: правилам шапки в браузере задают display:flex и разный flex-wrap, разметка при этом
 * не меняется (шаг «what removing the second row costs» отчёта `gap4001-head-before.txt`).
 * Поэтому шапка — сетка из одной строки: имя слева, кнопки справа.
 *
 * С подшага P41.2 шапка отделена от панели линейкой цветом текста, а не рамкой коробки: в бумажной
 * вёрстке это та же черта, что разделяет заголовок и набор в печатной полосе.
 */
.editor-head {
    display: grid;
    /*
     * Три ячейки одной строки: имя панели слева, выбор диалекта по центру, кнопки справа (подшаг P42.2).
     * Крайние ячейки — одинаковые свободные колонки, поэтому средняя стоит посередине строки, а не после
     * имени. `minmax(0, …)` обязателен: содержимое ячейки обязано
     * уметь сжиматься, иначе самая широкая подпись имени или самый длинный вариант диалекта вытолкнут
     * строку за край колонки (так с шапкой уже было: `gap3802-row-nowrap-attempt.txt`).
     */
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    column-gap: .75rem;
    padding: .2rem 0 .45rem;
    border-bottom: 1px solid var(--page-fg);
}

.editor-head .editor-brand {
    grid-row: 1;
    grid-column: 1;
    justify-self: start;
}

/*
 * Ячейка выбора диалекта. Подпись обязана остаться подписью поля (`label for="dialect"`), поэтому
 * переезжает вместе с селектом; поля вне `.form-row`, поэтому их нижние отступы сняты здесь, а не
 * наследуются от строки действий.
 */
.editor-head .editor-dialect {
    grid-row: 1;
    grid-column: 2;
    justify-self: center;
}

.editor-dialect {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}

.editor-dialect label,
.editor-dialect select {
    margin-bottom: 0;
}

.editor-head .editor-actions {
    grid-row: 1;
    grid-column: 3;
    justify-self: end;
}

/*
 * Центр шапки держится, только пока в колонке есть место для всех трёх ячеек одной строки. Граница
 * померена (отчёты `gap4109-arrange-p422-n0700.txt`, `-n0736.txt`, `-n0760.txt`, `-n0790.txt`):
 * при колонке 834 px (окно 860) выбор диалекта и кнопки разведены на 25 px, при 764 px (окно 790)
 * ячейка выбора наползает на кнопки уже на 10 px, при 734 px (окно 760) — на 25 px, при 674 px
 * (окно 700) — на 55 px. Арифметика у наползания простая: центрированная ячейка выбора шириной 313 px
 * оставляет справа столько же места, сколько слева, а группе кнопок справа нужно 235 px — значит
 * строке нужно 313 + 2 × 235 = 783 px колонки. Поэтому ниже 51rem выбор диалекта переходит на свою
 * строку: шапка становится выше, но ни одна ячейка ни на что не наползает и за край колонки ничего
 * не вылезает.
 */
@media (max-width: 51rem) {
    .editor-head {
        grid-template-columns: minmax(0, 1fr) auto;
        row-gap: .3rem;
    }

    .editor-head .editor-dialect {
        grid-row: 2;
        grid-column: 1 / -1;
    }

    .editor-head .editor-actions {
        grid-row: 1;
        grid-column: 2;
    }
}

.editor-brand {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}

/* Подпись поля остаётся настоящей подписью (`label for="text"`), просто стоит рядом со значком, а не
   над полем: клик по имени по-прежнему ставит каретку в поле. Имя набрано разряженно, как заголовок
   печатной полосы (подшаг P41.2). */
.editor-brand label {
    display: inline;
    margin: 0;
    font-weight: 600;
    font-size: .85rem;
    letter-spacing: .2em;
}

/*
 * Значок панели наведён тем же приёмом, что значки кнопок (шестнадцатое обращение посетителя,
 * подшаг P40.2): без заливки, контуром цветом текста — `stroke: currentColor` наследует цвет имени
 * рядом (по замеру той правки — rgb(17, 17, 17)). Прежний значок
 * был синим диском 18×18 px с белым штрихом 7×7 px внутри (отчёт `gap4001-head-p401.txt`:
 * `rgb(56, 116, 205)`), то есть различимая деталь
 * в нём занимала 7 px при диске в 18 px, и посетитель описал это как «синий, непонятно что это».
 * Знак кода крупнее: рамка 20×16 условных знаков при окне 24 px, стрелка 6 знаков высотой и
 * нижнее подчёркивание 5 знаков шириной, а цвет совпадает с цветом надписи, поэтому паре
 * переменных палитры, которые красили прежний диск, делать нечего — они из палитры убраны.
 */
.editor-brand-icon {
    width: 1.5rem;
    height: 1.5rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/*
 * Кнопок группы нет, пока скрипт не объявился: класс `scripted` навешивает на форму сам скрипт.
 * Без JavaScript «Загрузить» не открыла бы файл в поле, а «Очистить» не сняла бы карточки, —
 * кнопка, которая ничего не делает, была бы хуже отсутствующей. Поле файла лежит в этой же группе и
 * не участвует в потоке разметки: его открывает кнопка, а в отправку формы оно не попадает, потому что
 * имени у него нет.
 */
.editor-actions {
    display: none;
}

.scripted .editor-actions {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    flex: 0 0 auto;
}

/*
 * Поле файла не участвует в потоке разметки: окно выбора открывает кнопка, а в отправку формы поле не
 * попадает, потому что имени у него нет. Прятать его через `display: none` не стали: программный
 * остаётся нарисованным — но волоском и прозрачным.
 */
#editor-file {
    position: absolute;
    width: .06rem;
    height: .06rem;
    opacity: 0;
    pointer-events: none;
}

.editor-actions button {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    margin-bottom: 0;
    border-radius: 2px;
    font-size: .85rem;
    letter-spacing: .06em;
    flex: 0 0 auto;
    white-space: nowrap;
}

/*
 * Значки кнопок панели ввода — те же контурные значки, что у панели холста (P32.1): имя действия
 * написано подписью, а значок только помогает найти кнопку глазами. `currentColor` держит их тем же
 * цветом, что и подпись кнопки, fill выключен, потому что значки контурные.
 */
.editor-action-icon {
    width: 1rem;
    height: 1rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/*
 * Нижняя строка панели ввода (четырнадцатое обращение посетителя, подшаг P38.2): слева выгрузка,
 * справа — «Форматировать» и главная «Анализировать». Группа действий прижата вправо своей
 * автоотступкой, поэтому выгрузка остаётся на левом краю, а две кнопки действий —
 * на правом, и при переносе строки они уходят вправо же, а не налево. С подшага P42.2 выбор диалекта
 * стоит в шапке панели, а не здесь. С подшага P42.3 вся строка идёт сразу под панелью кода, и своей
 * верхней линейки у неё больше нет: панель уже отделена своей рамкой, а вторая такая же черта легла бы
 * в полпиксела-пикселе под неё, то есть в наборной полосе это была бы двойная линейка. Промежуток между
 * рамкой панели и кнопками держит отступ сверху.
 */
.editor-foot {
    padding-top: .55rem;
}

.form-actions {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: .5rem;
}

.editor-foot button {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    margin-bottom: 0;
    border-radius: 2px;
    font-size: .85rem;
    letter-spacing: .06em;
}

/*
 * Цвет главной кнопки взят из палитры, а не написан литералом: фон тёмный, текст светлый. Поведение
 * кнопки при этом не меняется ни на шаг: это та же submit-кнопка формы, цвет только говорит, какое
 * действие главное.
 */
.button-go {
    background: var(--go-bg);
    color: var(--go-fg);
    border-color: var(--go-border);
    font-weight: 600;
}

.button-go:not(:disabled):hover {
    background: var(--go-bg-hover);
}

.button-go:disabled {
    opacity: .55;
}

/*
 * Панель кода прокручивается одна (подшаг P31.1). До этого у поля, слоя окраски и колонки номеров
 * была своя прокрутка, а скрипт перекладывал `scrollTop` и `scrollLeft` с поля на слои. Посетитель
 * видит, что слова и номера «то расходятся, то сходятся» при прокрутке колесом по чуть-чуть назад;
 * три независимые прокрутки - единственный способ, которым эти три коробки вообще могут оказаться
 * на разных смещениях, поэтому от них отказались. Что показали промеры живым Chrome (отчёты
 * p31-check-before.txt и p31-check-after.txt, кэш браузера отключён): в прежней сборке потолок
 * прокрутки у поля 357 px, у слоя окраски и у колонок номеров по 368 px - то есть смещения три, и их
 * сводит к одному скрипт; в новой сборке потолок 357 px один у панели, а у поля, слоя и колонок
 * прокрутки нет вовсе (0 во всех кадрах, отчёт p31-lag-after.txt). Две другие версии причины не
 * подтвердились и оставлены как не доказанные: задержки зеркалирования при кадрах прокрутки не
 * измерено (p31-lag-before.txt: расхождения поля со слоем и с колонкой во всех кадрах 0), а
 * растровая теория про масштаб экрана 125% на тонком А/Б промере не выстояла: в прежней и в новой
 * сборке границы строк в колонке номеров и в словах расходятся не больше чем на 1 экранный пиксел, и
 * величина та же на всех смещениях прокрутки (p31-reg-before.txt и p31-reg-after.txt) - раньше её
 * приняли за «биение», но это форма цифр, а не рассогласование.
 */
.code-pane {
    overflow: auto;
    /*
     * Размерная инкапсуляция (подшаг P31.1): панель прокручивает длинную строку сама, и её содержимое
     * не должно сообщать странице ширину самой длинной строки. Замер живым Chrome в окне 640 px без
     * этого правила: панель стала 1094 px шириной, страница - 1118 px, горизонтальная прокрутка внутри
     * панели пропала (потолок 0 px), то есть длинная строка двигала всю страницу вместе с колонкой
     * номеров; в прежней сборке поле прокручивало строку на 428 px, а страница оставалась 681 px.
     */
    contain: size;
    height: 24rem;
    border: 1px solid var(--border);
    background: var(--code-bg);
}

.text-pane {
    /* Область одна: высота выше прежней панели SQL, потому что в неё встают и запросы, и DDL,
       и тела процедур; холст графа при этом остаётся на своей высоте окна. */
    height: 32rem;
}

/*
 * Внутри панели — сетка: колонка номеров и стопка «слой окраски + поле». Ширина по содержимому
 * держит горизонтальную прокрутку: перенос длинных строк выключен, поэтому ширину задаёт самая
 * длинная строка. Правила min-* не дают сетке схлопнуться на коротком тексте, чтобы клик в пустой
 * области панели по-прежнему попадал в поле.
 */
.code-grid {
    display: grid;
    grid-template-columns: 3.2rem auto;
    width: max-content;
    min-width: 100%;
    min-height: 100%;
}

/*
 * Базовой ширины колонки номеров хватает на четыре знака: 3.2rem, из них один rem уходит на
 * отступы, оставшиеся 2.2rem вмещают четыре моноширинных цифры (примерно по 0.51rem). Пять цифр
 * уже не влезают, и с десяти тысяч строк скрипт добавляет класс, расширяющий колонку. Меняются обе
 * колонки сетки сразу: поле, слой окраски и колонка номеров лежат в одной сетке, и расхождение
 * сдвинуло бы окраску относительно текста.
 */
.code-pane.gutter-wide .code-grid {
    grid-template-columns: 4.2rem auto;
}

/*
 * Правило первого абзаца объяснений под панелью снято (подшаг P42.3): строка действий встала между
 * панелью и объяснениями, поэтому абзаца, который шёл бы сразу за панелью, в разметке не осталось,
 * а висящее правило без элемента только притворяется, что о чём-то заботится (тот же приём, что с
 * правилами убранной второй строки шапки, подшаг P40.1). Промежуток от строки действий до первого
 * абзаца держат собственные отступы абзацев: замер после правки — 13 px, столько же, сколько между
 * самими абзацами (отчёт `gap4109-arrange-p423-f1440.txt`). С подшага P53.9 объяснительных абзацев
 * под панелью нет вовсе: их сняли по указу посетителя от 4 ноября 2026 года, и строка действий стала
 * последним блоком панели.
 */

.code-gutter,
.code-view,
.code-pane textarea {
    margin: 0;
    padding: .4rem .5rem;
    border: 0;
    font-family: var(--mono);
    font-size: .85rem;
    line-height: 1.4;
    letter-spacing: normal;
    tab-size: 4;
    white-space: pre;
    word-wrap: normal;
}

.code-gutter {
    grid-column: 1;
    grid-row: 1;
    /*
     * Колонка номеров прижата к левому краю панели (подшаг P31.1): длинную строку прокручивают,
     * а номера остаются на месте — так же, как когда-то отдельной ячейкой сетки. Липкость работает
     * внутри прокрутки панели, поэтому своей прокрутки у колонки больше нет: reserve под полосы
     * поля (подшаг P29.4) стал не нужен — полоса теперь одна на всю панель, и она не перекрывает
     * содержимое. Скрипт больше не перекладывает `scrollTop`: сдвиг у всех слоёв один.
     */
    position: sticky;
    left: 0;
    z-index: 1;
    padding-right: .5rem;
    text-align: right;
    color: var(--line-number-fg);
    background: var(--panel-bg);
    border-right: 1px solid var(--border);
    user-select: none;
}

/*
 * Стопка «слой окраски + поле» задаёт размер текстом: содержимое слоя — те же символы, что в поле,
 * поэтому ячейка вырастает ровно до текста, а поле лежит поверх ячейки и своего размера не имеет,
 * то есть прокрутиться отдельно не может.
 */
.code-stack {
    grid-column: 2;
    grid-row: 1;
    position: relative;
}

.code-view {
    pointer-events: none;
    color: var(--code-fg);
}

.code-pane textarea {
    /*
     * Коробка поля обязана совпадать с ячейкой стопки (подшаг P30.2). По умолчанию высота поля
     * считается только по содержимому, поэтому `height: 100%` делало коробку на толщину отступов
     * больше ячейки. Замер живым Chrome до правки: панель 514 px вместе с рамкой, поле 525 px, его
     * нижний край на 12 px ниже рамки панели, а приписка под панелью начинается в 5 px от этого края;
     * горизонтальная полоса прокрутки поля (10 px) и нижний конец вертикальной рисовались по тексту
     * приписки и свешивались за рамку. С подшага P31.1 поле стоит в ячейке по границам (`inset: 0`)
     * и само не прокручивается: полосой владеет панель, а `border-box` держит отступы внутри коробки,
     * чтобы глифы поля совпали с глифами слоя окраски.
     */
    box-sizing: border-box;
    position: absolute;
    inset: 0;
    resize: none;
    overflow: hidden;
    /* Фон носит сама панель, а не поле: слой окраски и колонка номеров лежат в тех же ячейках,
       и пока введённый текст не совпал с разобранным, глифы рисует поле — фон у него прозрачный, чтобы
       панель не отличалась цветом ни в одном из двух состояний. */
    background: transparent;
    color: var(--code-fg);
}

/*
 * Пока панель не показывает разобранный текст, глифы рисует само поле: слой под ним пуст. Когда
 * текст совпал с разобранным, поле перестаёт рисовать свой текст, а виден слой с окраской.
 */
.code-pane.is-analyzed textarea {
    color: transparent;
    caret-color: var(--code-fg);
}

/*
 * Поле панели не рисует рамку фокуса (подшаг P29.2). Приём подшага P27.2 здесь не работает: прогон
 * настоящего Chrome по DevTools-протоколу даёт после клика мышью и `:focus`, и `:focus-visible`, —
 * для текстового поля браузер считает фокус видимым всегда, поэтому правило
 * `:focus:not(:focus-visible)` отменило бы только клавиатурный проход, а рамку от клика оставило бы.
 * Фокус в текстовом поле виден и без рамки: в нём стоит мигающий курсор (`caret-color` задан слоем
 * окраски). У карточек графа (P27.2) другого знака фокуса не было, поэтому там рамка осталась
 * намеренно.
 */
.code-pane textarea:focus {
    outline: none;
}

/*
 * Карточка узла: тело (class="card"), разделитель под шапкой, строки колонок и порты. Тело и строки
 * лежат в одной группе SVG, поэтому правило тела ищет именно card: селектор по rect обвёл бы рамкой
 * выделения и каждую строку колонок.
 */
.lineage-node .card {
    fill: var(--node-fill);
    stroke: var(--node-stroke);
    stroke-width: 1.5;
}

.lineage-node .card-divider {
    stroke: var(--node-stroke);
    stroke-width: 1;
}

/*
 * Подписи карточки набраны моноширинным стеком палитры (подшаг P41.3, макет 07): имена таблиц и
 * колонок — это код, и в бумажной вёрстке они стоят тем же набором, что поле запроса и справочник.
 * Стек взят переменной палитры, а не выписан заново, потому что ширину карточек считает скрипт:
 * `lineage.js` читает `--mono` из стилей и меряет подписи тем же стеком. Если бы стеки разошлись,
 * карточка получала бы ширину по одному шрифту, а подпись рисовалась бы другим — ровно тот дефект,
 * из-за которого подписи обрезались на середине слова (подшаг P34.2).
 */
.lineage-node .node-title {
    font-weight: 600;
    font-size: .95rem;
    font-family: var(--mono);
    fill: var(--page-fg);
}

.lineage-node .node-subtitle {
    fill: var(--node-subtitle);
    font-size: .8rem;
    font-family: var(--mono);
}

.lineage-node.layer-RAW .card {
    fill: var(--layer-raw);
}

.lineage-node.layer-STAGING .card {
    fill: var(--layer-staging);
}

.lineage-node.layer-TRANSFORM .card,
.lineage-node.layer-MART .card {
    fill: var(--layer-mart);
}

.lineage-node.layer-TEMPORARY .card,
.lineage-node.layer-QUERY .card {
    fill: var(--layer-temp);
}

.lineage-node {
    cursor: pointer;
}

.lineage-node.selected .card {
    stroke: var(--selected-stroke);
    stroke-width: 2.5;
}

/*
 * Карточка узла, через который проходит выбранная цепочка колонок (подшаг P25.2). Правило стоит
 * после .selected: узел, по строке колонки которого кликнули, одновременно и выбран, и лежит на
 * цепочке, и зелёная обводка обязана перекрывать цвет выделения, иначе «путь» терялся бы на
 * собственной карточке.
 */
.lineage-node.chain-node .card {
    stroke: var(--chain-stroke);
    stroke-width: 2.2;
}

/*
 * Карточка узла принимает фокус с клавиатуры: по ней можно пройти Tab'ом, Enter и Space выбирают узел
 * (обработчики keydown висят на самой группе). Штатная обводка фокуса рисовала вокруг карточки вторую
 * рамку — поверх собственной обводки выбранного узла, и посетитель принял её за лишний контур графа
 * (подшаг P27.2): «при нажатии на таблицу появляется ещё одна обводка». Рамка снимается только для
 * фокуса, полученного мышью: прогон настоящего Chrome по DevTools-протоколу даёт после клика `:focus`
 * без `:focus-visible`, а после Tab — оба селектора. Поэтому щелчок рисует только сам выбор узла, а
 * клавиатурная навигация остаётся видимой — снимать её было бы хуже, чем лишняя рамка.
 */
.lineage-node:focus:not(:focus-visible) {
    outline: none;
}

.lineage-edge {
    fill: none;
    stroke: var(--edge-stroke);
    stroke-width: 1.9;
    cursor: pointer;
}

.arrow-head {
    fill: var(--arrow-fill);
}

/*
 * Выбранное ребро меняет цвет, а не только толщину: в плотном графе «жирнее тем же цветом» читается
 * плохо, поэтому выделенный путь получает отдельный оттенок палитры. Стрелка выбирается другим
    маркером: содержимое marker наследует стили от своего места в defs, а не от пути, и окрасить её
    тем же цветом, что и линия, через селектор пути нельзя.
 */
.lineage-edge.selected {
    stroke: var(--selected-stroke);
    stroke-width: 2.8;
    marker-end: url(#lineage-arrow-selected);
}

.arrow-head-selected {
    fill: var(--selected-stroke);
}

/*
 * Цепочка колонок (подшаг P25.2): клик по строке колонки подсвечивает зелёным не только связи
 * выбранного узла, а весь путь значения — вверх к источникам и вниз к конечной точке. Цвет свой,
 * отдельный от цвета выделенной связи: «одна связь» и «путь из нескольких связей» обязаны
 * различаться, а не только толщиной. Маркер стрелки свой по той же причине, что и у выделения:
 * содержимое marker наследует стили от своего места в defs, а не от пути, который на него ссылается.
 * Штрих у связи цепочка не снимает: он по-прежнему про отсутствие порта.
 */
.lineage-edge.chain {
    stroke: var(--chain-stroke);
    stroke-width: 2.6;
    marker-end: url(#lineage-arrow-chain);
}

.arrow-head-chain {
    fill: var(--chain-stroke);
}

/*
 * Строка колонки в карточке и её порты. Порт появляется только там, где анализатор принёс имя
 * колонки из разбора, поэтому связь без порта выделена штрихом: подогнать геометрию было бы
 * правдой про карточку и неправдой про связь.
 */
.lineage-node .node-column {
    fill: var(--page-fg);
    font-size: .84rem;
    font-family: var(--mono);
    pointer-events: none;
}

.lineage-node .node-more {
    fill: var(--muted-fg);
    font-size: .78rem;
    font-family: var(--mono);
    pointer-events: none;
}

/*
 * Пометка переименования в строке колонки (подшаг P37.4). Она пишется правее имени и другим
 * размером, чтобы имя колонки оставалось подписью строки: по нему страница ищет строку, порт
 * связи и отметку цепочки, а пометка — только объяснение, откуда пришло это имя.
 */
.lineage-node .node-column-note {
    fill: var(--muted-fg);
    font-size: .72rem;
    font-family: var(--mono);
    pointer-events: none;
}

.column-row {
    fill: transparent;
    stroke: none;
    cursor: pointer;
}

.column-row:hover {
    fill: var(--row-hover);
}

/*
 * Строка колонки, через которую проходит выбранная цепочка (подшаг P25.2). Правило стоит после
 * :hover: отметка цепочки важнее мгновенной подсветки курсора, иначе при наведении курсора колонка,
 * по которой идёт путь, теряла бы свою отметку.
 */
.column-row.chain-row {
    fill: var(--chain-row);
}

/*
 * Строка колонки, у которой нет ни одной колоночной связи (подшаг P26.2): цепочка из одного поля.
 * Отметка намеренно не зелёная — зелёный в легенде означает путь значения, а здесь пути нет;
 * серый тон тот же, что у подсветки курсора, чтобы отметка читалась как «вот это поле кликнули».
 */
.column-row.solo-row {
    fill: var(--row-hover);
}

/*
 * Порт строки колонки (подшаг P24.1 — левая и правая грань карточки). Кружок заметен на карточке
 * любого слоя: у него свой цвет заливки и обводка толще линии ребра, поэтому конец колоночной связи
 * видно независимо от оттенка слоя.
 *
 * Кликов порт не принимает (подшаг P26.5). Он лежит ровно на середине грани своей строки колонки —
 * там, куда приходит линия связи и куда естественно навести курсор. Пока порт принимал клики, такой
 * клик доходил до карточки и выбирал узел целиком: цепочка колонок не подсвечивалась, хотя связи у
 * поля есть. Отдельного действия у порта нет — выбрать саму связь можно, кликнув по её линии, а
 * дорожка колонки кликов не принимает по той же причине.
 */
.port {
    fill: var(--port-fill);
    stroke: var(--port-stroke);
    stroke-width: 1.6;
    pointer-events: none;
}

/*
 * Дорожка колонки: горизонтальная линия от левого порта к правому через всю карточку ровно по середине
 * её строки. Она фон, а не содержимое: opacity держится низкого, чтобы имя колонки читалось поверх
 * линии, и линия не принимает клики, иначе она спорила бы со строкой колонки за клик. Смысл один —
 * показать, что порт на левой грани, порт на правой и строка колонки лежат на одной горизонтали.
 */
.column-lane {
    stroke: var(--port-stroke);
    stroke-width: 1;
    stroke-dasharray: 2 3;
    opacity: .35;
    pointer-events: none;
}

/*
 * Штрих у колоночной связи, у которой одной из сторон нет порта (подшаг P25.1 — конец такой связи
 * лежит на середине тела карточки, ниже шапки). Штрих означает границу разбора, а не «другой характер
 * связи»: связь из AST есть, а колонки, к которой её можно подвести, в карточке нет. Табличная связь
 * штрихом не помечается никогда — у неё порта нет по смыслу, она приходит в шапку карточки.
 */
.lineage-edge.without-port {
    stroke-dasharray: 5 4;
}

/*
 * Метка происхождения связи (подшаг P16.5): короткий блок, внутри которого написан оператор, давший
 * связь. Связь из-под IF, цикла или тела процедуры выполняется не всегда — и это обязано быть видно на
 * самой линии, а не только в таблице. Текст обведён цветом карточки, чтобы читаться поверх линий графа,
 * и не принимает клики: метка не должна мешать выбирать саму связь.
 */
.edge-badge {
    fill: var(--edge-stroke);
    stroke: var(--node-fill);
    stroke-width: 3px;
    paint-order: stroke fill;
    font-size: 10px;
    pointer-events: none;
}

.dimmed {
    opacity: .35;
}

.legend {
    margin: .5rem 0;
}

.legend-item {
    margin-right: 1rem;
}

.swatch {
    display: inline-block;
    width: .75rem;
    height: .75rem;
    border: 1px solid var(--node-stroke);
    margin-right: .25rem;
}

/*
 * Тот же знак происхождения, что рисуется на линии графа, но в легенде: здесь обычный HTML, поэтому
 * рамка и цвет текста вместо обводки.
 */
.legend-badge {
    display: inline-block;
    border: 1px solid var(--badge-border);
    border-radius: .2rem;
    padding: 0 .25rem;
    color: var(--muted-fg);
    font-size: .8rem;
    margin-right: .25rem;
}

.swatch-port {
    border-radius: 50%;
    background: var(--port-fill);
    border-color: var(--port-stroke);
}

/*
 * Дорожка колонки в легенде (подшаг P24.1): та же штриховая горизонталь, что и на карточке, поэтому
 * квадратик превращается в узкую черту — образец обязан напоминать то, что нарисовано на графе.
 */
.swatch-lane {
    width: .9rem;
    height: .2rem;
    border: none;
    border-top: 2px dashed var(--port-stroke);
    background: transparent;
    vertical-align: middle;
}

/*
 * Связь объектов в легенде (подшаг P25.1): сплошная черта тем же цветом, что и линии графа. Квадратик
 * здесь означал бы карточку, а легенда говорит про линию, поэтому образец — узкая сплошная черта.
 */
.swatch-object {
    width: .9rem;
    height: .2rem;
    border: none;
    border-top: 2px solid var(--edge-stroke);
    background: transparent;
    vertical-align: middle;
}

.swatch-no-port {
    border-style: dashed;
    background: transparent;
}

/*
 * Цепочка колонок в легенде (подшаг P25.2): зелёная сплошная черта тем же цветом, что и подсветка
 * на графе, а не ещё один квадратик — легенда говорит про линию.
 */
.swatch-chain {
    width: .9rem;
    height: .2rem;
    border: none;
    border-top: 2px solid var(--chain-stroke);
    background: transparent;
    vertical-align: middle;
}

/*
 * Цвета лексем идут после подсветки интервалов графа: один фрагмент текста может быть и лексемой, и
 * интервалом связи. Фон интервала остаётся общим для всей связи, а цвет буквы берётся у лексемы —
 * так связь читается фоном, а состав текста цветом, и ни одно из двух не теряется.
 */
.tok-comment {
    color: var(--tok-comment);
    font-style: italic;
}

.tok-string {
    color: var(--tok-string);
}

.tok-number {
    color: var(--tok-number);
}

/*
 * Слово из перечня зарезервированных слов выбранного диалекта. Оттенок нарочно не совпадает с цветом
 * квотированного имени: «слово» и «имя» — разные факты, и на панели их нужно различать глазами, иначе
 * [user] и user сливаются в одно.
 */
.tok-keyword {
    color: var(--tok-keyword);
}

/*
 * Слово из перечня незарезервированных ключевых слов диалекта: 271 слово у T-SQL, 393 у PostgreSQL.
 * Оттенок светлее основного ключевого слова нарочно в той же семье — различие «зарезервировано /
 * может быть именем» должно читаться как степень, а не как другой факт. Шум у этого цвета известный:
 * в PostgreSQL {@code name}, {@code int} и {@code label} тоже стоят в приложении C, поэтому
 * {@code SELECT name FROM t} окрасит {@code name}, хотя разбор прочитал его именем колонки. Часть
 * составного имени цвета не получает: {@code c.name} остаётся именем.
 */
.tok-keyword-non-reserved {
    color: var(--tok-keyword-non-reserved);
}

/*
 * Имя из перечня функций диалекта, за которым следующей лексемой идёт «(»: 242 имени со страниц
 * Microsoft и 647 имён разделов 9.2—9.31 postgresql.org (подшаг P37.9). Без скобки цвет не появляется,
 * иначе цветом оказались бы имена колонок — у T-SQL 34 имени из перечня функций стоят и в
 * незарезервированных словах, у PostgreSQL 37. Два имени стоят и в перечне зарезервированных слов (LEFT
 * и RIGHT), и для такого слова скобка — единственный различимый признак: без неё то же слово рисуется
 * ключевым.
 */
.tok-function {
    color: var(--tok-function);
}

.tok-quoted {
    color: var(--tok-quoted);
}

.tok-variable {
    color: var(--tok-variable);
}

.tok-name {
    color: var(--tok-name);
}

.tok-operator {
    color: var(--tok-operator);
}

.tok-punctuation {
    color: var(--tok-punctuation);
}

.tok-unknown {
    color: var(--tok-unknown);
    text-decoration: underline dotted var(--tok-unknown);
}

.gap-error {
    color: var(--gap-error);
}

.gap-unknown {
    color: var(--gap-unknown);
}
