/* СГЕНЕРИРОВАНО — НЕ ПРАВИТЬ
   
   Источник: репозиторий TN_Services_Design, tokens/
   Версия визуального языка: 0.87
   Хеш содержимого: 6191d271e126
   
   Правки вносятся в источнике и раздаются:
       python3 tools/emit_tokens.py emit
   Правка этого файла будет затёрта, а до тех пор уронит CI
   (шаг `emit_tokens.py check`). */

/* --- tokens/tokens.css --- */
/* ====================================================================
   TN Services — токены визуального языка
   Версия 0.87 · 20.08.2026

   ЕДИНСТВЕННЫЙ ИСТОЧНИК ЗНАЧЕНИЙ. Правила применения — DESIGN.md.
   Значение, которого нет здесь, в продукте появляться не должно:
   хардкод цвета или отступа ломает тёмную тему и режим повышенного
   контраста, и ломает молча.

   Правило добавления цвета: цвет без определения в одну строку
   в язык не добавляется. Если определение не формулируется —
   нужен не цвет, а другой способ передать смысл.
   ==================================================================== */

:root {

  /* --- ИНТЕРВАЛЫ -----------------------------------------------------
     Атом 4, основной шаг 8. Промежуточных значений нет намеренно:
     свобода в отступах — главный источник ощущения случайности.     */
  --s1: 4px;    /* внутри ячейки, между значением и подписью */
  --s2: 8px;    /* между связанными элементами */
  --s3: 12px;   /* внутренний отступ плотной строки */
  --s4: 16px;   /* внутренний отступ блока */
  --s5: 20px;   /* поле секции */
  --s6: 24px;   /* отступ вложенности */
  --s8: 32px;   /* между смысловыми группами */

  /* --- РАДИУСЫ -------------------------------------------------------
     Скругление означает «управляемый элемент». Данные не скругляются. */
  --r-sm: 4px;  /* контролы, чипы, метки */
  --r-md: 6px;  /* кнопки, сегментированный контрол */
  --r-lg: 8px;  /* контейнер, если он вообще нужен */

  /* --- КЕГЛИ ---------------------------------------------------------
     Роль, а не размер. Обращаться по роли: «подпись колонки»,
     а не «одиннадцатый».                                            */
  --f-micro:   11px;  /* подписи колонок, сноски под значением */
  --f-small:   12px;  /* второстепенный текст, обвязка */
  --f-body:    13px;  /* данные в таблицах и списках */
  --f-ui:      14px;  /* контролы, основной текст интерфейса */
  --f-lead:    20px;  /* вторичное крупное значение */
  --f-display: 30px;  /* главный итог экрана */

  --lh-tight: 1.15;   /* крупные значения */
  --lh-base:  1.4;    /* интерфейс */
  --lh-text:  1.55;   /* абзацы объяснений */

  /* --- ШРИФТ ---------------------------------------------------------
     Только системный стек. Внешние шрифты невозможны: политика
     безопасности FinanceWatcher — default-src 'none' без font-src.
     Совпадает с тем, что будет в SwiftUI, и это удача, а не потеря. */
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
          Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  /* --- НЕЙТРАЛЬНАЯ ШКАЛА ---------------------------------------------
     Основа интерфейса. Всё, что не является состоянием данных,
     берётся отсюда.                                                 */
  --n0: #ffffff;   /* рабочая поверхность */
  --n1: #fafaf9;   /* подложка: шапка таблицы, боковая колонка */
  --n2: #f2f2f0;   /* фон приложения, заголовок группы */
  --n3: #e7e7e4;   /* линия-разделитель */
  --n4: #d4d4d0;   /* граница контрола, сильная линия */
  --n6: #6c6c68;   /* текст третьего уровня: подписи, единицы */
  --n7: #5c5c58;   /* текст второго уровня: пояснения */
  --n9: #1b1b1a;   /* основной текст и значения */

  /* --- АКЦЕНТ --------------------------------------------------------
     Ровно один. Означает «здесь ты сейчас» или «это изменено».
     Данные акцентом не красятся никогда.                            */
  --accent:      #2f5bd0;
  --accent-soft: #e9eefb;

  /* --- БРЕНД ---------------------------------------------------------
     ЦВЕТ ТОЖДЕСТВА, А НЕ СМЫСЛА (D-079, D-085). Отвечает на вопрос
     «чьё это», а не «что это значит». Продукт вправе переопределить
     обе величины; язык задаёт их равными акценту, чтобы продукт
     без бренда был верен без единой строки.

     --brand     — ТОЛЬКО поверхность: background, border, fill.
                   Как `color:` запрещён.
     --brand-ink — ТОЛЬКО текст, и только на фоне --brand.

     Пара обязана давать 4.5 между собой: tools/brand_check.py.
     Одним токеном это не решается — брендовый цвет и цвет, которым
     можно набирать текст, разные величины. STRU_magic показал числами:
     их жёлтый даёт 1.05 на белом и 13.09 на тёмном.                */
  --brand:     var(--accent);
  --brand-ink: var(--n0);

  /* НАБОР, А НЕ ПАРА (D-149). Продукт отвечает «чьё это» больше чем
     в одном месте, и роли у этих мест разные. Заявка-19 STRU_magic
     просила второй брендовый цвет и осталась без ответа; заявка-31
     назвала форму верно — нужен набор с ролями.

     Роль каждого формулируется в одну строку, иначе цвета не будет
     (принцип 3). Ограничение D-085 остаётся целиком: ни один брендовый
     токен не кодирует состояние данных, отклонение и положение.

     --brand-action — поверхность ГЛАВНОГО ДЕЙСТВИЯ: «нажми здесь».
                      Не «здесь ты сейчас» — это --accent, и он не бренд.
     --brand-mark   — поверхность ПОМЕТКИ: чем продукт помечает выбранное.
                      Положение обязано кодироваться ещё чем-то, кроме
                      цвета (§2а), и это ограничение здесь не ослаблено.


     ЗНАЧЕНИЕ ПО УМОЛЧАНИЮ — ТО, ЧЕМ ЯЗЫК КРАСИТ ЭТО МЕСТО СЕЙЧАС,
     а не `--brand` (D-160). Роль обязана читаться компонентом языка,
     иначе правило пишет каждый продукт сам — и это своя копия
     компонента, ровно то, от чего роль должна была избавить
     (находка STRU_magic 32). Но подставить туда `--brand` значило бы
     перекрасить главную кнопку у всех, кто бренда не заводил.
     Поэтому умолчание равно нынешнему цвету места, и продукт
     без бренда не замечает появления роли вовсе. */
  --brand-action:     var(--n9);
  --brand-action-ink: var(--n0);
  --brand-mark:       var(--accent-soft);
  --brand-mark-ink:   var(--accent);

  /* --- СОСТОЯНИЯ ДАННЫХ ----------------------------------------------
     Четыре, больше нет. Каждое — про доверие к числу, а не про
     хорошо/плохо. Определения печатаются на экране рядом с данными. */

  /* значение подтверждено сверкой на выбранную дату */
  --st-ok:            #2c7549;
  --st-ok-soft:       #ecf4ef;

  /* значение подтверждено, но давно. ПОРОГ ЗАДАЁТ ПРОДУКТ и называет
     его на экране: у сверки остатка и у биржевой цены он разный (D-023) */
  --st-old:           #8f6410;
  --st-old-soft:      #fcf7ed;

  /* данных нет; значение не показывается и нулём не считается */
  --st-none:          #6c6c68;
  --st-none-soft:     #f7f7f5;

  /* значение есть, но применить нельзя: нет курса, нет цены */
  --st-unusable:      #a52a24;
  --st-unusable-soft: #fbecea;

  /* НАВЕДЕНИЕ НА ПОДЛОЖКЕ — ШАГ, А НЕ ОТДЕЛЬНЫЙ СМЫСЛ (D-150).
     Подложка на шаг ближе к своим чернилам. Мера шага — ΔL* ≈ 2,
     и это НЕ ослабление порога 3.0 из theme_check: тот отвечает
     на вопрос «различимы ли две категории РЯДОМ», а наведение
     различается ВО ВРЕМЕНИ, на одном и том же элементе. Глаз
     ловит временную разницу заметно меньшую, чем пространственную.

     Чернила на подложке наведения обязаны остаться выше 4.5.
     Ради этого --st-old-soft и --st-none-soft стали светлее:
     они стояли впритык к порогу, и шага под ними не помещалось.
     Подложке, по которой водят мышью, нужен запас. */
  --st-ok-soft-hover:       #e4efe8;
  --st-old-soft-hover:      #f8f1e4;
  --st-none-soft-hover:     #f1f1ef;
  --st-unusable-soft-hover: #f8e5e3;
  --accent-soft-hover:      #e2e8f9;

  /* --- ПРИМЕЧАНИЕ ЧИТАТЕЛЮ -------------------------------------------
     Определение в одну строку: пояснение, обращённое к читателю,
     которое НЕ является состоянием данных и не требует действия.

     Отдельный цвет нужен именно потому, что состояние данных и
     примечание — разные вещи, и красить второе первым значило бы
     дать одному цвету два смысла (§10). Заявка-31 STRU_magic
     сформулировала это точнее, чем было записано у нас.

     Фиолетовый, а не синий: синий занят акцентом, и «примечание»
     не должно читаться как «здесь ты сейчас». */
  --info:            #5b3f9c;
  --info-soft:       #f0ecf9;
  --info-soft-hover: #eae5f5;

  /* --- ДОСТОВЕРНОСТЬ НА ГРАФИКАХ -------------------------------------
     Кодируется НЕ цветом, а заливкой и штрихом (IBCS / ISO 24896):
     сплошное — факт, штриховка — оценка, пунктир — план.
     Цвет остаётся свободным для смысла величины.                    */
  /* --- ЗНАКИ -------------------------------------------------------
     Один вес штриха на весь набор: разнобой в толщине читается как
     разное качество рисунка, а не как разные знаки (OPEN §5). */
  --ico-stroke: 1.5;
  /* Для знаков из двух и более штрихов: оптическая компенсация массы,
     а не производная от --ico-stroke (D-195). */
  --ico-stroke-dense: 1.25;

  /* --- ЗАЛИВКА ВЕЛИЧИНЫ БЕЗ ОТКЛОНЕНИЯ -------------------------------
     Полоса таймлайна красилась `--n4` — тоном ГРАНИЦЫ КОНТРОЛА,
     и давала 1.49 на строке. Полоса и есть диаграмма: её положение
     и длина — данные, а §9 требует 3.0 для значимой графики.

     Одно значение делало две разные работы: волосяную линию, которая
     обязана быть тихой, и заливку, которая обязана быть видимой.
     Развести пришлось потому, что затемнить `--n4` значило бы
     утяжелить рамку у каждого контрола (D-184).

     Цвет остаётся НЕЙТРАЛЬНЫМ: по D-043 заливка полосы кодирует
     отклонение, и оттенок закреплён за ним. Это заливка величины,
     у которой отклонения нет. */
  --fill-plain: #8c8c88;

  /* Заливка ОБЪЕДИНЕНИЯ, а не работы: раздел собирает задачи, сам
     работой не являясь. Темнее заливки задачи на 3.23 — различимы
     между собой, а не только каждая с фоном (D-193). */
  --fill-group: #3d3d3a;

  --dash-estimate: 3 2;   /* stroke-dasharray для оценки */
  --dash-plan:     1 3;   /* stroke-dasharray для плана */

  /* --- ПАЛИТРА ДАННЫХ ------------------------------------------------
     Для разрезов, категорий и компонент прироста. Не состояния:
     эти цвета ничего не означают сами по себе, только различают.
     Зелёный и красный сюда НЕ входят — они заняты отклонениями.     */
  --d1: #2f5bd0;  --d2: #0f7a86;  --d3: #7a4fbf;
  --d4: #b06a1f;  --d5: #3f7a3f;  --d6: #96336b;


  /* --- ТЕНИ ----------------------------------------------------------
     Тень означает ОДНО: элемент всплыл над плоскостью. Модал, поповер,
     перетаскиваемая карточка. Секции содержимого теней не имеют — они
     разделяются линиями (D-003), иначе экран снова распадается на
     острова.                                                         */
  /* --- ПОВЕРХНОСТЬ ВСПЛЫВШЕГО ---------------------------------------
     §6 говорит: тень означает ровно одно — элемент всплыл над
     плоскостью. В СВЕТЛОЙ теме это делает тень: белая поверхность
     на сером фоне страницы уже отделена, тень лишь подтверждает.

     В ТЁМНОЙ теме тень не работает и работать не может: она темнее
     фона, а фон уже тёмный. Календарь поверх модала давал контраст
     1.0 — поверхность всплывшего равнялась поверхности того, над чем
     она всплыла (замер STRU_magic, отчёт-39).

     Поэтому глубина в тёмной теме кодируется ПОВЕРХНОСТЬЮ, а не тенью,
     и это не обходной приём: так устроены все тёмные интерфейсы,
     потому что затемнять темнее нечего. Величина одна и переключается
     темой — второго правила заводить не нужно (D-179). */
  --surface-raised: var(--n0);
  /* ТРЕТИЙ УРОВЕНЬ: всплывшее НАД ВСПЛЫВШИМ — календарь внутри модала.
     Имя по роли, а не по номеру: `-2` пригласил бы четвёртый, и
     останавливаться было бы негде (довод STRU_magic, отчёт-40).
     Четвёртого нет по той же причине, по какой нет четвёртого
     приоритета кнопки: всплывающее внутри всплывающего внутри модала —
     ошибка планировки, а не глубина (D-182).

     В СВЕТЛОЙ теме равен предыдущему намеренно: над белым светлее
     нечего, и там работу делает тень — она на белом видна. Глубина
     кодируется поверхностью только там, где тень бессильна. */
  --surface-over: var(--surface-raised);

  --sh-raised:  0 1px 2px rgba(16,16,22,.06);   /* поднято на волос: карточка под курсором */
  --sh-popover: 0 4px 12px rgba(16,16,22,.12);  /* всплывающее: меню, подсказка */
  --sh-modal:   0 8px 30px rgba(16,16,22,.22);  /* поверх всего: модал */

  /* --- ДВИЖЕНИЕ ------------------------------------------------------
     Три длительности, больше не нужно. Движение подтверждает действие
     пользователя, а не развлекает: всё, что дольше --dur-slow,
     воспринимается как тормоза.                                      */
  --dur-fast: 120ms;  /* отклик контрола: нажатие, галочка */
  --dur-base: 160ms;  /* появление и скрытие */
  --dur-slow: 220ms;  /* въезд крупной поверхности: модал, панель */
  --ease-out:    cubic-bezier(.23, 1, .32, 1);
  --ease-in-out: cubic-bezier(.77, 0, .175, 1);

  /* --- СЛОИ ----------------------------------------------------------
     Числа z-index берутся только отсюда. Стихийные «z-index: 9999»
     разъезжаются между экранами и потом не чинятся.                  */
  --z-sticky:  2;   /* залипшая шапка или колонка таблицы */
  --z-header:  10;  /* шапка страницы поверх содержимого */
  --z-popover: 30;  /* меню, подсказка */
  --z-modal:   50;  /* подложка и окно */

  /* --- ЛИНИИ В SVG ---------------------------------------------------
     На диаграмме половина решений оказывается на элементах, у которых
     нет background и border, а есть fill и stroke. Цвет берётся из тех
     же токенов; толщина — отсюда.

     Волосяная линия в SVG не равна 1 px в CSS: при дробном
     devicePixelRatio она размывается, поэтому для сетки задан свой
     токен, а не «единица».                                          */
  --line-w:        1px;    /* сетка, разделители на диаграмме */
  --line-w-strong: 2px;    /* связь между задачами, ось, граница периода */
  /* ТОРЦЫ СВЯЗИ. Размеры — величины языка: они одинаковы всегда
     и читаются как знак. Сама геометрия (координаты уса) считается
     продуктом из данных, поэтому здесь только длины и угол (D-128).

     Ус короче полосы задачи по построению: 7 px против 20 px
     минимальной полосы — наконечник не должен спорить с тем,
     на что указывает. */
  /* --- СВЯЗЬ -----------------------------------------------------------
     СВОЙ ТОКЕН ВЕСА (D-190). Связь рисовалась `--line-w-strong`, а он
     объявлен для трёх употреблений: связь, ось, граница периода.
     Заказчик просил тоньше связь — правка токена изменила бы оси
     и границы заодно. Возражение TN Gantt, и оно верное.

     Единица, а не двойка: связь тоньше знака раздела, а не толще.
     Раздел — структура, связь — отношение между двумя задачами,
     и структура не может быть легче отношения.

     ПРИГЛУШЕНИЕ ИМЕЕТ ПОЛ. При плотности сверх 300 связей слой
     приглушается, и приглушённая единица толщины пропадает — второе
     возражение TN Gantt, тоже верное. Порог посчитан: 0.7 держит 3.0
     к фону в обеих темах, 0.65 уже нет. Ниже опускать нельзя. */
  /* --- ЗНАК РАЗДЕЛА ---------------------------------------------------
     ПОЛОСА, А НЕ СКОБКА (D-193, отменяет D-192). Раздел показывается
     литой полосой ВПОЛОВИНУ задачи, с торцами вниз на обоих концах.
     Так это устроено в MS Project, и заказчик TN Gantt, двадцать лет
     смотревший на него, назвал скобку непохожей на ожидаемое.

     ЧЕМ БЫЛ НЕВЕРЕН МОЙ ОТКАЗ. Я оставил скобку бестелесной, сославшись
     на D-046: «у полосы есть тело, которое читается как объём».
     Довод доказывает слишком много — у полосы ЗАДАЧИ тело тоже есть,
     и её длина тоже не равна трудоёмкости. Опасность была не в теле,
     а в НЕОТЛИЧИМОСТИ раздела от задачи, и снимается она тремя
     различиями сразу: толщина вдвое меньше, заливка своя, торцы,
     которых у задачи нет.

     ТОНКАЯ ЛИНИЯ ОСТАЁТСЯ ЗА СВЯЗЬЮ. Волосяной знак на полотне ровно
     один — связь; раздел им быть перестал. */
  --tl-bracket-h:    5px;   /* треть полосы задачи: вполовину
                               оказалось массивно на глаз */
  --tl-bracket-cap:  7px;   /* ширина торца: 5px не читались рядом
                               с плашкой имени — замер, не прикидка */
  --tl-bracket-drop: 5px;   /* насколько торец свисает ниже полосы */

  --link-w:    1px;
  --link-dim:  .7;         /* пол приглушения: держит 3.0 к фону */

  --link-head: 5px;        /* сторона литого наконечника */
  --link-head-deg: 30;     /* половина раствора, градусы — число, не угол:
                              продукт считает координаты, а не поворачивает */
  --link-blind: 7px;       /* высота глухого торца: «вам не покажут» */
  --line-w-bar:    3px;    /* полоса-метка, подчёркивание выбранного */

  /* --- ТАЧ-ТАРГЕТ ----------------------------------------------------
     Минимальная высота элемента, по которому попадают пальцем.
     Значение из Apple HIG; в вебе действует то же, планшет никто
     не отменял.                                                     */
  --tap-min: 44px;

  /* --- КОМПАКТНЫЙ ЭЛЕМЕНТ УПРАВЛЕНИЯ ---------------------------------
     Язык объявил подстроку в 24px (`--tl-row-sub`) и не дал ни одного
     элемента, который в неё влезает: любое поле при кегле 12 и отступах
     3 даёт 26 (замер TN Gantt, D-203). Строка, в которую нечего
     положить, — не строка.
     На крупном указателе действует `--tap-min`, и компактный размер
     на него не распространяется.                                    */
  --ctl-h-sm: 22px;

  /* Переключатель правого угла просроченной части (D-205). Объявлен
     здесь, а не только на самом знаке: `var()`, который разрешается
     лишь в одном правиле, для проверки языка неотличим от опечатки,
     а для продукта — от собственного имени. */
  --late-end: 0;

  /* --- ТОЧКИ ПЕРЕЛОМА ------------------------------------------------
     ЕДИНСТВЕННОЕ, что нельзя вынести в переменные: CSS-переменные
     в media-запросах не работают. Значения дублируются в каждом файле
     руками, поэтому записаны здесь — чтобы было где искать.

       1180px — реестр перестаёт помещаться рядом с аналитикой
                и уходит вниз ЦЕЛИКОМ (сжимать до 300 px нельзя)
        860px — боковая навигация убирается                          */
  /* --- РАЗМЕРЫ КАРКАСА ----------------------------------------------- */
  /* Зазор между модалом и краем окна по вертикали. ОДНА величина
     на два места: поле подложки и вычет из высоты модала. Записанные
     порознь, они разошлись бы при первой правке — а разошедшись, дали
     бы полосу, в которой содержимое видно ВЫШЕ шапки (D-134). */
  --modal-inset: 6vh;
  --side-w:  208px;  /* боковая навигация */
  /* Выехавшая панель ШИРЕ прижатой: на узком экране рядом с ней нет
     содержимого, ради которого её ужимали, а пункт навигации получает
     тач-габарит и полное имя раздела без переноса. */
  --side-w-open: 264px;
  --reg-w:   452px;  /* колонка реестра: минимум, на котором двухстрочная
                        ячейка ещё читается, и он же ширина iPhone-макета */
  --row-h:    32px;  /* строка плотной таблицы */
  --form-w:  340px;  /* отдельно стоящая форма: вход, узкий диалог.
                        Мера строки (--measure) для неё втрое широка */
  --measure: 760px;  /* мера строки объяснений: длиннее не читается */

/* --- ТАЙМЛАЙН (0.10) -------------------------------------------------
   Высота строки — единственное место, откуда её берут ОБЕ панели.
   Значение в одном месте потому, что два списка одних и тех же чисел
   расходятся всегда, а здесь расхождение делает диаграмму ложной. */
  --tl-row: 28px;          /* строка задачи: плотность реестра (D-004) */
  --tl-row-sub: 24px;      /* подстрока назначения — мельче, но не вдвое */
  /* ШИРИНОЙ ТАБЛИЦЫ УПРАВЛЯЮТ ДВА МЕХАНИЗМА: захват границы и свёртка.
     Захват писал прямо в `--tl-table-w` инлайновым стилем, а свёртка —
     классом; инлайновый стиль сильнее класса ВСЕГДА, и свёрнутая
     таблица оставляла на экране пустое место шириной с последнее
     перетаскивание.

     Величина, которой управляют два механизма, обязана иметь ОДИН
     выход: захват пишет в `--tl-table-set`, свёртка перебивает
     результат (D-114).

     ПРОИЗВОДНАЯ ВЕЛИЧИНА ВЫЧИСЛЯЕТСЯ ТАМ, ГДЕ ОБЪЯВЛЕНА. Пока
     `--tl-table-w: var(--tl-table-set)` стояло здесь, в `:root`, оно
     подставлялось из корневого значения и наследовалось вниз ГОТОВЫМ
     числом: задать `--tl-table-set` на самом таймлайне было уже поздно,
     и захват границы переставал работать (D-115). Вывод перенесён
     на `.tl` — туда, где меняется источник. */
  --tl-table-set: 520px;   /* левая панель: галочка, название, даты, дни */
  /* Умолчание на случай, если продукт не подключил каркас: вывод,
     который действительно работает, стоит на `.tl` во frame.css. */
  --tl-table-w: var(--tl-table-set);
  --tl-bar-h: 14px;        /* полоса: половина строки, поля сверху и снизу */
  --tl-tick-w: 3px;        /* засечка ниже порога — у неё НЕТ длины */
  --tl-min-bar: 6px;       /* порог смены знака: уже — засечка (D-046) */
  /* Колонки левой панели. Имя резиновое, три величины — по содержимому.
     Заведено 0.10.4: шапка обещала «начало · конец · раб. дн.», а строка
     была одним текстовым блоком. Обещание разметкой не выполнялось. */
  /* ШИРИНА ПОЛОТНА. Значение назначает ПРОДУКТ — это геометрия (D-028).
     Но то, что полотно имеет СОБСТВЕННУЮ ширину, а не ширину колонки,
     и что проценты считаются от неё, — планировка, и без этого липкая
     таблица и горизонтальная прокрутка были механизмом без предмета:
     переполнения не возникало никогда (D-058).
     100% сохраняет поведение «всё влезает». Масштаб «день» на годовом
     проекте — это ~3000px, и он же включает прокрутку. */
  /* ШИРИНА ДОРОЖКИ. Переименован из `--tl-canvas-w` в 0.25: полотна
     как элемента больше нет, а имя называло его. Ноль означает
     «дорожка по остатку окна», то есть горизонтали нет. */
  --tl-track-w: 0px;

  /* --- КАЛЕНДАРНАЯ ПОДЛОЖКА -------------------------------------------
     ДЕЛЕНИЕ ВРЕМЕНИ — ЛИНИЯ, НЕРАБОЧЕЕ ВРЕМЯ — ЗАЛИВКА. Линия делит,
     заливка кодирует; смешивать нельзя, иначе на полотне окажется
     четыре уровня серого позади полос, и полосы перестанут читаться
     (D-092).

     ЦВЕТ ЗДЕСЬ НЕ ТРАТИТСЯ. Оттенок закреплён за отклонением (D-010),
     а подложка календаря — это не состояние данных. Выходной
     и праздник различаются ГЛУБИНОЙ ТОНА одной шкалы, а не оттенком.

     Шаг сетки и ширину полосы зебры даёт продукт: это геометрия,
     а не язык. Ноль означает «не показывать».

     СЕТКА ДВУХСТУПЕНЧАТАЯ: мелкий шаг — единица (день на квартале),
     крупный — та единица, которую можно подписать (неделя). Подпись
     стоит ровно на крупной линии, иначе она называет не то деление,
     под которым стоит (D-093).

     ТОН КАЛЕНДАРЯ ХОЛОДНЫЙ, НО ЭТО ТОН, А НЕ ЦВЕТ: насыщенность
     держится ниже C* = 4 в Lab. Выше начинается цвет, а цвет закреплён
     за отклонением (D-010). Проверяется tools/theme_check.py.

     НОЛЬ НЕ ВЫКЛЮЧАЕТ СЛОЙ, А ЗАЛИВАЕТ ЕГО ЦЕЛИКОМ. Повторяющийся
     градиент с нулевым периодом не «ничего»: браузер разливает первый
     цвет по всей ширине. Именно так полотно квартала стало сплошь
     серым при `--tl-band: 0px`. Выключается слой ЦВЕТОМ, а не длиной:
     `transparent` — единственное значение, которое означает «нет»
     при любом периоде (D-100).                                      */
  --tl-step: 100%;         /* мелкий шаг сетки: единица масштаба */
  --tl-major: 100%;        /* крупный шаг: то, что подписывается */
  --tl-band: 100%;         /* ширина полосы зебры: обычно неделя */
  --tl-week: 100%;         /* период недели в доле дорожки */
  --tl-off-at: 100%;       /* сдвиг до первого выходного внутри недели */
  --tl-off-w: 0%;          /* длина выходных: обычно две доли из семи */
  --tl-grid-c: #f2f2f0;    /* мелкая линия */
  --tl-grid-major-c: #e7e7e4; /* крупная линия — та, что подписана */
  --tl-zebra-c: transparent; /* чередование недель: включает ГОДОВОЙ масштаб */
  --tl-off-c: #eff2f5;     /* выходной: L* 95.3, C* 1.9 */
  --tl-holiday-c: #e7e7ee; /* праздник: C* 3.6, ΔL* 3.5 к выходному */
  /* ВЫСОТА ШАПКИ — ОДНА НА ОБЕ ПАНЕЛИ, как и высота строки.
     Шапка двухъярусная: крупная единица сверху, мелкая снизу.
     Пока высота выводилась из содержимого, панели давали разное число
     рядов — таблица один, полотно два, — и всё полотно съезжало вниз
     на строку. Ровно тот же класс, что `--tl-row`: величина, обязанная
     совпадать, вычислялась каждой панелью отдельно (D-059). */
  /* ШАПКА — НАБОР ЯРУСОВ, И ЧИСЛО ИХ СЛЕДУЕТ ИЗ МАСШТАБА, а не задано
     раз навсегда. На квартале читается день: месяц · неделя · число ·
     день недели. На годе день не читается вовсе: месяц · неделя ·
     период дат. Ярус, который нечем заполнить, не показывается —
     иначе шапка занимает высоту ради пустоты (D-095).

     `--tl-head-h` стал НИЖНЕЙ границей, а не высотой: высота теперь
     считается из числа ярусов. Раньше это была величина, обязанная
     совпадать у двух панелей; панелей нет. */
  --tl-head-h: 40px;
  --tl-tier-h: 16px;       /* высота одного яруса шкалы */

  /* ПОДПИСЬ НЕ ЗАНИМАЕТ БОЛЬШЕ ПОЛОВИНЫ СВОЕГО ДЕЛЕНИЯ. Двузначное
     число кеглем --f-micro — около 14 px, значит деление, у которого
     подписано каждое, обязано быть не уже 28 px.

     БЫЛО 44 px — «двузначное число С ЗАПАСОМ НА ГЛАЗ». Запас взялся
     из референса, где кегль крупнее, и обоснования не имел: правило
     давало 28, остальные 16 были вкусом, выданным за правило (D-106).

     Ниже порога подписывается каждое N-е деление, и формула считается
     ПРОТИВ ПОЛОВИНЫ, потому что ограничивает половина:

         N = ceil(2 × ширина подписи / шаг деления)

     ФОРМУЛА БЫЛА НЕВЕРНА ДО 0.57: там стояло `ceil(подпись / деление)`,
     то есть она давала «подписывать каждое» ровно тогда, когда подпись
     уже не помещается — при подписи шире половины, но ýже деления.
     Правило и формула противоречили друг другу, и правило было верным
     (TN Gantt, замер 19.08 → D-133).

     ШИРИНУ ПОДПИСИ НАДО МЕРИТЬ, А НЕ ПРЕДПОЛАГАТЬ. Формула верна
     при любом шрифте, но с подставленным числом «как у нас» она
     ошибается у того, у кого шрифт шире, — и ошибается тихо и только
     у части читателей.

     ПЕРЕИМЕНОВАН ДВАЖДЫ. `--tl-label-min` говорил о подписи, а величина
     была про деление (D-129). `--tl-div-min` говорил о делении шкалы,
     а величина — про ШАГ ПОДПИСЕЙ: подписывают не каждое деление,
     и ограничивает подпись расстояние до соседней подписи, а не
     длительность названного интервала. Первое переименование починило
     половину лжи (D-133). */
  --tl-label-step-min: 28px;
  /* УСТАРЕЛИ, снять в 1.0. Оставлены значением нового имени, чтобы
     продукт со старым получал прежнее число, а не пустоту. */
  --tl-div-min:   var(--tl-label-step-min);
  --tl-label-min: var(--tl-label-step-min);
  /* ПОРЯДОК ЗДЕСЬ ОБЯЗАН СОВПАДАТЬ С ПОРЯДКОМ В РАЗМЕТКЕ.
     В 0.11 резиновая колонка стояла первой, а первым в разметке идёт
     треугольник свёртки: имя попадало в 20 px и схлопывалось в одну
     букву, треугольник — в резину. Ошибка бесшумная: сетка разложила
     всё честно, просто не то не туда (D-064). */
  /* КАЖДАЯ КОЛОНКА — СВОЙ ТОКЕН, а состав собирается из них. Иначе
     ширину одной колонки нельзя изменить, не переписав строку целиком,
     то есть не тронув остальные пять.

     ИМЯ ОСТАЁТСЯ РЕЗИНОВЫМ, и это не упущение: резиновая колонка
     в таблице ровно одна, иначе остаток делится между несколькими
     и ни одна не имеет своей ширины. Ширина имени меняется двумя
     способами, и оба сводятся к одному числу — остатку: раздвинуть
     панель или отнять у числовых колонок (D-090).

     У ИМЕНИ ЕСТЬ ПОЛ. `minmax(0, 1fr)` позволял ему схлопнуться
     в ноль: на узкой панели числовые колонки забирали всё, потому что
     их ширины заданы жёстко, а у имени нижней границы не было. */
  --tl-c-fold: 24px;
  --tl-name-min: 90px;
  --tl-c-date: 60px;
  --tl-c-days: 44px;
  --tl-prog-col: 104px;
  --tl-cols: var(--tl-c-fold) minmax(var(--tl-name-min), 1fr)
             var(--tl-c-date) var(--tl-c-date) var(--tl-c-days)
             var(--tl-prog-col);
  --tl-prog-w: 56px;       /* полоса прогресса: общая база для сравнения */
  /* ОТСТУП НА ОДИН УРОВЕНЬ. Задача — один, назначение — два.
     Уровней в архетипе три, и множитель пишется в правиле, чтобы
     число жило в одном месте (D-088). */
  --tl-indent: 14px;
  --tl-fold-mark: 14px;    /* треугольник свёртки: знак, а не точка */
  --tl-fold-btn:  20px;    /* сторона кнопки управления в шапке полотна.
                              Стояла числом внутри `.tl-tiers`, и продукт
                              снял её глазами — вторая копия чужой
                              величины (замер TN Gantt, D-202). */
  /* ПОЛОСА УПРАВЛЕНИЯ между таблицей и дорожкой: липкая вместе
     с таблицей, шириной в один знак с полем. Знаки полотна живут
     в ней, а не внутри того, что они сворачивают (D-202). */
  --tl-gutter: calc(var(--tl-fold-btn) + var(--s2));

  /* ЭЛЕМЕНТ СТОПКИ В ЯЧЕЙКЕ. Соседние колонки-списки обязаны идти
     параллельно: третий проект напротив своей третьей суммы. Общая
     высота элемента — величина языка, потому что расходится она молча
     и выглядит при этом правдоподобно (D-089). */
  --stack-item: 26px;
  --tl-grip-w: 5px;        /* полоса захвата границы панелей */       /* отступ задачи под разделом */
}

/* --- ТЁМНАЯ ТЕМА ------------------------------------------------------
   Переопределяются только значения, ни одно правило не дублируется.

   ТЁМНАЯ ТЕМА ОБЯЗАНА ДАВАТЬ НЕ МЕНЬШЕ СВЕТЛОЙ на структурных парах,
   и мерится это ΔL*, а не отношением контраста (D-087). Отношение
   при низкой яркости сжимается: две тёмные краски, различимые так же,
   как две светлые, дают заметно меньшее число. Считать по нему —
   значит либо не увидеть провал, либо переправить то, что верно.

   `--n2` был #111113: ΔL* до `--n0` — 2.73 против 4.60 в светлой теме.
   Это разделитель строки (`border-bottom` у каждой строки таблицы
   и таймлайна), то есть главный структурирующий приём плотного
   реестра. Проверяется tools/theme_check.py.                        */
[data-theme="dark"] {
  --n0: #17171a; --n1: #1d1d21; --n2: #0c0c0e; --n3: #2a2a30;
  --n4: #3a3a42; --n6: #8c8c97; --n7: #a8a8b2; --n9: #ececef;

  --accent: #7d9dff; --accent-soft: #1d2540;

  --st-ok:       #63c08c;  --st-ok-soft:       #16261d;
  --st-old:      #d8ad5b;  --st-old-soft:      #272013;
  --st-none:     #8c8c97;  --st-none-soft:     #1f1f24;
  --st-unusable: #ef8079;  --st-unusable-soft: #2b1917;

  /* ТОНА КАЛЕНДАРЯ И ПОЛОТНА В ТЁМНОЙ ТЕМЕ (D-165). Их не было вовсе:
     ни один `--tl-*` не переопределялся, и в тёмной теме подложка
     выходного оставалась СВЕТЛОЙ — на ней светлый текст. Это касалось
     не только календаря 0.63, но и всего полотна с 0.25; не увидел
     никто, потому что полотно ни у кого ещё не выпущено, а витрина
     до 0.61 рендерилась в колонку навигации.

     Отношения сохранены: ΔL* выходного к празднику 3.38 при пороге 3.0,
     насыщенность обоих ниже предела 4.0. Проверяет theme_check. */
  /* Глубина тёмной темы — поверхностью, а не тенью (D-179).
     ΔL* к --n0 равен 4.09 — столько же, сколько у --n0 к фону
     страницы: всплывшее отделено от своего основания ровно так же,
     как основание отделено от страницы. Тени переопределены тоже:
     на тёмном они не отделяют, но и мешать не должны. */
  --surface-raised: #1f1f24;
  --surface-over:   #272730;   /* ΔL* 4.03 к предыдущему — тот же шаг */
  --sh-raised:  0 1px 2px rgba(0,0,0,.40);
  --sh-popover: 0 4px 12px rgba(0,0,0,.55);
  --sh-modal:   0 8px 30px rgba(0,0,0,.65);

  --fill-plain:      #666672;
  --fill-group:      #c0c0c8;

  --tl-grid-c:       #1e1e1e;
  --tl-grid-major-c: #262626;
  --tl-off-c:        #202025;
  --tl-holiday-c:    #27272c;

  --st-ok-soft-hover:       #182b20;
  --st-old-soft-hover:      #2c2415;
  --st-none-soft-hover:     #242429;
  --st-unusable-soft-hover: #321d1a;
  --accent-soft-hover:      #212a48;

  --info: #b09ce8;  --info-soft: #221c33;  --info-soft-hover: #272039;

  --d1: #7d9dff; --d2: #4fc0cc; --d3: #b18cf0;
  --d4: #e0a45e; --d5: #71bb71; --d6: #e07bb0;

}

/* --- tokens/components.css --- */
/* ====================================================================
   TN Services — компоненты визуального языка
   Версия 0.87 · 20.08.2026

   Правила применения токенов. Значений здесь нет — только var().
   Любое число, кроме 0, 1px и 100%, — ошибка: его место в tokens.css.

   Подключать после tokens.css.
   ==================================================================== */

/* --- ОСНОВА --------------------------------------------------------- */
.tn, .tn * { box-sizing: border-box; }

/* --- [hidden] ОБЯЗАН СКРЫВАТЬ ----------------------------------------
   Браузерное правило `[hidden] { display: none }` — селектор по атрибуту,
   специфичность 0,1,0. Любой класс языка, задающий `display`, его
   перебивает: таких классов 49, включая `.app`, который оборачивает
   КАЖДЫЙ экран. Скрытый элемент продолжает рисоваться.

   Отказ при этом БЕСШУМНЫЙ. В продукте это выглядело так: владелец
   вводит пароль, вход проходит, поле очищается — и страница не меняется.
   Ни ошибки, ни отказа в консоли. Диагностировать пришлось по
   единственной зацепке «поле очистилось, а очищается оно только в then».

   Точечные `.work[hidden]`, `.panel[hidden]`, `.ms-pop[hidden]` были
   заплатками на три известных случая из сорока девяти — то есть
   приглашением наступить на сорок шестую.

   `!important` здесь оправдан и не является исключением из вкуса:
   `hidden` означает «элемент не относится к делу и не должен
   отображаться». Правило, которое это перебивает, — дефект по
   определению. Так же поступают все распространённые сбросы стилей.

   ПОЧЕМУ ЭТО НЕ ПОЙМАЛИ ПРОВЕРКИ. Прогон разметки БЕЗ стиля показывает
   идеально работающий `[hidden]`: ломает его именно CSS. Проверка
   вёрстки, не подключающая стиль, отвечает не на тот вопрос
   (D-027).                                                          */
.tn [hidden], .tn[hidden] { display: none !important; }
.tn {
  font: var(--f-ui)/var(--lh-base) var(--font);
  color: var(--n9);
  background: var(--n0);
  -webkit-font-smoothing: antialiased;
}

/* Цифры табличные ВЕЗДЕ, где число можно сравнить с другим числом.
   Пропорциональные цифры делают колонку сумм нечитаемой. */
.tn .num, .tn .value, .tn input[inputmode="decimal"],
.tn input[inputmode="numeric"] {
  font-variant-numeric: tabular-nums lining-nums;
}

/* ПОЛЕ ДАТЫ — СВОЁ, А НЕ `type="date"`. Нативное поле рисует формат
   по локали ЧИТАТЕЛЯ: `08/18/2026` при en-US у русского продукта,
   и управлять этим из разметки нельзя (§5а п.15, D-069). Ссылку
   на проект открывают несколько человек, скриншоты пересылают —
   `03.04` и `04.03` у двух читателей выглядят одинаково и означают
   разное. Цена ошибки — сорванный срок (TN Gantt, заявка о датах).

   Поведение календаря язык не раздаёт (D-137). Что он даёт — общий
   ВИД и общий договор о вводе: ширина по содержимому формата,
   табличные цифры, `inputmode` для цифровой клавиатуры на телефоне.

   Подсказка формата живёт в `placeholder`, но placeholder НЕ ЗАМЕНЯЕТ
   подпись: он исчезает при первом знаке, и поле остаётся без имени —
   ровно для того читателя, которому имя нужнее всех. Подпись —
   `.field > .label`, как у всех прочих полей. */
/* ШИРИНА В ЗНАКАХ — ПРО СОДЕРЖИМОЕ, А КОРОБКА СЧИТАЕТСЯ ПО ГРАНИЦЕ
   (D-189). `width: 10ch` при `box-sizing: border-box` — это десять
   знаков МИНУС поля и рамка: «18.08.2026» превращалось в «18.08.202».
   Год обрезался у поля, заведённого ради того, чтобы год был виден.

   Поля прибавлены явно. Величина остаётся в знаках: она про формат,
   а не про пиксели, и от кегля не отвяжется. */
.tn input.date {
  width: calc(10ch + var(--s2) * 2 + 2px); min-width: 0; text-align: right;
  font-variant-numeric: tabular-nums lining-nums; }
.tn input.date--short { width: calc(5ch + var(--s2) * 2 + 2px); }  /* ДД.ММ */
.tn input.time { width: calc(5ch + var(--s2) * 2 + 2px); text-align: right;
  font-variant-numeric: tabular-nums lining-nums; }

/* --- ТИПОГРАФИКА ---------------------------------------------------- */
.tn .t-label {              /* подпись колонки, заголовок секции */
  font-size: var(--f-micro); text-transform: uppercase;
  letter-spacing: .08em; color: var(--n6); font-weight: 600;
}
.tn .t-meta { font-size: var(--f-micro); color: var(--n6); }
.tn .t-sub  { font-size: var(--f-small); color: var(--n7); }
.tn .t-body { font-size: var(--f-body); }
.tn .t-explain {
  font-size: var(--f-small); color: var(--n7);
  line-height: var(--lh-text); max-width: var(--measure);
}

/* --- ЗНАЧЕНИЕ И ЕДИНИЦА ---------------------------------------------
   Величина и единица — разные вещи. Код валюты мельче и приглушён,
   иначе он спорит с числом за внимание. В таблицах — ISO-код, а не
   символ: «$» схлопывает USD, AUD и CAD в один знак.               */
.tn .value {
  font-size: var(--f-display); font-weight: 600;
  letter-spacing: -.025em; line-height: var(--lh-tight);
}
.tn .value--lead { font-size: var(--f-lead); }
.tn .value--body { font-size: var(--f-body); font-weight: 400; }
.tn .unit {
  font-size: var(--f-ui); font-weight: 500; color: var(--n6);
  margin-left: 6px; letter-spacing: 0;
}
.tn .value--lead .unit { font-size: var(--f-small); }
.tn .value--body .unit { font-size: 10px; margin-left: 4px; }

/* Дельта всегда с базой сравнения: «+4,3 %» без «к чему» — мусор. */
.tn .delta { display: inline-flex; align-items: baseline; gap: var(--s2);
  font-size: var(--f-small); }
.tn .delta .v { font-weight: 600; font-variant-numeric: tabular-nums; }
.tn .delta .v--up   { color: var(--st-ok); }
.tn .delta .v--down { color: var(--st-unusable); }
.tn .delta .base { color: var(--n6); }

/* --- КОНТРОЛЫ --------------------------------------------------------
   ГЕОМЕТРИЯ КНОПКИ ЖИВЁТ НА КЛАССЕ, А НЕ НА ТЕГЕ (D-121).

   До 0.51 отступы, рамка, радиус и минимальная высота стояли
   на `.tn button`, а вид — на `.btn-*`. Значит `<a class="btn-tinted">`
   получал цвет и не получал ничего остального: буквы сидели вплотную
   к подложке.

   Ссылка, выглядящая кнопкой, — не вольность: там, где действие
   ЯВЛЯЕТСЯ ПЕРЕХОДОМ (внешний адрес, файл, вход через провайдера),
   подменить её на `<button>` нельзя. Пропадут `Cmd`+клик и открытие
   в новой вкладке, пропадёт адрес в статусной строке, а скринридер
   объявит кнопку вместо ссылки — то есть соврёт о том, что произойдёт.

   Поэтому вид и геометрия — НЕ ДВА СЛОЯ, А ОДИН КОМПОНЕНТ: класс
   кнопки описывает кнопку целиком. Разделение было историей, а не
   устройством, и `.icon-btn` в этом же файле всё это время делал
   правильно — два соседних компонента отвечали на один вопрос
   по-разному.

   ПОЛЯ ВВОДА ОСТАЮТСЯ НА ТЕГЕ. У них те же значения, но `<input>`
   не бывает ссылкой: тег здесь и есть предмет.                     */
.tn input, .tn select, .tn textarea {
  font: inherit; font-size: var(--f-small); color: var(--n9);
  background: var(--n0); border: 1px solid var(--n4);
  border-radius: var(--r-sm); padding: 3px var(--s2); min-height: 26px;
}
.tn button, .tn .btn-primary, .tn .btn-tinted, .tn .btn-quiet,
.tn .icon-btn {
  font: inherit; font-size: var(--f-small); color: var(--n9);
  background: var(--n0); border: 1px solid var(--n4);
  border-radius: var(--r-md); padding: 3px var(--s2); min-height: 26px;
  cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box;
}
.tn input:focus-visible, .tn select:focus-visible,
.tn button:focus-visible, .tn .btn-primary:focus-visible,
.tn .btn-tinted:focus-visible, .tn .btn-quiet:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
  border-color: var(--accent);
}
.tn button:hover { border-color: var(--n6); }
/* ГЛАВНАЯ КНОПКА ЧИТАЕТ РОЛЬ ДЕЙСТВИЯ (D-160). До 0.63 роль была
   объявлена и не читалась ни одним компонентом языка — значит правило
   писал каждый продукт сам, своей строкой поверх языка. Пока продуктов
   двое, копии совпадут; на третьем разойдутся, и разойдутся молча,
   потому что каждая копия по отдельности верна. Нашёл STRU_magic.
   Умолчание роли равно прежнему цвету, поэтому продукт без бренда
   ничего не заметил. */
.tn .btn-primary {
  background: var(--brand-action); color: var(--brand-action-ink);
  border-color: var(--brand-action);
}
.tn .btn-primary:hover { border-color: var(--brand-action); opacity: .9; }

/* Сегментированный контрол — выбор одного из немногих. */
.tn .seg { display: inline-flex; border: 1px solid var(--n4);
  border-radius: var(--r-md); overflow: hidden; }
/* ПОДПИСЬ СЕГМЕНТА НЕ ПЕРЕНОСИТСЯ. Ширину сегментного переключателя
   задаёт содержимое; перенос делает один сегмент вдвое выше соседнего,
   то есть ломает элемент по определению, а не портит вид.
   Найдено STRU_magic на «Заказчики · 0» / «Подрядчики · 5» (D-118). */
.tn .seg button { border: none; border-radius: 0; background: var(--n0);
  color: var(--n7); padding: 4px var(--s3); white-space: nowrap; }
.tn .seg button + button { border-left: 1px solid var(--n4); }
.tn .seg button[aria-pressed="true"] { background: var(--n9); color: var(--n0); }

/* --- ФИЛЬТР ----------------------------------------------------------
   ПЕРЕИМЕНОВАН из `.chip` в 0.6. Имя `.chip` осталось рабочим, но
   ОБЪЯВЛЕНО УСТАРЕВШИМ. Удаление — не по номеру версии, а по условию:
   нулевое употребление во всех продуктах охвата и не раньше 01.09.2026.
   Срок в версиях отменён: их вышло шесть за двое суток, и окно
   устаревания при такой скорости не давало продукту ни дня (D-037).

   Причина — коллизия, которую назвал STRU_magic. В языке `.chip` был
   интерактивным фильтром: cursor: pointer, рамка, aria-pressed.
   В их продукте `.chip` — пассивная плашка статуса, 66 употреблений,
   все на `<span>`. Имя одно, контракт противоположный: подключение
   языка молча превратило бы 66 подписей в кнопки, и пользователь стал бы
   жать на подпись — ровно тот дефект, который язык у них и «лечил».

   Отдельно: обоснование разведения `.chip` / `.tag` в 0.2 ссылалось
   на дефект в Magic, которого там НЕТ. Их чип пассивен по коду,
   по комментарию и по всем 66 употреблениям. Разведение осталось
   верным, обоснование было ложным — исправлено в D-015 и D-031.

   Вывод шире случая: `.chip` — слово из общего оборота, и совпадение
   имени при разном контракте вероятнее, чем кажется. `.filter` называет
   назначение, а не форму.                                            */
.tn .filter, .tn .chip {
  font-size: var(--f-micro); padding: 3px var(--s2);
  border-radius: var(--r-sm); border: 1px solid var(--n4);
  background: var(--n0); color: var(--n7); cursor: pointer;
  min-height: 22px; display: inline-flex; align-items: center; gap: 5px;
}
.tn .filter[aria-pressed="true"], .tn .chip[aria-pressed="true"] {
  background: var(--n9); color: var(--n0); border-color: var(--n9);
}

/* --- МЕТКИ СОСТОЯНИЯ ------------------------------------------------- */
.tn .sw { width: 8px; height: 8px; border-radius: 2px; display: inline-block;
  flex: none; }
.tn .bar-mark { width: 3px; border-radius: 2px; flex: none; }
.tn .is-ok       { background: var(--st-ok); }
.tn .is-old      { background: var(--st-old); }
.tn .is-none     { background: var(--st-none); }
.tn .is-unusable { background: var(--st-unusable); }

/* --- ПОЛОСА ПОЛНОТЫ --------------------------------------------------
   ОДНА полоса на группу чисел, и она же легенда. Полоса под каждым
   значением по отдельности не даёт сравнить доли между собой.

   ОСТОРОЖНО С ШИРИНОЙ СЕГМЕНТА (D-025). Значение уходит в CSS и
   форматируется ИНАЧЕ, чем то же число для человека: «75.2%» против
   «75,2 %». Строку, показанную человеку, браузер молча отбросит,
   и полоса не нарисуется вовсе — без ошибки в консоли. Ловушка ровно
   для того, кто добросовестно переиспользует свою функцию
   форматирования.                                                  */
.tn .fillbar { display: flex; height: 6px; border-radius: 3px;
  overflow: hidden; background: var(--n3); }
.tn .fillbar i { display: block; height: 100%; }
.tn .fillbar-legend { display: flex; gap: var(--s5); margin-top: var(--s2);
  font-size: var(--f-small); color: var(--n7); flex-wrap: wrap; }
.tn .fillbar-legend span { display: inline-flex; align-items: center; gap: 6px; }
.tn .fillbar-legend b { font-variant-numeric: tabular-nums; }

/* --- ПЛОТНАЯ ТАБЛИЦА -------------------------------------------------
   Ширина по содержимому, НЕ на всю ширину окна: растянутая таблица
   разводит подпись и число по разным краям экрана.                 */
.tn table.tight { border-collapse: collapse; width: auto; }
.tn table.tight th, .tn table.tight td {
  padding: var(--s1) var(--s4) var(--s1) 0; font-size: var(--f-body);
  border-bottom: 1px solid var(--n3); white-space: nowrap;
}
.tn table.tight th {
  font-size: var(--f-micro); text-transform: uppercase; letter-spacing: .07em;
  color: var(--n6); font-weight: 600; text-align: left;
}
/* Заголовок числовой колонки выравнивается так же, как её значения. */
.tn table.tight th.num, .tn table.tight td.num { text-align: right; }
.tn table.tight tbody tr:last-child td { border-bottom: none; }
.tn table.tight tbody tr:hover td { background: var(--n1); }
.tn .share { color: var(--n6); font-size: var(--f-small);
  padding-left: var(--s2); }

/* --- СТРОКА РЕЕСТРА --------------------------------------------------
   Две колонки, каждая в две строки. Семь колонок не помещаются в
   452 px — и не нужны: этот же вид работает на iPhone без переделки. */
.tn .row {
  display: grid; grid-template-columns: 3px minmax(0,1fr) auto;
  gap: 0 var(--s3);
  padding: var(--s2) var(--s4) var(--s2) var(--s3);
  border-bottom: 1px solid var(--n3); background: var(--n0);
}
.tn .row:hover { background: var(--n1); }
.tn .row > .bar-mark { grid-row: 1 / -1; background: transparent; }
.tn .row .name {
  font-size: var(--f-body);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tn .row .meta { font-size: var(--f-micro); color: var(--n6); margin-top: 1px; }
.tn .row .right { text-align: right; }
.tn .row .own { font-size: var(--f-body); font-variant-numeric: tabular-nums; }
.tn .row .base { font-size: var(--f-micro); color: var(--n6); margin-top: 1px;
  font-variant-numeric: tabular-nums; }

.tn .row--ok       > .bar-mark { background: var(--st-ok); }
.tn .row--old      > .bar-mark { background: var(--st-old); }
.tn .row--none     > .bar-mark { background: var(--st-none); }
.tn .row--unusable > .bar-mark { background: var(--st-unusable); }

/* Отсутствие данных: курсив и приглушение всей строки. Видно
   периферийным зрением, не требует чтения. */
.tn .row--none .name, .tn .row--none .own {
  color: var(--n6); font-style: italic;
}
/* Величину красным НЕ красим: красное число читается как убыток,
   а смысл здесь другой — «применить нельзя». */
.tn .row--unusable .own  { color: var(--n6); }
.tn .row--unusable .base { color: var(--st-unusable); }

/* Вложенная строка: бумага внутри брокерского счёта. */
.tn .row--nested { padding-left: var(--s6); background: var(--n1); }
.tn .row--nested .name { color: var(--n7); }

/* Заголовок группы с агрегатом.

   ЛИПКОСТЬ — СВОЙСТВО ГРУППЫ, А НЕ МЕСТА. До 0.60 она стояла
   на `.reg-body > .group`, то есть на том единственном месте, где
   группу впервые применили: в модале, в колонке доски и в теле
   каркаса та же группа уезжала вверх. `position: sticky` без
   прокручиваемого предка не делает ничего, поэтому объявление
   безопасно везде (D-140).

   Это третий раз, когда правило записано про МЕСТО вместо ПРЕДМЕТА:
   `.tl-partial` (D-080), подвал карточки (D-119), теперь группа. */
.tn .group {
  position: sticky; top: 0; z-index: var(--z-sticky);
  display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s1) var(--s4); background: var(--n2);
  border-top: 1px solid var(--n3); border-bottom: 1px solid var(--n3);
  font-size: var(--f-small); font-weight: 600; color: var(--n7);
}
.tn .group .grow { flex: 1; }
.tn .group .count { font-weight: 400; color: var(--n6);
  font-size: var(--f-micro); font-variant-numeric: tabular-nums; }
.tn .group .total { font-variant-numeric: tabular-nums; color: var(--n9); }

/* --- БЛОК ПРОБЕЛА В ДАННЫХ -------------------------------------------
   Строки, разделённые линиями. Не плитки: плитка — остров со своими
   отступами, а из островов складывается ощущение случайности.      */
.tn .gap { display: grid; grid-template-columns: 1fr auto;
  gap: var(--s2) var(--s4); padding: var(--s3) 0;
  border-bottom: 1px solid var(--n3); }
.tn .gap:last-child { border-bottom: none; }
.tn .gap-title { font-size: var(--f-body); font-weight: 600;
  display: flex; align-items: center; gap: var(--s2); }
.tn .gap-title .bar-mark { height: 14px; }
.tn .gap-count { font-size: var(--f-body); font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--n7); }
.tn .gap-text { grid-column: 1 / -1; margin: 0; }
.tn .gap-list { grid-column: 1 / -1; margin: var(--s1) 0 0; padding: 0;
  list-style: none; font-size: var(--f-small); color: var(--n7); }
.tn .gap-list li { padding: 2px 0; display: flex;
  justify-content: space-between; gap: var(--s4); max-width: 560px;
  font-variant-numeric: tabular-nums; }
.tn .gap-list li :last-child { color: var(--n6); white-space: nowrap; }

/* --- РЕДАКТИРУЕМАЯ ТАБЛИЦА (экран ввода) ----------------------------- */
.tn table.entry { width: 100%; border-collapse: collapse; }
.tn table.entry th {
  position: sticky; top: 0; z-index: 2; background: var(--n1);
  font-size: var(--f-micro); text-transform: uppercase; letter-spacing: .07em;
  color: var(--n6); font-weight: 600; text-align: left;
  padding: var(--s2) var(--s3); border-bottom: 1px solid var(--n3);
  white-space: nowrap;
}
.tn table.entry th.num { text-align: right; }
.tn table.entry td { padding: 3px var(--s3); font-size: var(--f-body);
  border-bottom: 1px solid var(--n3); height: var(--row-h); }
.tn table.entry td.num { text-align: right;
  font-variant-numeric: tabular-nums; }
.tn table.entry tbody tr:hover td { background: var(--n1); }
.tn table.entry input { width: 132px; text-align: right;
  font-variant-numeric: tabular-nums; }
/* «Изменено» — своё значение и свой цвет. Не тот же жёлтый, что
   у предупреждения: один цвет на два смысла разрушает язык. */
.tn table.entry tr.is-changed input { border-color: var(--accent);
  background: var(--accent-soft); }
.tn table.entry tr.is-changed td:first-child {
  box-shadow: inset 3px 0 0 var(--accent); }
.tn .carry { cursor: pointer; color: var(--n6); user-select: none; }
.tn .carry:hover { color: var(--accent); }

/* --- ЛЕГЕНДА СОСТОЯНИЙ -----------------------------------------------
   Печатается рядом с данными, а не в документации: определение,
   до которого надо идти в другой файл, не работает.               */
.tn .legend { font-size: var(--f-micro); color: var(--n6); }
.tn .legend div { display: flex; gap: 6px; align-items: center; padding: 2px 0; }
.tn .legend .bar-mark { height: 12px; }

/* ====================================================================
   ЭЛЕМЕНТЫ, ПРИШЕДШИЕ ИЗ РАЗБОРА STRU_magic (версия 0.2)
   Взят перечень и поведение, не оформление: имена классов, палитра
   и шрифты — из этого языка. Разбор: docs/REFERENCES.md, решение D-014.
   ==================================================================== */

/* --- КНОПКИ: три уровня приоритета ----------------------------------
   Больше трёх не нужно: четвёртый уровень никто не различает.        */
.tn .btn-tinted {                    /* второе действие на экране */
  background: var(--n2); color: var(--n9); border-color: transparent;
}
.tn .btn-tinted:hover { background: var(--n3); border-color: transparent; }
.tn .btn-quiet {                     /* третье: подложка появляется при наведении */
  background: transparent; color: var(--n7); border-color: transparent;
}
.tn .btn-quiet:hover { background: var(--n2); color: var(--n9); }
/* Опасное действие — модификатор, а не отдельная кнопка. */
.tn .btn-tinted.is-danger, .tn .btn-quiet.is-danger {
  color: var(--st-unusable); background: var(--st-unusable-soft);
}
.tn button:disabled { opacity: .45; cursor: not-allowed; }
/* Нажатие подтверждается сжатием — единственная анимация у контрола. */
.tn button:not(:disabled):active { transform: scale(.97); }
.tn button { transition: background var(--dur-fast), border-color var(--dur-fast),
  transform var(--dur-fast) var(--ease-out); }

/* --- ПЛАШКА СОСТОЯНИЯ ------------------------------------------------
   ВНИМАНИЕ на разницу с .chip: .chip — интерактивный фильтр, .tag —
   пассивная подпись. В Magic оба назывались .chip, и по разметке было
   не понять, кликается элемент или нет.                             */
.tn .tag {
  display: inline-block; padding: 2px var(--s2); border-radius: var(--r-sm);
  font-size: var(--f-micro); font-weight: 500; line-height: 1.5;
  white-space: nowrap; background: var(--n2); color: var(--n7);
}
.tn .tag--ok       { background: var(--st-ok-soft);       color: var(--st-ok); }
.tn .tag--warn     { background: var(--st-old-soft);      color: var(--st-old); }
.tn .tag--error    { background: var(--st-unusable-soft); color: var(--st-unusable); }
.tn .tag--wait     { background: var(--n2);               color: var(--n6); }
.tn .tag--accent   { background: var(--accent-soft);      color: var(--accent); }
.tn .tag--info     { background: var(--info-soft);        color: var(--info); }

/* КАТЕГОРИЯ, а не состояние: роль, тип, провайдер, отдел. Цвет здесь
   различает, но ничего не означает — палитра данных, не палитра
   состояний. Заведено по замечанию STRU_magic: без этого первая строка
   собственного CSS поверх языка появилась бы именно здесь, а не
   на бренде. */
.tn .tag--cat-1 { background: var(--n1); color: var(--d1); box-shadow: inset 0 0 0 1px var(--d1); }
.tn .tag--cat-2 { background: var(--n1); color: var(--d2); box-shadow: inset 0 0 0 1px var(--d2); }
.tn .tag--cat-3 { background: var(--n1); color: var(--d3); box-shadow: inset 0 0 0 1px var(--d3); }
.tn .tag--cat-4 { background: var(--n1); color: var(--d4); box-shadow: inset 0 0 0 1px var(--d4); }
.tn .tag--cat-5 { background: var(--n1); color: var(--d5); box-shadow: inset 0 0 0 1px var(--d5); }
.tn .tag--cat-6 { background: var(--n1); color: var(--d6); box-shadow: inset 0 0 0 1px var(--d6); }

/* --- ЧЕКБОКС ---------------------------------------------------------
   Нативный скрыт: системный тянет свою палитру мимо токенов и не
   переживает тёмную тему. Галочка — clip-path, без картинок.        */
.tn .check { display: inline-flex; align-items: center; gap: var(--s2);
  cursor: pointer; font-size: var(--f-body); color: var(--n9); }
.tn .check input {
  appearance: none; -webkit-appearance: none; margin: 0; cursor: pointer;
  flex: 0 0 16px; width: 16px; height: 16px; padding: 0;
  border: 1px solid var(--n4); border-radius: var(--r-sm);
  background: var(--n0); display: inline-grid; place-content: center;
  min-height: 0;
  transition: background var(--dur-fast), border-color var(--dur-fast);
}
.tn .check:hover input { border-color: var(--n6); }
.tn .check input::after {
  content: ""; width: 9px; height: 9px; transform: scale(0);
  transition: transform var(--dur-fast) var(--ease-out);
  background: var(--n0);
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
}
.tn .check input:checked { background: var(--accent); border-color: var(--accent); }
.tn .check input:checked::after { transform: scale(1); }
.tn .check:has(input:disabled) { opacity: .45; cursor: not-allowed; }

/* ЧЕКБОКС КАТЕГОРИИ (D-187). Отмеченность — состояние ВЫБОРА, и выбор
   в языке кодируется акцентом; поэтому обычный флажок остаётся акцентным
   и таким останется.

   Но флажок бывает и другим: он отмечает КАТЕГОРИЮ, у которой цвет уже
   есть, — колонку доски, ряд диаграммы, тип файла. Тогда нейтральный
   акцент отнимает у списка то, ради чего категории и красятся: их
   различимость. Это ровно тот случай, для которого заведён `.tag--cat`,
   и палитра берётся та же — данных, а не состояний: цвет здесь
   РАЗЛИЧАЕТ, но ничего не означает (D-010).

   Состояний данных здесь НЕТ намеренно. Флажок «показать только ошибки»
   отмечает разрез, а не доверие к числу; покрасить его `--st-unusable`
   значило бы сказать про сам флажок, что с ним что-то не так.

   Категория видна и НЕ отмеченной — рамкой: список фильтров читается
   до того, как в нём что-то выбрали. Галочка остаётся `--n0`
   и переворачивается вместе с темой: в светлой белая на цветном,
   в тёмной тёмная на светлом. */
.tn .check--cat-1 input { border-color: var(--d1); }
.tn .check--cat-2 input { border-color: var(--d2); }
.tn .check--cat-3 input { border-color: var(--d3); }
.tn .check--cat-4 input { border-color: var(--d4); }
.tn .check--cat-5 input { border-color: var(--d5); }
.tn .check--cat-6 input { border-color: var(--d6); }
.tn .check--cat-1 input:checked { background: var(--d1); border-color: var(--d1); }
.tn .check--cat-2 input:checked { background: var(--d2); border-color: var(--d2); }
.tn .check--cat-3 input:checked { background: var(--d3); border-color: var(--d3); }
.tn .check--cat-4 input:checked { background: var(--d4); border-color: var(--d4); }
.tn .check--cat-5 input:checked { background: var(--d5); border-color: var(--d5); }
.tn .check--cat-6 input:checked { background: var(--d6); border-color: var(--d6); }
/* Наведение не перекрашивает категорию в нейтральный: цвет категории
   не состояние указателя. */
.tn .check[class*="--cat-"]:hover input { filter: brightness(.92); }

/* --- ПОЛЕ ФОРМЫ ------------------------------------------------------ */
.tn .field { display: flex; flex-direction: column; gap: var(--s1);
  margin-bottom: var(--s4); }
.tn .field > .label { font-size: var(--f-micro); text-transform: uppercase;
  letter-spacing: .08em; color: var(--n6); font-weight: 600; }
/* Подсказка ТИШЕ подписи, а не громче: подсказка весом крупнее подписи
   читается как заголовок, а заголовок — как подпись.

   ВИД ПОДСКАЗКИ НЕ ЗАВИСИТ ОТ ТОГО, ЧЕМУ ОНА СОПУТСТВУЕТ. До 0.56
   правило стояло на `.field > .hint`, и подсказка к ЦЕЛОМУ БЛОКУ —
   «крупность влияет только на вид, сроки не меняются» — оставалась
   обычным текстом. Обернуть её в `.field` нельзя: `.field` означает
   поле ввода с подписью, и обёртка ради стиля соврала бы разметкой
   (TN Gantt, заявка 19.08 → D-130).

   TN Gantt просил оставить `.field > .hint` ради отступов — не оставил:
   ОТСТУПОВ ТАМ НЕ БЫЛО. Правило состояло из кегля, цвета и насыщенности,
   то есть целиком из вида; расстояние подсказке даёт `gap` самого
   `.field`. Оставить селектор «на всякий случай» значило бы оставить
   пустое место, в которое однажды что-нибудь положат. */
.tn .hint { font-size: var(--f-micro); color: var(--n6);
  font-weight: 400; }
/* СООБЩЕНИЕ ОБ ОШИБКЕ — ТОЖЕ НЕ ЗАВИСИТ ОТ ТОГО, ЧЕМУ СОПУТСТВУЕТ
   (D-166). Ровно тот же довод, что абзацем выше про `.hint`, и он был
   применён к подсказке в 0.56, а к ошибке — нет. Сообщение об ошибке
   уровня блока — «даты не сходятся между собой» — оставалось обычным
   текстом, а обернуть его в `.field` нельзя по D-130.

   Нашлось косвенно: STRU_magic держал своё `.date-bad` и объяснил,
   почему НЕ подаёт заявку — язык не знает, какие даты продукт считает
   допустимыми. Про правило они правы целиком. Но вид сообщения языку
   принадлежит, и имя у него было — просто не доставало до их случая. */
.tn .err { display: block;
  font-size: var(--f-micro); color: var(--st-unusable); }
/* `display: block` — ПРОДОЛЖЕНИЕ ТОЙ ЖЕ ПРАВКИ, а не мелочь (D-168).
   Пока условие стояло в селекторе (`.field > .err`), допущение
   «родитель — колонка» было ЗАПИСАНО и видно. Освободив вид
   от контекста, я оставил допущение и сделал его молчаливым: правило
   красит где угодно, а встаёт по месту только у колоночного родителя.
   Внутри `.field` не меняется ничего — там элемент и так занимал
   свою строку; за его пределами перестаёт вставать сбоку.
   Замер STRU_magic: сообщение оказывалось на 113 px правее поля
   и на одной строке с ним. */

/* --- ПРЕДЕЛ ДЛИНЫ ТЕКСТА (PROJ-22) ------------------------------------
   Счётчик под полем, у которого есть предел длины. Молчит, пока до предела
   далеко; с 80 % говорит «осталось N», с 90 % — тем же текстом в цвете
   предупреждения; сверх предела — «лишние N» в цвете ошибки, а поле
   получает `aria-invalid` и форма не отправляется. Разметку и слова даёт
   `static/limit.js`, здесь только вид.

   ИМЯ НЕ `.count`. Оно уже занято числом у заголовка колонки доски и группы
   (`.board-col > .head .count`, `.group .count`), и блочный счётчик под полем
   сломал бы строку заголовка, где число стоит в строку.

   ЧИСЛО СЛОВАМИ, А НЕ ЦВЕТОМ: цвет лишь повторяет то, что сказано. */
.tn .limit-count { display: block; align-self: flex-end; text-align: right;
  font-size: var(--f-micro); color: var(--n6);
  font-variant-numeric: tabular-nums lining-nums; }
.tn .limit-count[data-state="warn"] { color: var(--st-old); }
.tn .limit-count[data-state="over"] { color: var(--st-unusable); font-weight: 600; }
.tn textarea[aria-invalid="true"], .tn input[aria-invalid="true"] {
  border-color: var(--st-unusable); }
/* Голос для диктора: слышен, не виден. Объявляет переход порога, а не
   каждое нажатие — поэтому это отдельный узел, а не сам счётчик. */
.tn .limit-sr { position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* --- РАСКРЫВАЮЩИЙСЯ БЛОК --------------------------------------------- */
.tn .disclosure { border-bottom: 1px solid var(--n3); }
.tn .disclosure > summary {
  cursor: pointer; padding: var(--s2) 0; font-size: var(--f-body);
  font-weight: 600; list-style: none; display: flex; align-items: center;
  gap: var(--s2);
}
.tn .disclosure > summary::-webkit-details-marker { display: none; }
/* МАРКЕР ЧИТАЕТСЯ, А НЕ УГАДЫВАЕТСЯ (D-186). Глиф ▸ при кегле подписи
   рисуется втрое мельче буквы: он занимает малую часть своей площадки,
   и на 13 px превращается в точку размером с буллит. Кегль задан явно
   и в долях подписи, а не абсолютом: подпись подрастёт — маркер за ней.

   Цвет — `--n7`, а не `--n6`: маркер это affordance, единственный знак
   того, что блок раскрывается. Третий уровень текста для него слишком
   тих, а `--n9` спорил бы с подписью за внимание. */
.tn .disclosure > summary::before {
  content: "\25b8"; color: var(--n7); flex: 0 0 auto;
  font-size: 1.35em; line-height: 1;
  transition: transform var(--dur-fast) var(--ease-out);
}
.tn .disclosure[open] > summary::before { transform: rotate(90deg); }
.tn .disclosure > .body { padding: 0 0 var(--s3); }

/* --- ИНФО-ПЛАШКА -----------------------------------------------------
   Для ошибок НЕ она: у ошибок своя палитра состояния.               */
.tn .note { background: var(--n1); border-left: 3px solid var(--n4);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: var(--s2) var(--s3); margin: var(--s3) 0;
  font-size: var(--f-small); color: var(--n7); }
.tn .note--warn  { background: var(--st-old-soft);      border-left-color: var(--st-old); }
.tn .note--error { background: var(--st-unusable-soft); border-left-color: var(--st-unusable); }
/* «ПОЛУЧИЛОСЬ» ОТДЕЛЕНО ОТ «ВОТ ПОЯСНЕНИЕ» (D-213, находка
   TN FinanceWatcher). До 0.81 успех операции и обычное пояснение были
   одним и тем же `.note`: два разных сообщения выглядели одинаково,
   и читатель не отличал «сохранено» от «тут написано, как считается».

   Краска взята у состояния данных `--st-ok`, а не заведена своя, —
   по тому же основанию, что у `.note--error`, который берёт
   `--st-unusable`. Примечания заимствуют краски состояний с самого
   начала; заводить второй зелёный значило бы иметь два зелёных
   и объяснять разницу.

   Знак недолговечности язык НЕ даёт: исчезает сообщение или остаётся —
   решает продукт, потому что это поведение (D-137). */
.tn .note--ok    { background: var(--st-ok-soft); border-left-color: var(--st-ok); }
/* ПРИМЕЧАНИЕ ЧИТАТЕЛЮ — не состояние данных (D-151). Четыре величины,
   которые просил STRU_magic — фон, линия, чернила, акцент, — в языке
   складываются в две: линия и акцент здесь одно и то же, а чернила
   и линия берут один токен, как уже устроены warn и error. */
/* Чернила остаются --n7, как у warn и error: цвет несёт ТОРЕЦ, а текст
   плашки читается как текст. Первая редакция красила и текст — форма
   разошлась бы с двумя соседями у одного компонента. */
.tn .note--info  { background: var(--info-soft); border-left-color: var(--info); }

/* НАВЕДЕНИЕ. Плашка сама по себе не интерактивна; модификатор ставится
   там, где по ней действительно жмут, — иначе курсор обещает действие,
   которого нет. */
.tn .note.is-clickable:hover               { background: var(--st-none-soft-hover); }
.tn .note--warn.is-clickable:hover         { background: var(--st-old-soft-hover); }
.tn .note--error.is-clickable:hover        { background: var(--st-unusable-soft-hover); }
.tn .note--ok.is-clickable:hover           { background: var(--st-ok-soft-hover); }
.tn .note--info.is-clickable:hover         { background: var(--info-soft-hover); }

/* --- БЛОК КОДА -------------------------------------------------------
   Цвет фона кода у языка БЫЛ — это `--n1`, им красится инлайн-код ниже.
   Не было МНОГОСТРОЧНОГО блока, и просьба «фон кода» на деле про него
   (D-151). Прокрутка по горизонтали, а не перенос: перенесённая строка
   кода меняет смысл того, что показано. */
.tn .code-block {
  font-family: var(--font-mono); font-size: var(--f-small);
  background: var(--n1); border: 1px solid var(--n3);
  border-radius: var(--r-sm); padding: var(--s3);
  margin: var(--s3) 0; overflow-x: auto;
  white-space: pre; tab-size: 2; color: var(--n9);
}

/* --- ЗНАКИ «СЖАТЬ» И «РАСКРЫТЬ» (D-162) -------------------------------
   Бургер не говорит, что произойдёт, и не показывает текущее состояние —
   он называет МЕСТО, где лежат команды, а нужен ЗНАК ДЕЙСТВИЯ. Просьба
   TN Gantt; те же две операции есть в реестре и на доске, поэтому знаки
   принадлежат языку, а не полотну.

   Набор иконок языком не выбран (`docs/OPEN.md`, вопрос 5), и выбирать
   его ради двух знаков — решение не того размера. Поэтому здесь то же
   разделение, что для торцов связи: РАЗМЕРЫ величина, ФОРМА правило,
   ПУТЬ вёрстка (D-128). Форма показана в стайлгайде, штрих берётся
   отсюда, путь строит продукт.

   Знак ПАРНЫЙ и меняется вместе с состоянием: `aria-expanded` говорит,
   что сейчас, а стрелки — что будет. Одна иконка на два состояния
   заставляет читателя гадать, какое из них показано. */
/* --- ЗНАК НАПРАВЛЕНИЯ: ОДНА СКОБКА НА ЧЕТЫРЕ СТОРОНЫ (D-200) --------
   «Назад», «вперёд», «сжать», «раскрыть» — это ОДИН знак, повёрнутый,
   а не четыре нарисованных. Скобка у языка уже была: пагинация,
   перелистывание месяца и стопка значений набраны `‹` и `›` с самого
   начала. Знак свернуть/развернуть я нарисовал заново штрихами SVG —
   и он вышел тяжелее соседей, потому что рисовался отдельно от них.

   Владелец языка сказал прямо: «у тебя уже есть готовые элементы
   для „ещё“ и „назад“ — используй их же».

   ОДИНАРНАЯ — ШАГ, ДВОЙНАЯ — ДО КОНЦА. Различие взято у самой
   типографики: `‹` и `«` — одна семья, и вторая читается как
   «дальше ещё» ровно так же, как двойной ус связи (D-197).

   Вес приходит от шрифта, а не от `stroke-width`, поэтому знак
   не может разойтись по весу с текстом рядом — по построению,
   а не по договорённости. `--ico-stroke-dense`, заведённая для того,
   чтобы уравнять вес вручную, этим и стала не нужна. */
.tn .chev { display: inline-block; line-height: 1; font-style: normal; }
.tn .chev--up   { transform: rotate(90deg); }
.tn .chev--down { transform: rotate(-90deg); }
.tn .chev--fwd  { transform: rotate(180deg); }

/* КРЕСТИК ЗАКРЫТИЯ КРУПНЕЕ ТЕКСТА КНОПКИ (D-201). `×` — знак
   умножения, он нарисован для формул и в размер строки сбивается
   в точку. Компенсация оптическая, поэтому величина названа в `em`:
   она следует за текстом, а не спорит с ним. Та же мера, что
   у маркера раскрывающегося блока. */
.tn .icon-btn--close { font-size: 1.35em; line-height: 1;
  padding-top: 0; padding-bottom: 0; }

/* --- КОМПАКТНЫЙ ЭЛЕМЕНТ УПРАВЛЕНИЯ (D-203) --------------------------
   Для строк, которые язык объявил ниже обычного, — прежде всего
   подстроки назначения в полотне (`--tl-row-sub`, 24px). Обычное поле
   даёт 26 и в неё не влезает; строка, в которую нечего положить,
   строкой не является.

   Отступы урезаны, кегль — нет: мельче `--f-small` язык не читает
   ничего, и подстрока не повод заводить исключение.

   На крупном указателе компактность снимается: `--tap-min` старше. */
.tn .ctl--sm { min-height: var(--ctl-h-sm); padding-top: 1px;
  padding-bottom: 1px; }
@media (any-pointer: coarse) {
  .tn .ctl--sm { min-height: var(--tap-min); }
}

.tn .ico { width: 16px; height: 16px; flex: none; display: block;
  fill: none; stroke: currentColor; stroke-width: var(--ico-stroke);
  stroke-linecap: round; stroke-linejoin: round; }
.tn .ico--lg { width: 20px; height: 20px; }

/* ЗНАК ИЗ ДВУХ ШТРИХОВ РИСУЕТСЯ ТОНЬШЕ (D-195). Равная толщина даёт
   равную толщину, а не равный вес: парная стрелка при `--ico-stroke`
   несёт вдвое больше краски, чем булавка или треугольник рядом,
   и в ряду знаков читается как выделенная — хотя ничем не выделена.
   Компенсация оптическая, поэтому и величина своя, а не производная. */
.tn .ico--dense { stroke-width: var(--ico-stroke-dense); }

/* --- ПОЛЕ ДАТЫ ПО СЕГМЕНТАМ (D-158) ----------------------------------
   Посегментная правка — то, ради чего продукты тянутся к `type="date"`,
   и она действительно удобна: день, месяц и год правятся по отдельности,
   стрелками, без разбора строки. Терять её вместе с нативным полем
   не нужно — она НЕ ТРЕБУЕТ его.

   Что требует нативное поле, так это браузер. Снимок STRU_magic
   19.08.2026: поле показывает `dd.mm.yyyy`, а календарь под ним —
   `August 2026`, `M T W T F S S`, `Clear`, `Today`. Формат дала локаль
   машины, подписи — язык интерфейса браузера, и ни то ни другое
   продукт не выбирал. У другого читателя тот же экран скажет
   `08/19/2026`.

   Сегменты — три поля в одной рамке, разделители нарисованы, а не
   набраны: набранная точка попадает в выделение и в копирование.  */
.tn .date-seg {
  display: inline-flex; align-items: baseline;
  border: 1px solid var(--n4); border-radius: var(--r-sm);
  background: var(--n0); padding: 2px var(--s2); min-height: 26px;
  font-variant-numeric: tabular-nums lining-nums;
}
.tn .date-seg:focus-within { outline: 2px solid var(--accent);
  outline-offset: 1px; border-color: var(--accent); }
.tn .date-seg > input {
  font: inherit; font-variant-numeric: inherit; border: none;
  background: none; padding: 0; text-align: center; min-height: 0;
  color: var(--n9);
}
.tn .date-seg > input:focus-visible { outline: none;
  background: var(--accent-soft); border-radius: 2px; }
.tn .date-seg > .d, .tn .date-seg > .m { width: 2ch; }
.tn .date-seg > .y { width: 4ch; }
/* Разделитель РИСУЕТСЯ, а не набирается: набранный попадает
   в выделение, в копирование и в чтение экранным диктором. */
.tn .date-seg > .d::after, .tn .date-seg > .m::after { content: none; }
.tn .date-seg > span[aria-hidden] { color: var(--n6); padding: 0 1px; }
/* Кнопка календаря — часть поля, а не сосед: иначе она отрывается
   от него при переносе строки. */
.tn .date-seg > .cal-btn { margin-left: var(--s1); min-height: 0;
  border: none; background: none; color: var(--n6); padding: 0 2px; }
.tn .date-seg > .cal-btn:hover { color: var(--n9); }

/* ====================================================================
   КАЛЕНДАРЬ — ДОГОВОР О ВИДЕ, А НЕ ПОВЕДЕНИЕ (D-157)

   D-137 отказал в календаре и НАЗВАЛ УСЛОВИЕ ПЕРЕСМОТРА: «когда
   календарь понадобится второму продукту». TN Gantt спросил, выполнено
   ли оно, и ответ у языка был перед глазами: в FinanceWatcher ШЕСТЬ
   полей `<input type="date">`, а владелец продукта уже прочёл дату
   неверно — это записано у них в коде. Условие выполнено, и выполнено
   давно; никто не посмотрел.

   ЧТО ЯЗЫК ДАЁТ И ЧЕГО НЕ ДАЁТ. Вид, состояния дня и словарь — здесь.
   Поведение — нет, и причина не принципиальная, а механическая:
   у языка НЕТ КАНАЛА РАЗДАЧИ КОДА. `emit_tokens.py` отдаёт CSS или
   Python-константу; у FinanceWatcher политика `default-src 'none'`,
   и сценарий со стороны туда не попадёт никак. Появится канал —
   вопрос открывается снова, и это записано, чтобы отказ не выглядел
   вечным.

   ТОНА КАЛЕНДАРЯ У ЯЗЫКА УЖЕ БЫЛИ. Выходной и праздник различаются
   глубиной тона одной шкалы, насыщенность ограничена и проверяется
   `theme_check.py`. Календарь берёт те же величины, что полотно, —
   иначе два календаря одного продукта разошлись бы между собой. */
/* ГДЕ СТОИТ КАЛЕНДАРЬ — ВОПРОС ЯЗЫКА (D-163). У `.ms-pop` и `.ac-list`
   размещение задано языком, и причина записана там же: «второе место
   для этих величин означало бы, что они разойдутся». У `.cal` его
   не было, хотя всплывающий он ровно так же и слой у него тот же —
   и каждый продукт писал свои четыре строки. Нашёл STRU_magic
   (заявка-33), их довод — слово в слово мой же из D-160. */
.tn .date-field { position: relative; display: inline-block; }
/* ГЛУБИНА — ПРИЗНАК ПОЛОЖЕНИЯ, А НЕ КЛАССА (D-182). Всплывающее
   на странице и всплывающее внутри модала — разные уровни, а класс
   у них один. Уровень назначает язык вложенностью, один раз;
   продукт номера не пишет никогда. */
.tn .modal .menu, .tn .modal .cal, .tn .modal .ms-pop, .tn .modal .ac-list {
  background: var(--surface-over); }

.tn .cal { background: var(--surface-raised); border: 1px solid var(--n4);
  border-radius: var(--r-lg); box-shadow: var(--sh-popover);
  padding: var(--s2); width: max-content;
  position: absolute; top: calc(100% + var(--s1)); left: 0;
  z-index: var(--z-popover); }
.tn .cal[hidden] { display: none; }
.tn .cal-head { display: flex; align-items: center; gap: var(--s2);
  padding: 0 var(--s1) var(--s2); }
.tn .cal-head > .t { font-size: var(--f-ui); font-weight: 600;
  flex: 1 1 auto; text-align: center; }
.tn .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr);
  gap: 1px; }
.tn .cal-dow { font-size: var(--f-micro); color: var(--n6);
  text-align: center; padding-bottom: var(--s1); font-weight: 600; }
/* День — КВАДРАТ С ТАБЛИЧНОЙ ЦИФРОЙ. Пропорциональные цифры в сетке
   недели не выстраиваются в столбец, и число «11» уезжает от «22». */
.tn .cal-day {
  font: inherit; font-size: var(--f-body);
  font-variant-numeric: tabular-nums lining-nums;
  min-width: 28px; min-height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 1px solid transparent;
  border-radius: var(--r-sm); color: var(--n9); cursor: pointer;
}
.tn .cal-day:hover { background: var(--n1); }
.tn .cal-day.is-off     { background: var(--tl-off-c); }
.tn .cal-day.is-holiday { background: var(--tl-holiday-c); }
/* Не из этого месяца — приглушён, но НЕ СКРЫТ: пустые клетки ломают
   счёт недель, а «нет данных» и «другой месяц» — разные вещи. */
.tn .cal-day.is-out { color: var(--n6); }
.tn .cal-day:disabled { color: var(--n6); cursor: default;
  background: var(--st-none-soft); }
/* СЕГОДНЯ — НЕ ТОЛЬКО ЦВЕТОМ (§9). Рамка держит смысл там, где цвет
   не читается: повышенный контраст, печать, дальтонизм. */
.tn .cal-day.is-today { border-color: var(--accent); font-weight: 600; }
.tn .cal-day[aria-selected="true"] {
  background: var(--accent); color: var(--n0); border-color: var(--accent); }
/* ПРАЗДНИК ОБЯЗАН БЫТЬ НАЗВАН, а не только окрашен — то же требование,
   что на полотне (D-092, D-095). В сетке для имени места нет, поэтому
   имя живёт в `title` и в доступном имени кнопки. */
.tn .cal-day.is-holiday::after { content: ""; }

/* ПОДВАЛ КАЛЕНДАРЯ (D-163). Две команды — «сегодня» и «очистить» —
   названы в обосновании самого компонента: отказ от нативного поля
   опирался в том числе на то, что браузер пишет `Today` и `Clear`
   на своём языке. Признать команды нужными и не дать им места —
   это оставить продукту решение, которое язык уже принял. */
.tn .cal-foot { display: flex; align-items: center; gap: var(--s2);
  padding-top: var(--s2); margin-top: var(--s1);
  border-top: 1px solid var(--n3); }
.tn .cal-foot > .grow { flex: 1 1 auto; }

@media (pointer: coarse) {
  .tn .cal-day { min-width: var(--tap-min); min-height: var(--tap-min); }
}

/* --- ИНЛАЙН-КОД ------------------------------------------------------ */
.tn .code { font-family: var(--font-mono); font-size: .92em;
  background: var(--n1); border: 1px solid var(--n3);
  border-radius: var(--r-sm); padding: 0 var(--s1); }

/* --- ПАРЫ «МЕТКА — ЗНАЧЕНИЕ» -----------------------------------------
   Основа архетипа «Деталь». Двухколоночная сетка с фиксированной
   колонкой меток: без неё значения не выстраиваются и читать пары
   приходится по одной.                                              */
.tn .pairs { margin-bottom: var(--s4); }
.tn .pairs > h3 { margin: 0 0 var(--s1); font-size: var(--f-micro);
  text-transform: uppercase; letter-spacing: .08em; color: var(--n6);
  font-weight: 600; }
.tn .pair { display: grid; grid-template-columns: 15rem minmax(0,1fr);
  gap: var(--s3); align-items: baseline; padding: var(--s1) 0;
  border-bottom: 1px solid var(--n3); font-size: var(--f-body); }
.tn .pair:last-child { border-bottom: none; }
.tn .pair > .k { color: var(--n6); }
.tn .pair > .v { white-space: pre-wrap; overflow-wrap: anywhere; }
.tn .pair > .v.is-empty { color: var(--n6); font-style: italic; }
@media (max-width: 640px) {
  .tn .pair { grid-template-columns: minmax(0,1fr); gap: 0; }
  .tn .pair > .k { font-size: var(--f-micro); }
}

/* --- ПОЛОСА ПРОКРУТКИ — ЧАСТЬ ПРОКРУЧИВАЕМОЙ ОБЛАСТИ ------------------
   Оставить её браузеру значит, что ширина колонки зависит от НАПОЛНЕНИЯ:
   там, где карточек больше высоты, полоса съедает место, и колонка
   становится ýже соседней. Ширина, зависящая от данных, — не ширина.

   `scrollbar-gutter: stable` резервирует место ВСЕГДА, независимо
   от того, нужна полоса сейчас или нет (D-119).

   ОБА ЦВЕТА ЗАДАНЫ, И ЭТО ИСПРАВЛЕНИЕ 0.49. `scrollbar-width: thin`
   без `scrollbar-color` оставляет ДОРОЖКУ системного цвета, и на её
   границе с содержимым видна вертикальная линия — на доске из шести
   колонок шесть лишних вертикалей. Хуже: системный цвет разный
   в двух браузерах и двух темах, то есть ровно то, что я сам назвал
   недопустимым, принимая этот пункт. Дорожка прозрачна, ползунок —
   токен языка (D-120). */
.tn .grid-wrap, .tn .board-col > .body, .tn .reg-body,
.tn .col, .tn .ac-list, .tn .ms-list {
  scrollbar-gutter: stable; scrollbar-width: thin;
  scrollbar-color: var(--n4) transparent; }

/* ПОЛОСА В ПОКОЕ СКРЫТА ТАМ, ГДЕ КОЛИЧЕСТВО НАЗВАНО ЧИСЛОМ.
   Я отказал на доводе «полоса — единственное, что говорит: здесь есть
   ещё». Довод неверен не потому, что счётчик его заменяет, а потому
   что вопросов ДВА и они разнесены во времени:

     «сколько всего»  — спрашивают в покое; отвечает счётчик
     «где я сейчас»   — спрашивают во время прокрутки; отвечает ползунок

   Полоса нужна там и тогда, где задают второй вопрос, — то есть
   при прокрутке и при наведении. В покое, когда рядом есть число,
   она второй голос о том же (D-120).

   Модификатор явный, а не `:has(.count)`: язык не знает, отрисовал ли
   продукт счётчик, а обещание «где-то рядом есть число» — часть
   контракта, и его подписывает продукт. */
.tn .scroll-quiet { scrollbar-color: transparent transparent; }
.tn .scroll-quiet:hover, .tn .scroll-quiet:focus-within,
.tn .scroll-quiet:active { scrollbar-color: var(--n4) transparent; }

/* --- ШИРОКАЯ ТАБЛИЦА --------------------------------------------------
   Отдельный компонент от .tight: та по содержимому и без прокрутки,
   эта — на много колонок, с горизонтальной прокруткой, залипшей
   шапкой и залипшими первыми колонками.                             */
.tn .grid-wrap {
  overflow: auto; -webkit-overflow-scrolling: touch;
  max-height: var(--grid-max, 70vh);
  border: 1px solid var(--n3); border-radius: var(--r-lg);
  background: var(--n0);
}
.tn table.grid { border-collapse: separate; border-spacing: 0;
  width: max-content; font-size: var(--f-body); }
/* width: max-content обязателен. min-width: 100% возвращает дефект:
   когда содержимое уже контейнера, браузер раздаёт остаток колонкам
   пропорционально, и под словом «нет» оказывается полэкрана. */
.tn table.grid th, .tn table.grid td {
  text-align: left; padding: var(--s1) var(--s3); white-space: nowrap;
  border-bottom: 1px solid var(--n3); vertical-align: middle;
  height: var(--row-h);
}
.tn table.grid th {
  position: sticky; top: 0; z-index: var(--z-sticky);
  background: var(--n1); color: var(--n6); font-weight: 600;
  font-size: var(--f-micro); text-transform: uppercase; letter-spacing: .07em;
}
.tn table.grid tbody tr:hover td { background: var(--n1); }
.tn table.grid th.num, .tn table.grid td.num {
  text-align: right; font-variant-numeric: tabular-nums lining-nums;
}

/* Залипшая колонка. Класс вешается на конкретные th/td, а НЕ через
   :nth-child — тот зависит от порядка соседей и однажды молча
   перестаёт совпадать. Смещение задаёт страница: оно зависит от
   ширины предыдущих колонок, а не от компонента. */
.tn table.grid th.col-stick, .tn table.grid td.col-stick {
  position: sticky; left: var(--stick-left, 0); z-index: var(--z-sticky);
  background: var(--n0);
}
.tn table.grid thead th.col-stick { background: var(--n1); z-index: 3; }
/* ЧЕРТА СПРАВА ОТ ЛИПКОЙ: у ячейки — псевдоэлементом, у ШАПКИ — тенью. `::after`
   шапки занят стрелкой сортировки (`th[aria-sort]::after`): черта, заняв его,
   уводила стрелку в правый верхний угол шапки и сама пропадала — высота
   у стрелки нулевая. Поймано на «Контрактах», CTR-19 (15.09.2026). */
.tn table.grid td.col-stick::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0;
  width: 1px; background: var(--n3);
}
.tn table.grid th.col-stick { box-shadow: inset -1px 0 var(--n3); }

/* Обрезка длинного значения. max-width на самой ячейке не работает,
   пока table-layout не fixed: ширина колонки считается по содержимому.
   Поэтому ограничивается СОДЕРЖИМОЕ — инлайн-блоком внутри ячейки. */
.tn .clip { display: inline-block; max-width: var(--clip, 20rem);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom; }

/* --- ДОСКА ------------------------------------------------------------
   Карточка на доске — объект, который перетаскивают, поэтому тень
   здесь уместна: она означает «поднято», а не «это отдельный остров». */
.tn .board { display: flex; gap: var(--s3); align-items: flex-start;
  overflow-x: auto; padding-bottom: var(--s3); }
/* ВЫСОТА КОЛОНКИ ОГРАНИЧИВАЕТСЯ, ИНАЧЕ УЕЗЖАЕТ ВСЯ СТРАНИЦА.
   На 445 карточках колонка растёт по содержимому и утаскивает за собой
   шапку экрана и фильтры: прокручиваться должна колонка, а не страница.

   У таблицы для этого есть `--grid-max`, у доски не было ничего —
   и это несимметрично без причины: обе живут в одном месте страницы
   и обе не знают, что над ними (D-118).

   ЗАПОЛНЕНИЕ — СВОЙСТВО ДОСКИ, А НЕ КОЛОНКИ. Первая редакция давала
   колонке `flex: 1 1 auto`, и это было неверно дважды (STRU_magic,
   заявка 24):

   — по высоте не работало: `.board` выравнивает детей `flex-start`,
     то есть колонка не растягивается по поперечной оси вовсе. Снятие
     потолка даёт ей ПРАВО быть высокой, но никто её такой не делает;
   — по ширине ломало верное: `.board` — СТРОЧНЫЙ флекс, значит
     `flex: 1 1 auto` относится к ШИРИНЕ и перебивает
     `flex: 0 0 var(--col-w)`. Класс, заведённый ради высоты, молча
     менял ширину.

   Причина названа ими же: объявление перенесено с `.grid-wrap`,
   лежащего в КОЛОНОЧНОМ флексе, на `.board-col`, лежащий в СТРОЧНОМ.
   Одна запись, две оси — D-057 ещё раз, только механизм здесь даже
   не сменился на другой, сменилось направление флекса.

   Поэтому заполнение объявляет ДОСКА: она задаёт поперечное
   выравнивание и берёт высоту у родителя. Колонка лишь снимает
   свой потолок, ширина не трогается вовсе.

   `.board--fill` без `flex: 1 1 auto` бесполезен: растяжение по
   поперечной оси даёт колонке высоту ДОСКИ, а высота доски по
   содержимому — та же беда этажом выше (D-122). */
.tn .board--fill { align-items: stretch; flex: 1 1 auto; min-height: 0; }
.tn .board-col { flex: 0 0 var(--col-w, 280px); background: var(--n1);
  border: 1px solid var(--n3); border-radius: var(--r-lg);
  display: flex; flex-direction: column; min-height: 0;
  max-height: var(--board-col-max, 70vh); }
.tn .board-col--fill { max-height: none; min-height: 0; }
/* ВНУТРИ ЗАМКНУТОЙ КОРОБКИ РАЗДЕЛИТЕЛЬ НЕ НУЖЕН: колонка уже отделена
   рамкой и фоном, и линия под заголовком делит разделённое. На доске
   из шести колонок это шесть лишних горизонталей в одну строку.
   Заголовок отделяется расстоянием — так же в референсе (D-119). */
.tn .board-col > .head { display: flex; align-items: baseline;
  gap: var(--s2); padding: var(--s3) var(--s3) var(--s2);
  font-size: var(--f-micro); text-transform: uppercase; letter-spacing: .08em;
  color: var(--n6); font-weight: 600; }
.tn .board-col > .head .count { font-variant-numeric: tabular-nums;
  letter-spacing: 0; }
/* ВОЗДУХ МЕЖДУ КАРТОЧКАМИ ВАЖНЕЕ ВОЗДУХА ВНУТРИ, и это не вкус:
   просвет между объектами позволяет их СЧИТАТЬ, внутреннее поле
   только добавляет массы. На восьми карточках разницы не видно,
   на четырёхстах слипание — первое, что замечает глаз (D-097).
   Поэтому растёт `gap`, а `padding` карточки остаётся прежним. */
/* Верхнего поля нет по той же причине, что у тела модала: колонка
   доски прокручивается, и липкая группа карточек в ней — законный
   случай (D-139). */
.tn .board-col > .body { padding: 0 var(--s2) var(--s2); display: flex;
  flex-direction: column; gap: var(--s3); overflow: auto; min-height: 0; }
.tn .board-col > .body > :first-child { margin-top: var(--s2); }
.tn .board-col.is-over { outline: 2px dashed var(--accent); outline-offset: -2px; }

.tn .board-card {
  display: block; text-decoration: none; color: inherit; cursor: pointer;
  background: var(--n0); border: 1px solid var(--n3);
  border-radius: var(--r-md); padding: var(--s2) var(--s3);
  transition: box-shadow var(--dur-fast), border-color var(--dur-fast);
}
.tn .board-card:hover { box-shadow: var(--sh-raised); border-color: var(--n4); }
.tn .board-card[draggable="true"] { cursor: grab; }
.tn .board-card.is-dragging { opacity: .5; }
.tn .board-card .nm { font-size: var(--f-body); font-weight: 600;
  line-height: 1.25; margin-bottom: 2px; }
.tn .board-card .meta { font-size: var(--f-micro); color: var(--n6);
  display: flex; flex-direction: column; gap: 1px; }
.tn .board-card .meta b { color: var(--n7); font-weight: 600; }

/* КАРТОЧКА В ОДНУ СТРОКУ — предел правила объёма, а не другой элемент.
   Имя, точка категории и один факт справа; всё, что не помещается,
   не показывается вовсе, а не переносится (D-097). */
.tn .board-card--line { display: flex; align-items: center;
  gap: var(--s2); padding: var(--s1) var(--s3); }
.tn .board-card--line > .nm { flex: 1 1 auto; min-width: 0;
  margin-bottom: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-weight: 500; }
.tn .board-card--line > .cat { margin-bottom: 0; flex: none; }
.tn .board-card--line > .meta { flex: none; }

/* --- УГОЛ КАРТОЧКИ ----------------------------------------------------
   Правый верхний угол — место действия над карточкой, а не над её
   содержимым: «…», «обновить», «закрыть». Занят он и в референсе,
   и у продукта — значит это место, а не чья-то выдумка (D-097). */
.tn .card-corner { position: absolute; top: var(--s1); right: var(--s1); }
.tn .board-card { position: relative; }

/* --- РЯД ЗНАЧКОВ-ССЫЛОК -----------------------------------------------
   У ряда есть требование, которого нет у одиночной кнопки: ПРОМАХ
   МЕЖДУ ЗНАЧКАМИ НЕ ДОЛЖЕН СРАБАТЫВАТЬ КАК ЩЕЛЧОК ПО КАРТОЧКЕ.
   Значок 16 px в ряду из пяти — промахнуться легче, чем попасть,
   и цена промаха здесь не «ничего», а «открылся модал».

   Язык даёт зону: ряд перехватывает указатель на всей своей площади,
   включая промежутки. Что делать с перехваченным — дело продукта,
   но перехват обязан быть, иначе безопасной зоны нет (D-097). */
/* РЯД ЗНАЧКОВ — ПРИЁМ УКАЗАТЕЛЯ, А НЕ ПАЛЬЦА. Пять целей по 44 px —
   220 px, из которых глифов 80; на карточке в 280 px это дыра, а не
   действия. Увеличить ряд нельзя, а уменьшить цель — значит отменить
   правило тач-габарита.

   Задача не про габарит, а про ЧИСЛО ДЕЙСТВИЙ: пять действий в строке
   карточки — приём для указателя. На тач-экране ряд заменяется одной
   кнопкой меню, и 44 px получает она. Так же поступает референс,
   на который ссылались обе стороны (D-119).

   Поэтому внутри ряда тач-габарит НЕ применяется: ряда там уже нет. */
.tn .icon-row { display: inline-flex; align-items: center;
  gap: var(--s1); padding: var(--s1); margin: calc(var(--s1) * -1);
  border-radius: var(--r-sm); }
@media (pointer: coarse) {
  .tn .icon-row { display: none; }
  .tn .icon-row + .icon-menu { display: inline-flex; }
}
.tn .icon-menu { display: none; }

/* ЗНАЧОК ВНУТРИ КНОПКИ ПОЛУЧАЕТ РАЗМЕР ОТ КНОПКИ, А НЕ ОТ ФАЙЛА.
   `.icon-btn` рассчитывался на глиф и потомкам размера не задавал.
   Картинка с собственными размерами нарисовалась в натуральную
   величину и разнесла карточку — у STRU_magic значки оказались
   крупнее названия проекта.

   Это дефект языка, а не их случай: значок-ссылка на картинке — то,
   чем набираются ряды действий в любой карточке (D-097). */
.tn .icon-btn > svg, .tn .icon-btn > img,
.tn .icon-row > a > svg, .tn .icon-row > a > img {
  display: block; width: 16px; height: 16px; flex: none; }

/* --- СОДЕРЖИМОЕ КАРТОЧКИ --------------------------------------------
   Строение подсмотрено в ERP-концепте SITEON: категория, главная
   величина, строки фактов, полоса, итог за разделителем. Оно делает
   карточку читаемой без открытия — но стоит вертикали.

   ПРАВИЛО ОБЪЁМА: чем больше карточек на доске, тем меньше внутри.
     до ~8 карточек   — можно всё: величина, факты, полоса, итог
     8–20             — величина и одна-две строки фактов
     20–100           — имя, категория и один факт
     больше 100       — карточка в ОДНУ СТРОКУ (`.board-card--line`)
   Двенадцать одинаково устроенных карточек — таблица, притворяющаяся
   доской: колонку значений в ней глазами не просканировать.

   ФОРМУЛИРОВКА «больше 20 — это реестр» БЫЛА НЕВЕРНОЙ (D-097).
   Она отвечала на вопрос «сколько влезает внутрь», а звучала как ответ
   на «сколько бывает карточек». Доска остаётся доской, пока колонки
   несут смысл, а перенос карточки МЕНЯЕТ ДАННЫЕ: у STRU_magic 445
   карточек в колонке, и «сделайте реестр» отняло бы у них операцию,
   ради которой экран существует.

   Правило доведено до предела вместо отмены: колонка выше экрана
   перестаёт быть колонкой и становится списком, а карточка в списке —
   строкой. Меньше строки внутри не бывает, поэтому четвёртая ступень
   последняя.

   СЧЁТЧИК В ШАПКЕ КОЛОНКИ ОБЯЗАТЕЛЕН при любом объёме: длину списка,
   который не помещается, иначе узнать неоткуда.                    */

/* Категория над названием: точка и слово, не плашка. */
.tn .board-card .cat { display: flex; align-items: center; gap: 5px;
  font-size: var(--f-micro); color: var(--n6); margin-bottom: var(--s1); }
.tn .board-card .cat .sw { width: 6px; height: 6px; border-radius: 50%; }

/* Главная величина карточки: подпись слева, число справа. */
.tn .board-card .lead { display: flex; align-items: baseline;
  justify-content: space-between; gap: var(--s3); margin: var(--s1) 0; }
.tn .board-card .lead .k { font-size: var(--f-body); color: var(--n7); }
.tn .board-card .lead .v { font-size: var(--f-lead); font-weight: 600;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums lining-nums; }
/* У КАРТОЧКИ РОВНО ОДНА ГЛАВНАЯ ВЕЛИЧИНА, И ЕСЛИ ЧИСЛА НЕТ — ЕЮ
   СТАНОВИТСЯ ИМЯ. Референс держится на числе; карточка проекта,
   задачи, контрагента или документа числа не имеет вовсе, и без
   главной величины получается набор одинаковых серых строк, который
   глазами не сканируется.

   `.lead--text` снимает табличные цифры и разрядку — они для чисел, —
   но сохраняет вес. Второй «главной» величины не бывает: две главные
   означают, что главной нет (D-097). */
.tn .board-card .lead--text .v { font-variant-numeric: normal;
  letter-spacing: 0; font-size: var(--f-body); text-align: right; }
/* Отсутствующее значение приглушается, а не набирается наравне:
   на живых данных половина фактов — «НЕТ», и жирные «НЕТ» кричат
   громче названий. Тот же разбор, что у `.pair > .v.is-empty`. */
.tn .board-card .facts dd.is-empty { color: var(--n6); font-weight: 400; }

/* Строки фактов. Числа выровнены по правому краю — их сравнивают
   между собой внутри карточки. */
.tn .board-card .facts { display: grid; grid-template-columns: 1fr auto;
  gap: 1px var(--s3); font-size: var(--f-micro); }
.tn .board-card .facts dt { color: var(--n6); }
.tn .board-card .facts dd { margin: 0; text-align: right; color: var(--n7);
  font-variant-numeric: tabular-nums lining-nums; }

/* Полоса с подписями концов: без них проценту не с чем соотноситься. */
.tn .meter { margin: var(--s2) 0 0; }
.tn .meter > .track { height: 4px; border-radius: 2px; background: var(--n3);
  overflow: hidden; }
.tn .meter > .track > i { display: block; height: 100%; background: var(--d1); }
.tn .meter > .ends { display: flex; justify-content: space-between;
  margin-top: 2px; font-size: var(--f-micro); color: var(--n6);
  font-variant-numeric: tabular-nums; }

/* РАЗДЕЛИТЕЛЬ ПРИНАДЛЕЖИТ ИТОГУ, А НЕ ПОДВАЛУ. Описание гласило «итог
   за разделителем», а линия стояла у `.foot` — то есть у МЕСТА, а не
   у того, ради чего линия заведена.

   Нижняя строка есть у карточки и без числового итога: ряд действий,
   идентификатор, дата. Там разделять нечего, и линия становится вторым
   лишним горизонталем рядом с рамкой карточки (D-119).

   Линию даёт `.foot--total` — там, где внизу действительно итог. */
.tn .board-card .foot { display: flex; align-items: baseline;
  justify-content: space-between; gap: var(--s3);
  margin-top: var(--s2); font-size: var(--f-body); }
.tn .board-card .foot--total { padding-top: var(--s2);
  border-top: 1px solid var(--n3); }
.tn .board-card .foot .k { color: var(--n7); }
.tn .board-card .foot .v { font-weight: 600;
  font-variant-numeric: tabular-nums lining-nums; }

/* --- МОДАЛ ------------------------------------------------------------ */
/* ПРОКРУЧИВАЕТСЯ МОДАЛ, А НЕ ПОДЛОЖКА. До 0.58 областью прокрутки была
   подложка, а шапка липла `position: sticky; top: 0`. Липкость
   отсчитывается от ПОЛЯ области, а обрезка идёт по её КРАЮ, и между
   ними ровно поле подложки: содержимое, уехавшее под шапку, продолжало
   рисоваться НАД ней полосой в 6vh (STRU_magic, заявка 28, замер
   в Chromium: шапка на y=48, тело на y=-283 и видно).

   Отказ бесшумный и выглядит небрежностью вёрстки — они трижды чинили
   его как внешний вид, потому что искали причину у себя, а её там
   не было.

   ЛИПКОСТЬ УБРАНА, А НЕ ПОДКРАШЕНА. Когда прокручивается тело, шапка
   вообще не участвует в прокрутке: липнуть незачем, обрезать нечего,
   выше неё содержимого не существует. Величина устранена, а не
   исправлена — это тот же ход, что `--doc-top` у них же (D-134).

   `--modal-inset` — ОДНА величина: поле подложки и то, что вычитается
   из высоты модала. Записанные порознь, 6vh и 12vh разошлись бы при
   первой правке. */
.tn .modal-backdrop {
  position: fixed; inset: 0; background: rgba(16,16,22,.42);
  display: flex; align-items: flex-start; justify-content: center;
  padding: var(--modal-inset) var(--s4); z-index: var(--z-modal);
  overflow-y: auto;
  animation: tn-fade var(--dur-base) ease-out;
}
.tn .modal {
  background: var(--surface-raised); border: 1px solid var(--n3);
  border-radius: var(--r-lg); box-shadow: var(--sh-modal);
  width: 100%; max-width: 720px;
  max-height: calc(100dvh - var(--modal-inset) * 2);
  display: flex; flex-direction: column; min-height: 0;
  animation: tn-rise var(--dur-slow) var(--ease-out);
}
.tn .modal > .body { flex: 1 1 auto; min-height: 0; overflow: auto; }
/* МОДАЛ ВО ВЕСЬ ЭКРАН — не «побольше», а другой случай: внутри лежит
   то, у чего своя ширина. Таблица на 35 колонок в 720 px рвёт
   заголовки по буквам, и это не теснота, а нечитаемость.

   Поля по краям остаются: модал во весь экран без полей перестаёт
   читаться как слой над страницей и становится страницей — а закрыть
   страницу нечем (D-097).

   Поле берётся ОТ ПОДЛОЖКИ, а не своё: `--s6` по вертикали и `6vh`
   у подложки — две записи одного зазора, и модал во весь экран
   вылезал бы за неё при другой высоте окна. */
.tn .modal--full { max-width: none; width: calc(100vw - var(--s4) * 2);
  height: calc(100dvh - var(--modal-inset) * 2); }
/* ШАПКА — ЗОНА, А НЕ СТРОКА. Одна строка была умолчанием, а не
   решением: `.head` объявлен флексом по базовой линии, и второй ярус
   в нём встаёт рядом, а не под. Продукту с трёхъярусной шапкой
   (заголовок · состояние и действия · разделы) приходилось держать
   свою липкую полосу и ИЗМЕРЯТЬ высоту чужой шапки.

   Ярусы узнаются по структуре: `.line` внутри шапки — то же имя, что
   у строки шапки реестра. Разметка без `.line` работает как прежде,
   поэтому модификатора нет: он был бы вторым способом сказать
   то самое, что уже сказано содержимым (D-135). */
.tn .modal > .head { display: flex; align-items: baseline; gap: var(--s2);
  padding: var(--s3) var(--s4); border-bottom: 1px solid var(--n3);
  background: var(--n0); flex: none;
  border-radius: var(--r-lg) var(--r-lg) 0 0; }
.tn .modal > .head:has(> .line) { flex-direction: column;
  align-items: stretch; gap: var(--s2); }
.tn .modal > .head > .line { display: flex; align-items: baseline;
  gap: var(--s2); }
.tn .modal > .foot { flex: none; }
/* У ОБЛАСТИ ПРОКРУТКИ НЕ БЫВАЕТ ВЕРХНЕГО ПОЛЯ. Липкий элемент внутри
   прилипает к краю СОДЕРЖИМОГО, а обрезка идёт по краю ОБЛАСТИ:
   в полосе высотой в это поле содержимое продолжает ехать и остаётся
   видимым НАД липким. Замер STRU_magic: тело на y=229 при поле 16px,
   липкий заголовок на y=245 — «залип раньше времени».

   Это D-134 этажом ниже: там поле было 6vh у подложки, здесь 16px
   у тела, и второй раз уцелел потому, что чинилось МЕСТО, А НЕ ФОРМА.

   Верхнее поле отдано содержимому: первый ребёнок получает отступ
   сам. Боковое и нижнее остаются — по вертикали сверху ограничивает
   липкость, и только там (D-139). */
.tn .modal > .body { padding: 0 var(--s4) var(--s4); }
.tn .modal > .body > :first-child { margin-top: var(--s4); }
/* ДЕТИ ПОДВАЛА ВЫРАВНИВАЮТСЯ ПО ЦЕНТРУ. По умолчанию флекс их
   растягивает: текст садится по верхнему краю, подписи кнопок — по
   центру своей высоты, и строка пляшет. Умолчание `stretch` верно для
   колонок и почти никогда — для строки из разнородных детей (D-118). */
.tn .modal > .foot { display: flex; gap: var(--s2); justify-content: flex-end;
  align-items: center;
  padding: var(--s3) var(--s4); border-top: 1px solid var(--n3); }
@keyframes tn-fade { from { opacity: 0 } to { opacity: 1 } }
@keyframes tn-rise { from { opacity: 0; transform: translateY(8px) }
                     to   { opacity: 1; transform: none } }

/* --- ТАБЫ СОДЕРЖИМОГО -------------------------------------------------
   Переключатель — существующий .seg. Отдельный класс, а не inline
   display:none: панель может понадобиться в разметке (счётчики,
   предзагрузка), скрыт только показ.                                */
.tn .panel[hidden] { display: none; }

/* --- ЗАГРУЗКА И ПУСТОТА ----------------------------------------------- */
.tn .spinner { width: 14px; height: 14px; display: inline-block;
  border: 2px solid var(--n3); border-top-color: var(--n7);
  border-radius: 50%; animation: tn-spin .6s linear infinite; }
@keyframes tn-spin { to { transform: rotate(360deg) } }

/* Пустое состояние ЗАМЕНЯЕТ элемент, а не живёт внутри него: у пустой
   таблицы убираются и заголовки колонок, и подвал, иначе скринридер
   зачитывает всю пустую таблицу. */
.tn .empty { padding: var(--s8) var(--s5); text-align: center;
  color: var(--n6); font-size: var(--f-small); }
.tn .empty > .t { font-size: var(--f-body); color: var(--n7);
  font-weight: 600; margin-bottom: var(--s1); }

/* --- УТИЛИТЫ ТЕКСТА ---------------------------------------------------- */
.tn .muted { color: var(--n6); }
.tn .ok    { color: var(--st-ok); }
.tn .danger{ color: var(--st-unusable); }

/* ====================================================================
   МЕНЬШЕ ДВИЖЕНИЯ — НЕ ЗНАЧИТ БЕЗ ОТКЛИКА (D-145)

   Раньше здесь стоял глобальный сброс всего в 1ms !important. Он
   выключал ровно то движение, которое несёт смысл: §6 языка говорит
   «движение подтверждает действие пользователя», и подтверждение
   нажатия — единственная анимация у контрола. Просьба «меньше
   движения» отменяет не отклик, а вестибулярную нагрузку: крупные
   перемещения, въезды и параллакс.

   Поэтому разделено по ПРИРОДЕ движения, а не по его длительности:

     остаётся   — цвет, рамка, тень, галочка, поворот раскрывателя,
                  сжатие при нажатии. Это отклик: он на месте
                  и в пределах элемента.
     заменяется — перемещение крупной поверхности: панель навигации
                  и въезд модала. Проявление вместо въезда.
     остаётся   — вращение индикатора загрузки. Оно сообщает, что
                  работа идёт; остановленный индикатор врёт.
   ==================================================================== */
@media (prefers-reduced-motion: reduce) {
  /* Модал появляется, а не въезжает: сдвиг убран, проявление оставлено. */
  @keyframes tn-rise { from { opacity: 0 } to { opacity: 1 } }

  /* Панель навигации — в frame.css, внутри её собственной точки
     перелома: здесь правило накрыло бы и десктоп, где панель —
     обычная колонка, и спрятало бы её совсем. Точка перелома
     объявлена один раз, и повторять её число тут нельзя. */
}

/* ====================================================================
   КАРКАС ПРИЛОЖЕНИЯ И НЕДОСТАЮЩЕЕ ДЛЯ ОТЧУЖДЕНИЯ (версия 0.2)
   Раньше каркас жил внутри превью FinanceWatcher. Продукт, берущий
   язык со стороны, не должен верстать боковую навигацию заново —
   иначе первое же чужое применение начнётся с изобретения.
   ==================================================================== */

/* --- КАРКАС ----------------------------------------------------------
   min-height: 0 в каждом звене обязателен: без него flex- и grid-элемент
   не сжимается ниже содержимого, распирает страницу, и вместо прокрутки
   колонок начинает ездить весь документ вместе с шапкой.             */
/* КАРКАС — ЕДИНСТВЕННЫЙ КОМПОНЕНТ, КОТОРЫЙ МОЖЕТ ОКАЗАТЬСЯ КОРНЕМ
   СКОУПА. `<body class="tn app">` — законная и естественная разметка,
   но `.tn .app` её НЕ ловит: это потомок, а тут один элемент. Отказ
   бесшумный — панель и содержимое просто встают друг под другом,
   как блоки без сетки. Поймано в собственном превью языка, где оно
   держалось с той версии, когда планировка переехала из превью
   в `frame.css` (D-124).

   ВЫСОТА БЕРЁТСЯ ОТ ЭКРАНА, А НЕ ОТ ЦЕПОЧКИ РОДИТЕЛЕЙ. `height: 100%`
   требовал `html, body { height: 100% }` — условия, о котором язык
   сказать не может (он скоупирован ниже) и которое отказывает молча:
   липкость объявлена и не работает. `100dvh` спрашивает устройство
   напрямую; `--app-h` оставлен для случая, когда каркас вложен
   в размеренный контейнер — витрина стайлгайда как раз такой (D-125). */
:is(.tn .app, .tn.app) {
  display: grid; grid-template-columns: var(--side-w) minmax(0,1fr);
  grid-template-rows: auto minmax(0, 1fr);
  /* ДВЕ ЗАПИСИ ПОДРЯД — НЕ ОПЕЧАТКА. Браузер, не знающий `dvh`,
     отбрасывает второе объявление целиком, и высота осталась бы
     `auto`: каркас сложился бы по содержимому — то есть отказ был бы
     полным и бесшумным. Первая строка ловит этот случай, вторая
     уточняет для тех, кто умеет (D-127). */
  height: var(--app-h, 100vh);
  height: var(--app-h, 100dvh); background: var(--n2); }
/* РАЗМЕЩЕНИЕ ЯВНОЕ, А НЕ САМОТЁКОМ. Автоматическая раскладка сетки
   переставляет всё, когда сосед скрыт, — этим уже поймало таймлайн
   при схлопывании таблицы (D-110). Здесь скрываемых соседей два:
   `.app-top` на широком экране и `.app-side` на узком. */
.tn .app-top  { grid-column: 1; grid-row: 1; }
.tn .app-side { grid-column: 1; grid-row: 2; }
.tn .app-main { grid-column: 2; grid-row: 1 / -1; }
:is(.tn .app--nonav, .tn.app--nonav) { grid-template-columns: minmax(0,1fr); }
:is(.tn .app--nonav, .tn.app--nonav) :is(.app-top, .app-main) {
  grid-column: 1; }
.tn .app-side { background: var(--n1); border-right: 1px solid var(--n3);
  display: flex; flex-direction: column; overflow: auto; min-height: 0; }
.tn .app-main { display: flex; flex-direction: column; min-width: 0;
  min-height: 0; overflow: hidden; background: var(--n0); }
/* Полоса состояния: всё, что описывает САМИ данные — дата, курс, возраст
   сверки, — живёт здесь, а не повторяется в каждой строке. */
.tn .app-bar { display: flex; align-items: center; gap: var(--s4);
  flex-wrap: wrap; padding: var(--s2) var(--s5);
  border-bottom: 1px solid var(--n3); background: var(--n1);
  font-size: var(--f-small); color: var(--n7); min-height: var(--tap-min); }
.tn .app-bar .fact { display: flex; align-items: baseline; gap: var(--s1); }
.tn .app-bar .fact b { font-weight: 600; color: var(--n9);
  font-variant-numeric: tabular-nums; }
.tn .vr { width: 1px; height: var(--s4); background: var(--n3); flex: none; }
.tn .grow { flex: 1; }
/* Область содержимого прокручивается сама. */
.tn .app-body { flex: 1; min-height: 0; overflow: auto; }

/* --- ПОЛОСА ПРИЛОЖЕНИЯ -----------------------------------------------
   ДВЕ ВЕРХНИЕ ПОЛОСЫ — НЕ ОДНА. `.app-bar` говорит о ДАННЫХ (дата,
   курс, возраст сверки) и живёт внутри `.app-main`. `.app-top`
   говорит о ПРИЛОЖЕНИИ — марка и вход в навигацию — и живёт в самом
   каркасе. Слить их значило бы сказать, что курс валют и название
   продукта одного рода.

   ПОЛОСА ЕСТЬ НА ОБОИХ ШИРИНАХ, МЕНЯЕТСЯ ТОЛЬКО ЕЁ ПРОТЯЖЁННОСТЬ:
   на широком экране она занимает колонку навигации и читается как
   шапка боковой панели, на узком — тянется во всю ширину и держит
   кнопку. Это ответ на «узкому экрану нужна не только кнопка,
   но и поверхность под неё» (STRU_magic, заявка 24), и ответ именно
   такой, потому что вторая поверхность означала бы вторую марку
   в разметке — то есть одну величину в двух местах, а она расходится
   всегда.

   Поэтому `.brand` переезжает из `.app-side` в `.app-top`. Старая
   разметка продолжает работать: `.brand` — это набор, а не место,
   и внутри панели он выглядит как прежде. Но кнопки навигации
   на узком экране у такой разметки не будет (D-123).            */
.tn .app-top { display: flex; align-items: center; gap: var(--s2);
  background: var(--n1); border-right: 1px solid var(--n3);
  min-height: var(--tap-min); }
.tn .app-top > .brand { flex: 1 1 auto; min-width: 0; }
/* Кнопка навигации существует только там, где навигация спрятана.
   На широком экране панель на экране — кнопка «показать» была бы
   кнопкой без действия. */
.tn .nav-toggle { display: none; flex: none; margin-left: var(--s3); }
/* ЗАТЕМНЕНИЕ — КНОПКА, А НЕ СЛОЙ. Щелчок мимо панели закрывает её
   у всех, кто такие панели видел; если это делает `<div>`, действие
   недоступно ни с клавиатуры, ни скринридеру, и закрыть панель
   становится нечем. Поэтому элемент — `<button>` с именем. */
.tn .nav-scrim { display: none; position: fixed; inset: 0;
  z-index: calc(var(--z-modal) - 1); border: none; border-radius: 0;
  padding: 0; min-height: 0; background: rgba(16,16,22,.42);
  animation: tn-fade var(--dur-base) ease-out; cursor: default; }

/* --- НАВИГАЦИЯ -------------------------------------------------------- */
.tn .brand { padding: var(--s4) var(--s5) var(--s3);
  font-size: var(--f-body); font-weight: 600; }
.tn .brand small { display: block; font-weight: 400; color: var(--n6);
  font-size: var(--f-micro); margin-top: var(--s1); }
.tn .nav-group { padding: var(--s2) var(--s3);
  border-top: 1px solid var(--n3); }
.tn .nav-group > .t-label { margin: 0 0 var(--s2) var(--s2); display: block; }
.tn .nav-link { display: flex; align-items: center;
  justify-content: space-between; gap: var(--s2);
  padding: var(--s1) var(--s2); border-radius: var(--r-sm);
  font-size: var(--f-body); color: var(--n7); text-decoration: none;
  cursor: pointer; min-height: var(--row-h); }
.tn .nav-link:hover { background: var(--n2); color: var(--n9); }
/* ТЕКУЩИЙ РАЗДЕЛ — РОЛЬ ПОМЕТКИ, И ПОЛОЖЕНИЕ КОДИРУЕТСЯ НЕ ТОЛЬКО ЕЮ.
   Умолчание равно акценту, которым это место красилось всегда: акцент
   означает «здесь ты сейчас». Продукт вправе поставить сюда свой цвет
   тождества — §2а это разрешает, — но `font-weight: 600` остаётся
   вторым каналом и снимать его нельзя: положение, закодированное
   одним цветом, теряется в повышенном контрасте и на печати. */
.tn .nav-link[aria-current="true"], .tn .nav-link[aria-current="page"] {
  background: var(--brand-mark); color: var(--brand-mark-ink);
  font-weight: 600; }
.tn .nav-link .badge { font-size: var(--f-micro); color: var(--n6);
  font-variant-numeric: tabular-nums; }
.tn .nav-link[aria-current] .badge { color: inherit; }
.tn .nav-foot { margin-top: auto; padding: var(--s3) var(--s5);
  border-top: 1px solid var(--n3); font-size: var(--f-small);
  color: var(--n6); }

/* --- СЕКЦИЯ И ПОВЕРХНОСТЬ ---------------------------------------------
   .section — обычный способ разделить содержимое: линия, не плитка.
   .surface — контейнер там, где он ДЕЙСТВИТЕЛЬНО нужен: отдельно стоящая
   форма входа, узкая страница профиля. Рамка, а не тень: тень означает
   всплытие, а страница ни над чем не всплывает.                      */
.tn .section { padding: var(--s5); border-bottom: 1px solid var(--n3); }
.tn .section:last-child { border-bottom: none; }
.tn .section > .inner { max-width: var(--measure); }
.tn .surface { background: var(--n0); border: 1px solid var(--n3);
  border-radius: var(--r-lg); padding: var(--s5); }
/* Форма шириной с содержимое, а не с меру строки: карточка входа
   в 760 px втрое шире того, что в ней лежит. */
.tn .surface--form { width: 100%; max-width: var(--form-w); }

/* --- ПАНЕЛЬ ИНСТРУМЕНТОВ ---------------------------------------------- */
.tn .toolbar { display: flex; align-items: center; gap: var(--s2);
  flex-wrap: wrap; padding-bottom: var(--s3);
  border-bottom: 1px solid var(--n3); }
.tn .toolbar .grow { flex: 1; }

/* --- МЕНЮ / ПОПОВЕР ---------------------------------------------------
   Основа выпадающих списков и мультиселектов. Тень уместна: меню
   действительно всплывает над содержимым.                            */
.tn .menu { min-width: 200px; background: var(--surface-raised);
  border: 1px solid var(--n3); border-radius: var(--r-md);
  box-shadow: var(--sh-popover); padding: var(--s1);
  z-index: var(--z-popover); }
.tn .menu-item { display: flex; align-items: center; gap: var(--s2);
  width: 100%; padding: var(--s1) var(--s2); border-radius: var(--r-sm);
  font-size: var(--f-body); color: var(--n9); background: none;
  border: none; cursor: pointer; text-align: left; min-height: 28px; }
.tn .menu-item:hover { background: var(--n1); border-color: transparent; }
.tn .menu-item[aria-selected="true"] { background: var(--accent-soft);
  color: var(--accent); }
.tn .menu-sep { height: 1px; background: var(--n3); margin: var(--s1) 0; }

/* --- КНОПКА-ИКОНКА ----------------------------------------------------- */
.tn .icon-btn { display: inline-flex; align-items: center;
  justify-content: center; width: 26px; height: 26px; padding: 0;
  background: transparent; border-color: transparent; color: var(--n6);
  line-height: 1; }
.tn .icon-btn:hover { background: var(--n2); color: var(--n9);
  border-color: transparent; }

/* --- ССЫЛКА ------------------------------------------------------------ */
.tn .link { color: var(--accent); text-decoration: none;
  border-bottom: 1px solid transparent; }
.tn .link:hover { border-bottom-color: currentColor; }

/* --- СВОДКА ЧИСЕЛ ------------------------------------------------------
   Ряд показателей, разделённых линиями, а не карточками. Подпись
   мелким капсом сверху, величина под ней.                            */
.tn .stats { display: flex; flex-wrap: wrap; }
.tn .stat { flex: 1 1 180px; padding: var(--s3) var(--s4);
  border-right: 1px solid var(--n3); }
.tn .stat:last-child { border-right: none; }
.tn .stat > .k { font-size: var(--f-micro); text-transform: uppercase;
  letter-spacing: .08em; color: var(--n6); font-weight: 600;
  margin-bottom: var(--s1); }
.tn .stat > .v { font-size: var(--f-lead); font-weight: 600;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums lining-nums; }

/* --- ЛИПКАЯ СТОПКА ----------------------------------------------------
   Когда залипающих уровней больше одного (шапка страницы, заголовок
   раздела, шапка таблицы), каждый обязан считать свой `top` ОТ ОБЩЕЙ
   переменной, а не сам по себе. Иначе уровни наезжают друг на друга,
   а между ними остаются щели в 1–2 px со скользящим содержимым.

   Страница задаёт --stack один раз; каждый следующий уровень прибавляет
   свою высоту и прилипает на 1px выше суммы — перекрытие прячет
   субпиксельный шов при дробном devicePixelRatio.                   */
.tn .stick-1 { position: sticky; top: calc(var(--stack, 0px) - 1px);
  z-index: var(--z-sticky); background: var(--n0); }
.tn .stick-2 { position: sticky;
  top: calc(var(--stack, 0px) + var(--stack-1, 0px) - 1px);
  z-index: calc(var(--z-sticky) - 1); background: var(--n1); }

/* ====================================================================
   МУЛЬТИСЕЛЕКТ (версия 0.2)
   Фильтр по многозначному полю: институт, владелец, валюта, статус.
   Нужен любому реестру, поэтому живёт в языке, а не в продукте.

   Почему не нативный <select multiple>: он показывает список без
   поиска, не даёт счётчика выбранного, по-разному выглядит в каждой
   системе и на тач-экранах открывается системным листом, куда нельзя
   добавить поиск. При двадцати институтах это неработоспособно.
   ==================================================================== */

.tn .ms { position: relative; display: inline-block; }

/* Кнопка-триггер выглядит как поле, а не как кнопка: это ввод значения. */
.tn .ms-btn {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: 26px; max-width: 100%;
  padding: 3px var(--s2); font-size: var(--f-small);
  background: var(--n0); color: var(--n9);
  border: 1px solid var(--n4); border-radius: var(--r-sm);
  cursor: pointer; text-align: left;
}
.tn .ms-btn:hover { border-color: var(--n6); }
.tn .ms-btn[aria-expanded="true"] { border-color: var(--accent); }
.tn .ms-btn > .val { overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
/* Пока не выбрано ничего — подпись приглушена, как placeholder. */
.tn .ms-btn.is-empty > .val { color: var(--n6); }
/* Счётчик выбранного. Перечислять двадцать значений в кнопке нельзя:
   она станет шире таблицы. Первое значение плюс «+N» — читается сразу. */
.tn .ms-btn > .n {
  font-size: var(--f-micro); font-variant-numeric: tabular-nums;
  background: var(--accent-soft); color: var(--accent);
  border-radius: var(--r-sm); padding: 0 5px; flex: none;
}
.tn .ms-btn > .arrow { margin-left: auto; color: var(--n6); flex: none;
  font-size: 9px; }

/* Всплывающая часть — на общем .menu, чтобы тень, радиус и слой
   не расходились с остальными поповерами. */
.tn .ms-pop {
  position: absolute; top: calc(100% + var(--s1)); left: 0;
  min-width: 240px; max-width: 340px;
}
.tn .ms-pop[hidden] { display: none; }
.tn .ms-search { width: 100%; margin-bottom: var(--s1); }
.tn .ms-list { max-height: 260px; overflow: auto; }
/* Пункт — это .menu-item плюс чекбокс: выбор множественный, и это
   должно быть видно ДО открытия, а не после первого клика. */
.tn .ms-list .menu-item { justify-content: flex-start; }
.tn .ms-list .menu-item .cnt { margin-left: auto; color: var(--n6);
  font-size: var(--f-micro); font-variant-numeric: tabular-nums; }
.tn .ms-foot { display: flex; align-items: center; gap: var(--s2);
  border-top: 1px solid var(--n3); margin-top: var(--s1);
  padding-top: var(--s1); }
.tn .ms-foot .grow { flex: 1; }
.tn .ms-empty { padding: var(--s3) var(--s2); text-align: center;
  color: var(--n6); font-size: var(--f-small); }

/* ====================================================================
   ПЕРЕТАСКИВАНИЕ И ЕГО ЗАПАСНОЙ ПУТЬ
   HTML5 drag-and-drop НЕ РАБОТАЕТ на тач-экранах. Это не предположение:
   проверено в STRU_magic на живых устройствах. Поэтому запасной путь —
   часть компонента, а не недоделка: на тач-экране статус меняется
   селектом внутри карточки.

   Переключение по типу указателя, а не по ширине окна: планшет
   с большим экраном — всё ещё палец, а узкое окно на десктопе —
   всё ещё мышь.

   С 0.7 — `any-pointer`: у ноутбука с тачскрином основной указатель
   мышь, но перетаскивать пальцем там всё равно нечем, и запасной способ
   переместить карточку обязан быть доступен.
   ==================================================================== */

.tn .board-card .move { display: none; margin-top: var(--s2); }
.tn .board-card .move select { width: 100%; font-size: var(--f-micro); }

/* Запасной способ переместить карточку показывается, если ХОТЯ БЫ ОДИН
   способ ввода неточен: на ноутбуке с тачскрином перетаскивать пальцем
   нечем, даже когда основной указатель — мышь. */
@media (any-pointer: coarse) {
  .tn .board-card .move { display: block; }
}

/* А курсор «схватить» убирается только там, где мыши НЕТ ВООБЩЕ.
   Признаки разные, потому что вопросы разные: «доступен ли запасной
   способ» и «обманывает ли курсор». Свести их в один — то же
   склеивание, что было в аудите (D-033). */
@media (pointer: coarse) {
  .tn .board-card[draggable="true"] { cursor: default; }
}

/* Целевая колонка подсвечивается пунктиром — заливка при перетаскивании
   скрыла бы карточки, которые в ней уже лежат. */
.tn .board-col.is-over { outline: 2px dashed var(--accent);
  outline-offset: -2px; }
.tn .board-col.is-over > .head { color: var(--accent); }


/* ====================================================================
   SVG (версия 0.5)
   Заведено по заявке TN Gantt: диаграмма — SVG, и без этого «значений
   мимо токенов ноль» достигалось бы тем, что SVG просто не сканируется.

   ЧТО СЧИТАЕТСЯ ЯЗЫКОМ: цвет, толщина линии, штрих достоверности,
   правила состояний данных. Геометрия — положение, длина, координаты —
   ВЁРСТКА ПРОДУКТА: она вычисляется из его данных, и язык о ней
   ничего сказать не может.
   ==================================================================== */

/* Корневой элемент диаграммы. Наследование цвета из CSS в SVG работает,
   но fill по умолчанию чёрный, а не currentColor — задаём явно. */
.tn .chart { display: block; fill: currentColor; color: var(--n9); }

/* Линии сетки, осей и связей. Цвет — из нейтральной шкалы, потому что
   сетка не данные. */
.tn .chart .grid-line   { stroke: var(--n3); stroke-width: var(--line-w); fill: none; }
.tn .chart .axis-line   { stroke: var(--n4); stroke-width: var(--line-w); fill: none; }
/* `.link-line` ВНЕ ГРУППЫ — линия диаграммы: тренд, факт, оценка, план.
   Её вес — `--line-w-strong`, тот же, что у оси и границы периода.
   ВНУТРИ группы `.link` та же линия — связь, и вес у неё свой (D-190):
   одно имя, два предмета, и различает их вложенность, а не модификатор. */
/* МОДИФИКАТОР ПЕРЕЧИСЛЕН ЗДЕСЬ ЖЕ, А НЕ ОСТАВЛЕН НАСЛЕДОВАТЬ (D-194).
   `.link-line--active` объявлял один `stroke`, а `fill: none` брал
   от базового класса — которого на витрине не оказалось. `<polyline>`
   по умолчанию ЗАЛИВАЕТСЯ и замыкается: линия тренда превращалась
   в синюю линзу. Правило, которое держится на дисциплине разметки,
   не правило: продукт напишет один класс из двух и получит кляксу. */
.tn .chart .link-line,
.tn .chart .link-line--active { stroke: var(--n6);
                          stroke-width: var(--line-w-strong);
                          fill: none; }
.tn .chart .link .link-line, .tn .tl-links .link .link-line {
  stroke-width: var(--link-w); }
.tn .chart .link-line--active { stroke: var(--accent); }

/* --- СЛОЙ СВЯЗЕЙ ------------------------------------------------------
   СВЯЗЬ — ГРУППА, А НЕ ЭЛЕМЕНТ, и это главное здесь. Линия и торец
   обязаны быть одного цвета всегда; если цвет объявлен у обоих,
   он объявлен дважды и разойдётся (`--active` красит линию и оставляет
   серый наконечник — TN Gantt назвал этот отказ заранее).

   `stroke` в SVG НАСЛЕДУЕТСЯ. Значит цвет объявляется один раз —
   на группе, — а торцы его получают и спорить им нечем (D-128).

   ТОРЕЦ РИСУЕТСЯ ОБВОДКОЙ, А НЕ ЗАЛИВКОЙ И НЕ `<marker>`. Маркер
   пришлось бы заводить по экземпляру на каждый цвет: `marker-end`
   ссылается на `<defs>` по имени, а `<marker>` не знает, какой линией
   его позвали. `fill: context-stroke` это чинит там, где поддержан,
   и молча не чинит там, где нет. Ус, нарисованный тем же штрихом,
   не может разойтись с линией по построению.                     */
/* ЦВЕТ ОБЪЯВЛЕН ОДИН РАЗ — И ТЕПЕРЬ ЭТО `color`, А НЕ `stroke` (D-191).
   D-128 запретил заливку торцов, и довод был про `<marker>`
   и `fill: context-stroke`: цвет мог разойтись с линией. Довод верен
   и к этому случаю не относится — TN Gantt указал точно: путь
   с заливкой рисует тот же продукт, что и линию.

   Но гарантию D-128 надо сохранить, а не отменить. Она сохраняется
   так: группа объявляет `color`, линия берёт `stroke: currentColor`,
   торец — `fill: currentColor`. Цвет по-прежнему один, и разойтись
   нечему по построению — просто теперь его берут двумя способами
   вместо одного. */
.tn .chart .link, .tn .tl-links .link {
  color: var(--n6); stroke: currentColor; stroke-width: var(--link-w);
  fill: none; stroke-linecap: round; stroke-linejoin: round; }
.tn .chart .link *, .tn .tl-links .link * { stroke: inherit; fill: none; }
.tn .chart .link--active, .tn .tl-links .link--active { color: var(--accent); }
/* ТОРЕЦ ЛИТОЙ, ЛИНИЯ ШТРИХОВАЯ — И ЭТО НЕ РАЗНЫЕ СПОСОБЫ РИСОВАНИЯ,
   А РАЗНЫЕ ПРЕДМЕТЫ. Ус из двух отрезков той же толщины, что линия,
   сходящихся под углом, читается как утолщение линии, а не как остриё:
   замер TN Gantt на живом полотне, где вертикаль связи стоит рядом.

   Литые ВСЕ ТРИ, и это ответ на их же возражение: если залить только
   наконечник, различие «пришли / дальше ещё» станет различием способа,
   а не формы. Различает форма — треугольник, двойной ус, поперечина. */
.tn .chart .link-head,  .tn .tl-links .link-head,
.tn .chart .link-edge,  .tn .tl-links .link-edge,
.tn .chart .link-blind, .tn .tl-links .link-blind {
  fill: currentColor; stroke: none; }
/* Слой приглушается при плотности — но не ниже пола (D-190). */
.tn .tl-links.is-dense .link { opacity: var(--link-dim); }
/* ТРИ ТОРЦА. Формулировка «стиля у них своего нет» была верна до 0.74
   и перестала быть: заливка — их собственное объявление. Верным
   осталось главное — ЦВЕТА у них своего нет, он приходит от группы
   одной величиной. Различает их по-прежнему ФОРМА, и форму считает
   продукт; правило существует ради имён: неназванное имя не попадает
   в поверхность, а значит его нельзя ни посчитать аудитом,
   ни объявить устаревшим. */

/* Слой связей поверх строк полотна. `.chart` сюда не годится: он значит
   «корневой элемент диаграммы», а полотно таймлайна и есть диаграмма —
   вложив одно в другое, получили бы диаграмму внутри диаграммы (вопрос
   TN Gantt 3.1). Слой не перехватывает указатель: он лежит поверх
   строк, по которым жмут. */
/* ГЕОМЕТРИЯ УСА ЗАДАНА ПРАВИЛОМ, А НЕ ПЕРЕРИСОВЫВАЕТСЯ ПО МЕСТУ
   (D-197). Путь считает продукт — это осталось верным, — но ПРОПОРЦИЯ
   больше не отдана вёрстке: раствор уса вдвое больше его длины.

   Как нашлось: на одной странице витрины один и тот же `.link-head`
   был нарисован дважды от руки и вышел 5×9 в перечне и 6,1×7 в слое
   связей. Владелец языка увидел это глазами и сказал «внизу другая
   стрелка» — раньше любой проверки. Замер подтвердил.

   Величина `--link-head` объявляла ДЛИНУ и молчала о растворе, а
   молчание правилом не является: два человека дорисуют по-разному,
   и оба будут правы. Теперь длина `--link-head`, раствор `2 ×`,
   и `tools/check_layout.js` меряет пропорцию на живом движке. */
.tn .tl-links { position: absolute; inset: 0; display: block;
  overflow: visible; pointer-events: none; fill: none; }

/* Достоверность кодируется штрихом, а не цветом — то же правило,
   что и для обычных графиков (D-010). */
.tn .chart .is-estimate { stroke-dasharray: var(--dash-estimate); }
.tn .chart .is-plan     { stroke-dasharray: var(--dash-plan); }

/* Заливки из палитры данных. Именованные классы, а не произвольный fill:
   иначе цвет в SVG уедет мимо токенов первым же. */
.tn .chart .fill-1 { fill: var(--d1); } .tn .chart .fill-2 { fill: var(--d2); }
.tn .chart .fill-3 { fill: var(--d3); } .tn .chart .fill-4 { fill: var(--d4); }
.tn .chart .fill-5 { fill: var(--d5); } .tn .chart .fill-6 { fill: var(--d6); }

/* Состояния данных действуют и здесь: доверие к величине не зависит
   от того, нарисована она прямоугольником или строкой. */
.tn .chart .fill-ok       { fill: var(--st-ok); }
.tn .chart .fill-old      { fill: var(--st-old); }
.tn .chart .fill-none     { fill: var(--st-none); }
.tn .chart .fill-unusable { fill: var(--st-unusable); }

/* Приглушённое второе представление той же сущности: базовый план,
   предпросмотр перетаскивания. Прозрачность, а не отдельный цвет —
   иначе понадобится по «призраку» на каждый цвет палитры. */
.tn .chart .is-ghost { opacity: .38; }

/* Фон нерабочего времени: подложка, а не данные. */
.tn .chart .band-off { fill: var(--n2); }

/* Текст внутри диаграммы наследует роли языка. */
.tn .chart text        { fill: var(--n7); font-size: var(--f-micro); }
.tn .chart text.value  { fill: var(--n9); font-size: var(--f-body);
                         font-variant-numeric: tabular-nums lining-nums; }


/* ====================================================================
   ТАЧ-ТАРГЕТ: ПРАВИЛО НАКОНЕЦ ДЕЙСТВУЕТ (версия 0.6)

   Токен `--tap-min` был объявлен с обоснованием из Apple HIG — и применён
   ровно один раз, к `.app-bar`, по которой не жмут. Все интерактивные
   элементы оставались 22–32 px: чип 22, кнопка 26, строка меню 28.
   Нашёл STRU_magic, и формулировка их замечания точная: токен,
   применённый один раз и не туда, ХУЖЕ ОТСУТСТВУЮЩЕГО — он создаёт
   впечатление, что вопрос закрыт.

   Разрешение противоречия не «сделать всё по 44». Плотность в 26 px
   осмысленна на экране с сотней строк и мышью; 44 px осмысленны там,
   где попадают пальцем. Признак — ТИП УКАЗАТЕЛЯ, а не ширина окна:
   планшет с большим экраном всё ещё палец (тот же довод, что в D-020).

   На мыши плотность сохраняется. На тач-экране правило действует.

   ПОЧЕМУ `any-pointer`, А НЕ `pointer` (0.7, вопрос STRU_magic).
   `pointer` описывает ОСНОВНОЙ указатель. У ноутбука с тачскрином
   основной — мышь, то есть `pointer: fine`, и правило не срабатывало бы
   на устройстве, по экрану которого регулярно жмут пальцем.
   `any-pointer: coarse` — «хотя бы один способ ввода неточен».

   Размен, которого опасались (графический планшет включит тач-габариты
   на десктопе), НЕ НАСТУПАЕТ: по спецификации `coarse` — указатель
   ОГРАНИЧЕННОЙ ТОЧНОСТИ, а перо точное и попадает в `fine`.

   ИСПРАВЛЕНО В 0.49: ГАБАРИТ — ПО `pointer`, ДОСТУПНОСТЬ — ПО
   `any-pointer` (D-119).

   Довод выше верен для ДОСТУПНОСТИ и неверен для ПЛОТНОСТИ, а я применил
   его к обоим. «Есть ли палец» и «чем пользуются» — разные вопросы:

     any-pointer: coarse — «палец есть». Отсюда решается, существует ли
                           запасной способ сделать то, что иначе делается
                           только перетаскиванием (`.board-card .move`).
     pointer: coarse     — «палец основной». Отсюда решается РАЗМЕР
                           всего интерфейса.

   Плата, которую я назвал осознанной, оказалась измеренной: у владельца
   STRU_magic ноутбук с тачскрином и мышью, и он получал тач-габарит
   на каждом значке доски из сотен карточек. Это не «немного крупнее»,
   а другой экран. **Моя оценка была догадкой, их — замером.**
   ==================================================================== */
@media (pointer: coarse) {
  .tn button,
  .tn .filter, .tn .chip,
  .tn .ms-btn,
  .tn .menu-item,
  .tn .nav-link,
  .tn .check,
  .tn .icon-btn,
  .tn input, .tn select {
    min-height: var(--tap-min);
  }
  .tn .icon-btn { width: var(--tap-min); }
  /* Строка реестра кликабельна целиком — её высота тоже. */
  .tn .row { padding-top: var(--s3); padding-bottom: var(--s3); }
}

/* ====================================================================
   ТАЙМЛАЙН: ЗНАКИ (версия 0.10)

   ЦВЕТ КОДИРУЕТ ОТКЛОНЕНИЕ, А НЕ СТАТУС (D-043). Заливка полосы говорит
   об отношении её дней ко времени: красным — прошедшая невыполненная
   часть, нейтральным — всё остальное, включая выполненное. Зелёного
   на полотне нет: «выполнено» выражается отсутствием красного
   и галочкой в таблице.

   Решение принято по ответу TN Gantt. Поля `status` у них нет вовсе —
   состояние вычисляется из процента и опорной даты, — а мои три
   исходных варианта были построены на модели, взятой с чужого
   интерфейса (D-043).

   ЦЕНА НАЗВАНА: на полотне три красных — просрочка, нарушенная связь,
   `.is-unusable`. Все три законны по D-010, все три отклонения.
   Различает их ФОРМА: заливка, линия, метка. Не цвет.
   ==================================================================== */

/* --- ПОЛОСА ЗАДАЧИ ---------------------------------------------------
   ГРАНИЦА ВНУТРИ ПОЛОСЫ ЗАДАЁТСЯ ДАТОЙ, А НЕ ДОЛЕЙ ВЫПОЛНЕНИЯ.
   Незавершённая задача, начавшаяся в прошлом и идущая в будущее, —
   ОДНА полоса с границей ровно на опорной дате. Не две полосы
   и не полоса с индикатором: одна сущность, два состояния.

   `--tl-late` — доля просроченной части, 0…1. Приезжает из продукта
   вместе с геометрией; НЕ через атрибут style= (D-028), а классом-
   лесенкой либо серверным <style nonce>.                            */
/* Полоса — над календарной подложкой и под линией опорной даты.
   Порядок объявлен в frame.css у `.tl-rail` (D-154). */
.tn .tl-bar { position: absolute; height: var(--tl-bar-h);
  z-index: var(--z-sticky);
  top: calc((var(--tl-row) - var(--tl-bar-h)) / 2);
  border-radius: var(--r-sm); background: var(--fill-plain);
  min-width: var(--tl-min-bar); }
/* ПРАВЫЙ УГОЛ ПРОСРОЧЕННОЙ ЧАСТИ ПОЯВЛЯЕТСЯ РОВНО ТОГДА, КОГДА ОНА
   ДОХОДИТ ДО КОНЦА (D-205). Скругление слева и прямой срез справа
   верны, пока часть короче полосы: срез — это граница между
   просроченным и нет. При стопроцентной просрочке границы нет,
   часть накрывает полосу целиком — и прямой правый край читается
   как обрез, хотя обрезать нечего.

   `overflow` на полосе продукту не годится: подпись задачи стоит
   СНАРУЖИ полосы (D-028) и была бы срезана вместе с ней.

   Величина `--late-end` — переключатель, а не длина: ноль, пока часть
   не дошла, единица, когда дошла, и плавный переход в последнем
   полупроценте. Умножение длины на безразмерное — обычная арифметика
   CSS, читается сверху вниз. */
.tn .tl-bar .late { position: absolute; left: 0; top: 0; bottom: 0;
  width: calc(var(--tl-late, 0) * 100%);
  --late-end: min(1, max(0, (var(--tl-late, 0) - .995) * 200));
  background: var(--st-unusable);
  border-radius: var(--r-sm)
                 calc(var(--r-sm) * var(--late-end))
                 calc(var(--r-sm) * var(--late-end))
                 var(--r-sm); }

/* --- РАЗДЕЛ: СКОБКА, А НЕ ПОЛОСА -------------------------------------
   Решение TN Gantt, и лучшее из трёх в D-046. У раздела ДВЕ разные
   величины: протяжённость (сколько занимает на шкале) и трудоёмкость
   (сумма длительностей). На их реальных данных — 85 против 124,
   и расхождение растёт с длиной.

   Полоса читается как «столько работы» и неверна всегда, кроме случая,
   когда задачи идут впритык. Скобку так прочитать НЕЛЬЗЯ: у неё нет
   тела, которое можно принять за объём. Это сильнее, чем сделать
   различие видимым, — это делает ошибку невозможной.                */
/* ЗНАК РАЗДЕЛА — ЛИТАЯ ПОЛОСА ВПОЛОВИНУ ЗАДАЧИ, С ТОРЦАМИ (D-193).
   Три различия сразу, и ни одного лишнего: толщина вдвое меньше,
   заливка своя, торцы вниз, которых у задачи нет. Одного различия
   было бы мало — заказчик и должен узнавать раздел, не приглядываясь.

   Торцы свисают НИЖЕ полосы и указывают вниз, на строки, которые
   раздел объединяет. Вверх они указывали бы в пустоту над ним. */
/* УРОВЕНЬ ОБЪЯВЛЕН — ЗНАЧИТ ОБЪЯВЛЕН ЧИСЛОМ (D-206). D-154 назвал
   уровень 2 «полосы, вехи, слой связей», а `z-index` получила одна
   полоса. Скобка и веха оставались на `auto`, то есть НИЖЕ рельса
   с календарной подложкой: праздник разрезал знак посередине.
   Замер TN Gantt: `auto`, `auto`, 2, рельс 1.

   Видно стало в 0.75, когда знак раздела стал литым. До того скобка
   была полой, и подложка проступала сквозь неё законно — дефект
   существовал и не показывался. Я и сам принял это за пропажу торца
   на собственном снимке, прежде чем получил их замер. */
.tn .tl-bracket, .tn .tl-milestone { z-index: var(--z-sticky); }
.tn .tl-bracket { position: absolute; height: var(--tl-bracket-h);
  top: calc((var(--tl-row) - var(--tl-bracket-h)) / 2);
  background: var(--fill-group); border-radius: 1px; }
.tn .tl-bracket::before, .tn .tl-bracket::after {
  content: ""; position: absolute; top: 0;
  width: var(--tl-bracket-cap);
  height: calc(var(--tl-bracket-h) + var(--tl-bracket-drop));
  background: var(--fill-group); }
.tn .tl-bracket::before { left: 0;
  clip-path: polygon(0 0, 100% 0, 0 100%); }
.tn .tl-bracket::after  { right: 0;
  clip-path: polygon(100% 0, 0 0, 100% 100%); }

/* --- ЗАСЕЧКА: ЗНАК БЕЗ ДЛИНЫ -----------------------------------------
   Ниже порога полоса перестаёт быть полосой. Требования «минимальный
   видимый размер» и «не врать про длину» несовместимы, пока элемент
   остаётся полосой: полоса и ЕСТЬ длина. Поэтому меняется знак,
   а не размер знака (D-046) — тот же приём, что реестр, уходящий вниз
   целиком (D-002), и «< 0,1 %» вместо «0,0 %» (D-009).             */
.tn .tl-tick { position: absolute; width: var(--tl-tick-w);
  height: var(--tl-bar-h);
  top: calc((var(--tl-row) - var(--tl-bar-h)) / 2);
  background: var(--n7); border-radius: 1px; }
.tn .tl-tick.is-late { background: var(--st-unusable); }

/* --- ВЕХА: МОМЕНТ, А НЕ СХЛОПНУТЫЙ ИНТЕРВАЛ --------------------------
   Веха и засечка обязаны различаться, и не «просто формой»: они
   означают РАЗНОЕ ОТНОШЕНИЕ КО ВРЕМЕНИ. Веха — момент, длительности
   нет вовсе. Засечка — интервал, схлопнутый до неразличимости.
   На квартальном масштабе перепутать их значит перепутать «сдача
   14 сентября» и «работа 14 сентября».                              */
/* РОМБ РИСУЕТ ПСЕВДОЭЛЕМЕНТ, А НЕ ПОВОРОТ САМОГО ЗНАКА.
   Поворот на элементе разворачивал вместе с ним ВСЁ содержимое — подпись
   имени вставала под 45°. Поворот у знака и содержимое внутри знака —
   вещи несовместимые, и это видно только когда содержимое появляется
   (D-067). */
.tn .tl-milestone { position: absolute; width: 9px; height: 9px;
  top: calc((var(--tl-row) - 9px) / 2); }
.tn .tl-milestone::before { content: ""; position: absolute; inset: 0;
  background: var(--n7); transform: rotate(45deg); }

/* --- ПРЕДПРОСМОТР ПЕРЕТАСКИВАНИЯ: КОНТУР, НЕ ПРОЗРАЧНОСТЬ -----------
   Третья смена знака в том же обмене. Полупрозрачная копия завела бы
   прозрачность как ЧЕТВЁРТЫЙ канал кодирования, у которого нет
   письменного определения, — при том что на полотне уже есть
   приглушённый нейтральный и `.is-ghost`. Контур оставляет
   прозрачность в одном употреблении.                                */
.tn .tl-bar--preview { background: none; border: 1px dashed var(--n6); }
.tn .tl-bar--preview .late { display: none; }

/* --- ПОКАЗАНО НЕ ВСЁ: ЗНАК УРОВНЯ СТРАНИЦЫ ---------------------------
   У TN Gantt внутренние задачи фильтруются НА СЕРВЕРЕ до сериализации,
   поэтому связь с одним скрытым концом в ответ не попадает: задача
   выглядит свободной, не будучи такой. Знак агрегированный, без числа
   и без указания мест — сам факт наличия скрытого раскрывается,
   и это дешевле, чем зритель, требующий начать «свободную» задачу.

   Знак МЕНЯЕТ СМЫСЛ ВСЕГО, что на странице, — значит он не может быть
   уголком в углу. Тот же приём, что плашка режима снимка.           */
/* --- ПРИЗНАК УРОВНЯ СТРАНИЦЫ -----------------------------------------
   Утверждение, меняющее смысл ВСЕГО, что на странице. Три случая
   назвал TN Gantt, и они одной природы:

     «показано не всё»          — картина неполна
     «версия от 01.07.2026»     — числа не сегодняшние
     «только чтение»            — числа не ваши, чтобы их менять

   Число на замороженной версии и то же число на живом плане — РАЗНЫЕ
   УТВЕРЖДЕНИЯ, и различие не в самом числе. Значит признак не может
   быть уголком в углу, вкладкой браузера или строкой в меню.

   ОН ОБЯЗАН ПОПАДАТЬ В СНИМОК ЭКРАНА. Это проверяемое требование
   и главная причина, по которой признак живёт в потоке страницы,
   а не в обвязке приложения: страницу публичной ссылки фотографируют
   и пересылают, и снимок обязан нести то же утверждение,
   что и страница (D-080).

   `.tl-partial` — тот же элемент, названный по одному из случаев.
   Объявлен устаревшим: имя описывало повод, а не предмет.        */
.tn .page-note, .tn .tl-partial {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s4); background: var(--st-old-soft);
  border-bottom: 1px solid var(--n3); font-size: var(--f-small);
  color: var(--n9); }

/* --- МИКРОЗНАК В СТРОКЕ · черновик -----------------------------------
   Отдельный размерный ряд, а не `.tag`: у плашки есть поле, а поле
   в строке 28 px забирает высоту у самой строки. Микрознак поля
   не имеет вовсе — его создаёт строка (D-082).

   Метка состояния `.sw` (8×8) этот случай не закрывает: она одна
   и означает состояние ДАННЫХ. Микрознаков несколько видов сразу,
   и они про свойства ЗАПИСИ: булавка, внутренняя, ведущая величина
   срока, конфликт.

   ТРИ В СТРОКЕ — ПРЕДЕЛ, дальше «+N» с раскрытием. Предел взят
   не из вкуса: четыре знака по 12 px с интервалом — это 60 px,
   то есть колонка, а колонка обязана иметь заголовок и правило
   сортировки. Молчаливая колонка из значков — худший вид колонки. */
.tn .mk { width: 12px; height: 12px; flex: none; display: inline-flex;
  align-items: center; justify-content: center;
  font-size: var(--f-micro); line-height: 1; color: var(--n6); }
.tn .mk-more { font-size: var(--f-micro); color: var(--n6);
  font-variant-numeric: tabular-nums; }

/* --- ПАГИНАЦИЯ --------------------------------------------------------
   Часть архетипа «реестр», а не оформление под ним. Реестр без неё
   описывает только случай «данные кончаются раньше терпения»:
   на 32 тысячах строк прокрутка целиком не вариант ни при какой
   плотности (STRU_magic, заявка 17).

   ПОЛОЖЕНИЕ ПОКАЗЫВАЕТСЯ ЧИСЛОМ, а не только кнопками. «Вперёд»
   и «назад» отвечают, куда можно пойти; «страница 4 из 17» — где ты
   сейчас, а это разные вопросы (D-089).

   ЕСЛИ ОБЩЕЕ ЧИСЛО НЕИЗВЕСТНО — форма ДРУГАЯ, и это не оформление.
   Точный подсчёт на большой выборке стоит дорого, и продукт вправе
   его не делать. Но «страница 4 из 17», где 17 — оценка, врёт молча:
   человек считает, сколько осталось. Тогда `.pager--open`: показано
   «строки 61–80», кнопка «ещё», и ни одного числа, которого нет.  */
.tn .pager { display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s3); border-top: 1px solid var(--n3);
  font-size: var(--f-small); color: var(--n7); }
/* Имя с приставкой намеренно: `.at` — слово из общего оборота,
   и совпадение при разном смысле вероятнее, чем кажется (D-031). */
.tn .pager .pager-at { font-variant-numeric: tabular-nums lining-nums; }
.tn .pager button[disabled] { opacity: .5; cursor: default; }
.tn .pager button[disabled]:hover { border-color: var(--n4); }

/* --- ПОИСК ПО ЗНАЧЕНИЯМ -----------------------------------------------
   ОТДЕЛЬНЫЙ КОМПОНЕНТ ОТ `.ms`, А НЕ ЕГО РАЗМЕР. `.ms` показывает все
   значения с флажками — это верно, пока их десятки. Граница проходит
   ПО КАРДИНАЛЬНОСТИ: выбор из десятков — показать, выбор из тысяч —
   искать. STRU_magic замерил её на своём экране, а не выбрал на вкус:
   девять фасетов — десятки, пять — тысячи.

   `.ms` на тысячах не деградирует плавно: он либо грузит всё, либо
   показывает произвольную часть — то есть отвечает на вопрос «какие
   бывают» там, где спросили «есть ли такое» (D-089).

   ТРИ СОСТОЯНИЯ ПУСТОГО СПИСКА, А НЕ ОДНО. «Ещё не искали»,
   «искали и не нашли» и «спросить не удалось» — разные утверждения
   в одной коробке, и путать их так же дорого, как «нет данных»
   и «ноль» (§6). Класс на каждое, текст пишет продукт.            */
.tn .ac { position: relative; }
/* Список кандидатов — на общем `.menu`, как и `.ms-pop`: тень, радиус
   и слой у всех всплывающих одни. Второе место для этих величин
   означало бы, что они разойдутся. */
.tn .ac-list { position: absolute; left: 0; right: 0; top: 100%;
  margin-top: var(--s1); max-height: 240px; overflow: auto; }
.tn .ac-list .menu-item[aria-selected="true"] { background: var(--n1); }
.tn .ac-hint { padding: var(--s2) var(--s3); font-size: var(--f-small);
  color: var(--n6); }
.tn .ac-picked { display: flex; flex-wrap: wrap; gap: var(--s1);
  margin-top: var(--s1); }

/* --- ВСПЛЫВАЮЩЕЕ ПО НАВЕДЕНИЮ -----------------------------------------
   ПРАВИЛО ВАЖНЕЕ КЛАССА: всплывающее рисует КОРЕНЬ СТРАНИЦЫ, а строка
   только сообщает, на что наведено.

   Причина названа STRU_magic и стоила им дорого: липкая ячейка создаёт
   СОБСТВЕННЫЙ КОНТЕКСТ НАЛОЖЕНИЯ, и карточка, отрисованная внутри
   строки, тонет под соседними строками. `position: fixed` не спасает —
   он меняет систему координат, но не контекст наложения.

   Это наступит у любого продукта, который покажет всплывающее из
   липкой таблицы или из архетипа «таймлайн»: там липкая шапка, липкая
   панель и абсолютные знаки в строках (D-089).

   ПО НАВЕДЕНИЮ НЕЛЬЗЯ ПОКАЗЫВАТЬ ТО, ЧЕГО НЕТ БОЛЬШЕ НИГДЕ. На
   сенсорном экране наведения не существует ни при какой ширине
   (D-073), поэтому содержимое всплывающего обязано быть достижимо
   и другим путём.                                                  */
/* Тоже на общем `.menu`. Своё здесь только положение и то, что
   всплывающее по наведению не перехватывает указатель. */
.tn .pop { position: fixed; max-width: var(--measure);
  padding: var(--s3); font-size: var(--f-small);
  color: var(--n9); pointer-events: none; }

/* --- СТОПКА ЗНАЧЕНИЙ В ЯЧЕЙКЕ -----------------------------------------
   Строка — договор, внутри неё N проектов, N сумм, N ссылок. Соседние
   колонки-списки обязаны идти ПАРАЛЛЕЛЬНО: третий проект напротив
   своей третьей суммы.

   Две величины держат параллельность, и обе не отдаются продукту:
   высота элемента (`--stack-item`) и выравнивание ячейки по верху.
   Разойдутся они молча и будут при этом выглядеть правдоподобно —
   то самое семейство, которым архетип «таймлайн» болел четырежды.

   НЕРАВНОЕ ЧИСЛО ЭЛЕМЕНТОВ В СОСЕДНИХ КОЛОНКАХ — НЕ ПОВОД ВЫРОВНЯТЬ
   МОЛЧА. Три проекта и две суммы означают, что у третьего суммы нет;
   пустое место читается как «не заполнено», а прочерк — как
   «величины не существует» (D-070). Стопка обязана быть одной длины
   во всех колонках строки, недостающее — прочерком.                */
.tn td.cell-stack, .tn th.cell-stack { vertical-align: top; }
.tn .cell-stack > * { min-height: var(--stack-item);
  display: flex; align-items: center; }

/* --- ТАБЛИЦА, ЗАПОЛНЯЮЩАЯ ОТВЕДЁННОЕ ---------------------------------
   `--grid-max: 70vh` по умолчанию ПРЕДПОЛАГАЕТ, ЧТО ТАБЛИЦА НАЧИНАЕТСЯ
   СВЕРХУ. Если над ней шапка экрана и полоса фильтров, низ уезжает
   за край окна — тихо, у каждого, у кого над таблицей есть что-нибудь
   (STRU_magic, заявка 17).

   Элемент не знает, что находится выше него, — значит умолчание,
   зависящее от этого, задать нельзя в принципе. `.grid-wrap--fill`
   отдаёт высоту родителю: он единственный, кто её знает.

   Умолчание оставлено прежним намеренно: менять его молча значило бы
   сломать раскладку у тех, у кого оно сегодня верно. Долг записан
   в OPEN §12 и снимается в 1.0.

   ВАЖНО ДЛЯ ПЕРЕХОДА: `--fill` работает только тогда, когда родитель
   знает свою высоту, то есть когда страница собрана каркасом языка.
   Пока продукт не на каркасе, `--fill` недоступен и остаётся число —
   и это не недосмотр, а следствие: высоту даёт родитель, а родителя
   ещё нет (названо STRU_magic, заявка 20).                          */
.tn .grid-wrap--fill { max-height: none; min-height: 0; flex: 1 1 auto; }

/* --- tokens/frame.css --- */
/* ====================================================================
   TN Services — планировка экранов
   Версия 0.87 · 20.08.2026

   ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Механизм раздачи стерёг значения и не стерёг
   планировку — а все дефекты вёрстки, пойманные при обкатке 0.1–0.2,
   были именно в планировке: `min-height: 0`, `[hidden]` против
   специфичности класса, поведение реестра при сужении. Ни один из них
   не про цвет и не про отступ.

   Планировка архетипа — это язык, а не «про эту страницу». Продукт
   собирает свой экран из этих классов и дописывает единицы строк,
   а не сто двадцать.

   Подключать после tokens.css и components.css.
   ==================================================================== */

/* --- РАБОЧАЯ ОБЛАСТЬ АРХЕТИПА «ОБЗОР» --------------------------------
   Две колонки: слева аналитика (резиновая), справа реестр
   (фиксированный, --reg-w). Свободное место уходит в поле слева,
   а не размазывается между колонками таблицы.                      */
.tn .work { flex: 1; display: grid; min-height: 0;
  grid-template-columns: minmax(0, 1fr) var(--reg-w); }
.tn .work--single { grid-template-columns: minmax(0, 1fr); }

/* ИЗБЫТОЧНО с версии 0.4.1: общее правило `.tn [hidden]` в components.css
   закрывает все 49 классов, задающих `display`. Строка оставлена как
   документация: именно здесь капкан впервые проявился — два экрана
   рисовались один поверх другого. */
.tn .work[hidden] { display: none; }

/* min-height: 0 — в КАЖДОМ звене. Без него flex- и grid-элемент
   не сжимается ниже своего содержимого, распирает страницу, и вместо
   прокрутки колонок начинает ездить весь документ вместе с шапкой:
   полоса с датой и курсом уезжает вверх ровно тогда, когда нужна. */
.tn .col { min-width: 0; min-height: 0; overflow: auto; }
.tn .col--main { border-right: 1px solid var(--n3); }
.tn .col--aside { overflow: visible; display: flex; flex-direction: column;
  background: var(--n1); }

@media (max-width: 1180px) {
  /* Реестр уходит вниз ЦЕЛИКОМ, а не сжимается: колонка в 300 px
     нечитаема, а «немного уже» — худший вариант из возможных. */
  .tn .work { grid-template-columns: minmax(0, 1fr); }
  .tn .col--main { border-right: none; }
  .tn .col--aside { border-top: 1px solid var(--n3); }
}
/* --- УЗКИЙ ЭКРАН: НАВИГАЦИЯ НЕ ИСЧЕЗАЕТ, А ПЕРЕЕЗЖАЕТ -----------------
   До 0.52 здесь стояло `.app-side { display: none }` и не появлялось
   взамен ничего: на телефоне продукт оставался на той странице,
   на которую вошёл, и уйти мог только через адресную строку. Это
   не «неудобно на узком экране» — это ОТСУТСТВИЕ СПОСОБА, и держалось
   оно ровно потому, что скрытие писалось как ответ на вопрос «что
   не влезает», а не «чем тогда пользоваться» (STRU_magic, заявка 24).

   Панель выезжает поверх содержимого, а не раздвигает его: раздвигать
   на 360 px нечего.

   СОСТОЯНИЕ ХРАНИТСЯ ОДИН РАЗ — на кнопке, в `aria-expanded`, откуда
   его читает и раскладка, и скринридер. Класс на предке плюс атрибут
   на кнопке — это одна величина в двух местах, то есть расхождение
   при первой же ошибке продукта; `:has()` избавляет от второй записи.

   Здесь `:has()` уместен, а в заявке-22 (`:has(.count)`) — нет,
   и разница не в приёме: кнопка навигации — элемент САМОГО каркаса,
   язык читает собственную разметку. Счётчик же рисует продукт,
   и обещание «рядом есть число» подписывает он.                  */
@media (max-width: 860px) {
  :is(.tn .app, .tn.app) { grid-template-columns: minmax(0, 1fr); }
  .tn .app-top, .tn .app-main { grid-column: 1; }
  .tn .app-main { grid-row: 2; }
  .tn .app-top { padding-right: var(--s3); border-right: none;
    border-bottom: 1px solid var(--n3); }
  .tn .nav-toggle { display: inline-flex; }

  .tn .app-side { position: fixed; top: 0; bottom: 0; left: 0;
    width: min(var(--side-w-open, 264px), 84vw);
    z-index: var(--z-modal); box-shadow: var(--sh-modal);
    transform: translateX(-100%);
    transition: transform var(--dur-slow) var(--ease-out);
    visibility: hidden; }
  /* `visibility` — не украшение к сдвигу: без него панель за краем
     экрана остаётся в порядке обхода, и Tab уходит в невидимое. */
  :is(.tn .app, .tn.app):has(.nav-toggle[aria-expanded="true"]) .app-side {
    transform: translateX(0); visibility: visible; }
  :is(.tn .app, .tn.app):has(.nav-toggle[aria-expanded="true"]) .nav-scrim {
    display: block; }

  /* МЕНЬШЕ ДВИЖЕНИЯ: панель проявляется, а не въезжает (D-145).
     Сдвиг остаётся способом убрать её за край — но переход к нему
     мгновенный, а видимой она становится проявлением. Правило живёт
     здесь, внутри своей точки перелома: на десктопе .app-side —
     обычная колонка, и `opacity: 0` спрятал бы её насовсем. */
  @media (prefers-reduced-motion: reduce) {
    .tn .app-side {
      transition: opacity var(--dur-base) var(--ease-out),
                  visibility var(--dur-base);
      opacity: 0;
    }
    :is(.tn .app, .tn.app):has(.nav-toggle[aria-expanded="true"]) .app-side {
      opacity: 1;
    }
  }
}

/* --- КОНТЕЙНЕР РЕЕСТРА -----------------------------------------------
   Шапка с фильтрами, прокручиваемое тело, подвал с легендой.
   Заголовки групп липнут к верху ТЕЛА, а не страницы.              */
.tn .reg { display: flex; flex-direction: column; min-height: 0; flex: 1; }
.tn .reg-head { padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--n3); }
.tn .reg-head .line { display: flex; align-items: baseline; gap: var(--s2);
  margin-bottom: var(--s2); }
.tn .reg-head input[type="search"] { width: 100%; margin-bottom: var(--s2); }
.tn .reg-head .filters { display: flex; gap: var(--s1); flex-wrap: wrap; }
.tn .reg-body { flex: 1; min-height: 0; overflow: auto; }
/* Липкость группы переехала на сам `.group` (D-140): она свойство
   группы, а не реестра. */
.tn .reg-foot { border-top: 1px solid var(--n3);
  padding: var(--s3) var(--s4); background: var(--n1); }

/* --- РЯД ГЛАВНЫХ ВЕЛИЧИН ---------------------------------------------- */
.tn .kpis { display: flex; gap: var(--s8); align-items: flex-start;
  flex-wrap: wrap; }
.tn .kpis > * > .t-label { margin-bottom: var(--s2); display: block; }
.tn .kpis .delta { margin-top: var(--s2); }
.tn .kpis .t-sub { margin-top: var(--s2); }

/* --- ПОЛОСА ПОЛНОТЫ ПОД ВЕЛИЧИНАМИ ------------------------------------ */
.tn .fill { margin-top: var(--s5); }

/* --- ЭКРАН ВВОДА ------------------------------------------------------
   Полная ширина: ввод — не чтение, колонок нужно больше, и
   двухколоночная планировка отнимает место у того, ради чего экран
   существует.                                                       */
.tn .entry-screen { min-width: 0; min-height: 0; display: flex;
  flex-direction: column; }
.tn .entry-bar { display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s5); border-bottom: 1px solid var(--n3);
  background: var(--n1); font-size: var(--f-small); flex-wrap: wrap; }
.tn .entry-wrap { flex: 1; min-height: 0; overflow: auto; }
.tn table.entry tr.is-group td { background: var(--n2); font-weight: 600;
  font-size: var(--f-small); color: var(--n7);
  border-top: 1px solid var(--n3); }

/* --- ОТДЕЛЬНО СТОЯЩАЯ ФОРМА ------------------------------------------
   Экран входа, узкий диалог на пустой странице. Центрирует по обеим
   осям на всей высоте области.

   Заведён по запросу продукта: раньше это показывалось в стайлгайде
   через `style="display:flex;justify-content:center"`, то есть приёмом,
   которым продукт со строгой политикой безопасности воспользоваться
   не может. Язык не должен учить тому, что в продукте не работает —
   тем более на экране входа, который видят раньше остальных.        */
.tn .standalone { flex: 1; min-height: 0; display: flex;
  align-items: center; justify-content: center;
  padding: var(--s5); overflow: auto; }
/* На узком экране форма прижимается к верху: центрирование по вертикали
   на телефоне уводит поле ввода под клавиатуру. */
@media (max-height: 560px) {
  .tn .standalone { align-items: flex-start; }
}

/* --- СЕКЦИЯ БЕЗ РАЗДЕЛИТЕЛЯ ------------------------------------------
   Пояснение НАД содержимым, к которому оно относится, и сообщение
   уровня экрана. Обычная секция ставит линию снизу — а она в этом
   месте отделяет пояснение от того, что оно поясняет.

   Поля берутся те же, поэтому сообщение, положенное сюда, не липнет
   к краям экрана. Скрывать целиком через `hidden` безопасно: класс
   не задаёт `display`, и браузерное правило работает.               */
.tn .section--plain { border-bottom: none; }

/* --- ОТСТУПЫ МЕЖДУ БЛОКАМИ -------------------------------------------
   Единственная разрешённая «утилита», и она существует по причине:
   политика безопасности продуктов запрещает атрибут style=, поэтому
   отступ «на один блок ниже» больше нечем задать. Значений четыре,
   больше не заводить — иначе это превратится в набор утилит,
   а вместе с ним вернётся свобода отступов.                        */
.tn .gap-2 { margin-top: var(--s2); }
.tn .gap-3 { margin-top: var(--s3); }
.tn .gap-5 { margin-top: var(--s5); }
.tn .gap-8 { margin-top: var(--s8); }

/* --- АРХЕТИП «ТАЙМЛАЙН» (версия 0.10) --------------------------------
   Пятый архетип. Заведён по заявке TN Gantt; цвет решён в D-043,
   знаки — в D-046.

   ЧЕТЫРЕ ВЕЩИ, НА КОТОРЫХ ГАНТТ ЛОМАЕТСЯ, И ВСЕ ЧЕТЫРЕ — ПЛАНИРОВКА.
   Полосы и стрелки рисуются за час; ломается совпадение строк двух
   панелей, синхронизация прокрутки, маршрутизация стрелок и смена
   масштаба. Первые две — здесь; третья считается продуктом (геометрия
   не язык); четвёртая — в components.css через смену знака.       */

/* ВЫСОТА `.tl` ЗАДАЁТСЯ ИЗВНЕ, И ЭТО НЕ МЕЛОЧЬ. Липкость считается
   относительно области прокрутки; если высота не ограничена, областью
   прокрутки становится страница, `.tl` по вертикали не прокручивается
   вовсе — и шапка уезжает вместе с содержимым. Она при этом «липкая»
   по объявлению и не липкая на экране (D-103).

   Ограничивает высоту РОДИТЕЛЬ: он единственный, кто знает, сколько
   места осталось. Умолчание задать нельзя по той же причине, по какой
   его нельзя задать таблице (OPEN §12). */
.tn .tl { --tl-table-w: var(--tl-table-set);
  position: relative; min-height: 0;
  /* ОДИН СКРОЛЛ-КОНТЕЙНЕР НА ОБЕ ОСИ, и в этом всё разрешение.
     Прокрутка одна — синхронизировать нечего. Неподвижность колонок
     и шапки достигается ЛИПКОСТЬЮ, а не второй прокруткой. */
  overflow: auto; overscroll-behavior: contain;
  /* Полотно — контейнер запроса по размеру: от него считается высота
     хвоста захвата (D-155). Собственная высота полотна задаётся снаружи
     и от содержимого не зависела и раньше. */
  container-type: size; }

/* --- СТРОКА — ОДИН ЭЛЕМЕНТ НА ВСЮ ШИРИНУ (D-091) ----------------------
   ДО 0.25 СТРОКА БЫЛА РАЗРЕЗАНА НА ДВЕ ПАНЕЛИ, и её единство держалось
   на совпадении величин, вычисляемых порознь. За сутки это дало пять
   дефектов, и все пять — одной формы:

     высота строки        полотно съезжало на строку        D-059
     высота шапки         то же, этажом выше                D-059
     порядок колонок      имя схлопывалось в букву          D-064
     соединение панелей   подсвечивалась чужая строка       D-068, D-072
     иерархия             раздел не сворачивал первую задачу D-088

   Ни один не был ошибкой в правиле. Все пять — следствие устройства,
   при котором ОДНА вещь описана в двух местах. Чинить их по одному
   значило чинить случаи: пятый нашёлся после того, как четыре были
   исправлены.

   Теперь строка — один элемент. Совпадать перестало быть чему:
   высота одна, число строк одно, `:hover` работает сам, а порядок
   колонок остаётся единственной величиной, живущей в двух местах,
   потому что вторым местом является разметка продукта (D-064). */
/* ШИРИНА ОБЪЯВЛЕНА ОДИН РАЗ — НА `.tl-body`. Шапка лежит внутри него
   и получает ту же ширину тем, что она блок: собственной ширины у неё
   нет, и разойтись ей не с чем.

   ДО 0.33 ШИРИНА БЫЛА ОБЪЯВЛЕНА ДВАЖДЫ — у шапки и у тела, — и оба
   раза через `max(100%, …)`. Выражения совпадали, а `100%` считался
   от РАЗНЫХ родителей: у шапки от области прокрутки, у тела от неё же,
   но при появлении вертикальной полосы прокрутки эти величины
   расходятся на её ширину. Шапка съезжала относительно полос,
   и даты переставали стоять над своими днями (D-101). */
/* ТРИ КОЛОНКИ, А НЕ ДВЕ (D-202). Средняя — полоса управления: липкая
   вместе с таблицей, шириной в один знак.

   ЗАЯВКА TN GANTT И ИХ ЖЕ ВОЗРАЖЕНИЕ К НЕЙ. Они просили колонку
   и сами возразили: «добавляет структуру каждой из двух тысяч строк
   ради знаков, которые живут только в шапке». Возражение снимается
   устройством сетки: колонка объявлена ОДНИМ правилом, а разметка
   нужна только там, где в неё что-то кладут. Строки тела остаются
   двухдетными и ничего не платят — `grid-column` расставлен явно.

   ЗАЧЕМ ВООБЩЕ. Кнопку, которую нажимают дважды подряд, нельзя двигать
   её же нажатием — правило их заказчика, и оно верное. Пока знак живёт
   внутри того, что он сворачивает, устойчивость приходится добывать;
   в своей колонке она получается сама. Их обходной путь — внутренний
   отступ дорожки — не работал и не мог: отступ есть часть
   ПРОКРУЧИВАЕМОГО содержимого, и при любой прокрутке он уезжает.
   Та же форма, что D-139: у области прокрутки не бывает поля. */
.tn .tl-row, .tn .tl-head {
  display: grid;
  grid-template-columns: var(--tl-table-w) var(--tl-gutter) minmax(0, 1fr);
}
/* Полоса непрозрачна и липнет сразу за таблицей: содержимое дорожки
   уходит ЗА неё так же, как за колонку имён, — а не под неё. */
.tn .tl-gutter { grid-column: 2; position: sticky; left: var(--tl-table-w);
  z-index: calc(var(--z-sticky) + 2);
  background: var(--tl-row-bg, var(--n0));
  display: flex; justify-content: center;
  /* СОДЕРЖИМОЕ ЛИПНЕТ К ВЕРХУ, А НЕ ЦЕНТРИРУЕТСЯ. Шапка теряет ярусы
     снизу, когда шкалу сворачивают; centered-знак уезжал бы вверх
     на половину потери — то есть двигался бы собственным нажатием.
     Замер TN Gantt: `y` 397 → 403 → 397 по одной оси и 406 → 381
     по другой. Верх шапки закреплён липкостью, поэтому привязка
     к нему — единственная неподвижная точка в строке. */
  align-items: flex-start; padding-top: var(--s1); }

/* ФОН СТРОКИ — ОДНА ВЕЛИЧИНА, А НЕ ДВЕ. Липкие ячейки обязаны быть
   непрозрачны, иначе под ними просвечивает дорожка. Раньше это значило
   второй набор правил фона; теперь фон объявлен переменной строки,
   и ячейки его наследуют — раздел, наведение и обычная строка
   описаны по одному разу. */
.tn .tl-row { --tl-row-bg: var(--n0);
  background: var(--tl-row-bg); }
.tn .tl-cells { position: sticky; left: 0;
  z-index: calc(var(--z-sticky) + 2);
  background: var(--tl-row-bg, var(--n0));
  border-right: 1px solid var(--n3);
  display: grid; grid-template-columns: var(--tl-cols);
  align-items: center; gap: var(--s2); padding: 0 var(--s3);
  min-width: 0; }
.tn .tl-cells > .nm { min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* КОЛОНКИ ИСПОЛНИТЕЛЯ И ЗАГРУЗКИ ОТМЕНЕНЫ В 0.41. Исполнитель
   и его нагрузка живут в ПОДСТРОКЕ назначения, а не в колонке строки:
   их у задачи произвольное число, а колонка — место ровно для одного
   значения. Колонка заставляла выбирать «кого показать», то есть
   отвечала на вопрос, которого нет (D-110). */

/* ДОРОЖКА — СОДЕРЖАЩИЙ БЛОК ДЛЯ ЗНАКОВ. Без неё архетип не рисуется
   вообще: знаки абсолютны и разрешались бы относительно окна.
   `left` и `width` продукт назначает в ПРОЦЕНТАХ ОТ ДОРОЖКИ (D-052). */
/* КАЛЕНДАРНАЯ ПОДЛОЖКА РИСУЕТСЯ КАЖДОЙ СТРОКОЙ, А НЕ ОДНИМ ВЫСОКИМ
   ЭЛЕМЕНТОМ. До 0.35 это был слой во всю высоту содержимого, и после
   нескольких десятков строк он переставал закрашиваться: длинный
   элемент с фоном — не то же самое, что сто коротких, и браузер
   вправе рисовать его иначе.

   Строчная заливка вдобавок не может разъехаться со строкой: она
   и есть строка. Сетка, чередование недель и выходные — величины
   РЕГУЛЯРНЫЕ, то есть выражаются повторением; нерегулярны только
   праздники, и они остаются элементами (D-103).

   Все длины — в доле дорожки (D-102). */
/* ЯВНАЯ КОЛОНКА, А НЕ АВТОРАЗМЕЩЕНИЕ. Когда ячейки скрыты
   (`.tl--no-table`), дорожка при авторазмещении переезжает в ПЕРВУЮ
   колонку — ту, ширину которой свёртка обнулила, — и полотно
   схлопывается вместе с таблицей. Сетка не жалуется: она разместила
   единственный оставшийся элемент в первую свободную ячейку, честно
   и не туда (D-110). */
.tn .tl-cells { grid-column: 1; }
.tn .tl-track { grid-column: 3; position: relative; min-width: 0;
  background-image:
    /* крупная линия — та, у которой есть подпись; первой, чтобы
       рисоваться поверх мелкой в местах совпадения */
    repeating-linear-gradient(to right,
      var(--tl-grid-major-c) 0 1px,
      transparent 1px var(--tl-major, 100%)),
    repeating-linear-gradient(to right,
      var(--tl-grid-c) 0 1px,
      transparent 1px var(--tl-step, 100%)),
    /* выходные: доля недели, со сдвигом до первой субботы */
    repeating-linear-gradient(to right,
      transparent 0 var(--tl-off-at, 100%),
      var(--tl-off-c) var(--tl-off-at, 100%)
        calc(var(--tl-off-at, 100%) + var(--tl-off-w, 0%)),
      transparent calc(var(--tl-off-at, 100%) + var(--tl-off-w, 0%))
        var(--tl-week, 100%)),
    repeating-linear-gradient(to right,
      var(--tl-zebra-c) 0 var(--tl-band, 100%),
      transparent var(--tl-band, 100%) calc(var(--tl-band, 100%) * 2));
}

/* --- СЛОЙ КООРДИНАТ ---------------------------------------------------
   Линия опорной даты и затенение нерабочих дней пересекают ВСЕ строки,
   значит живут не в строке. Раньше они были детьми полотна; полотна
   больше нет, и его роль занимает `rail` — прямоугольник ровно
   по дорожке, во всю высоту содержимого.

   Это же будущее пространство координат слоя связей (D-074, D-077):
   стрелка обязана считаться в тех же процентах, что полоса, и рисоваться
   поверх строк, но под липкими ячейками. Отдельный слой отвечает
   на оба требования сразу. */
/* ЕДИНСТВЕННОЕ МЕСТО, ГДЕ ОБЪЯВЛЕНА ШИРИНА СОДЕРЖИМОГО.
   `max()` избавляет от переключателя «широкий масштаб»: при
   `--tl-track-w: 0` содержимое ровно по окну и горизонтали нет. */
.tn .tl-body { position: relative;
  width: max(100%, calc(var(--tl-table-w) + var(--tl-track-w, 0px))); }
/* КАЛЕНДАРНАЯ ПОДЛОЖКА ЖИВЁТ НА СЛОЕ КООРДИНАТ, а не в строках:
   сетка и зебра пересекают все строки, как линия опорной даты.

   СЕТКА — ГРАДИЕНТОМ, А НЕ ЭЛЕМЕНТАМИ. На квартале 92 дня, на годе
   52 недели: столько же узлов разметки в каждой строке — это
   не оформление, а нагрузка. Повторяющийся градиент рисует их один
   раз на слой и переживает смену масштаба сменой ОДНОГО числа.

   ЕДИНИЦА СЕТКИ ЗАВИСИТ ОТ МАСШТАБА, и это не вкус: на квартале день
   читается, на годе даёт 4 px и превращается в рябь. Продукт задаёт
   `--tl-step` тем, что помещается: день на квартале, неделю на годе
   (D-092).

   ШАГ СЕТКИ ЗАДАЁТСЯ В ДОЛЕ ДОРОЖКИ, А НЕ В ПИКСЕЛЯХ (D-102).
   Подписи и заливки календаря стоят в ПРОЦЕНТАХ от дорожки; сетка
   в пикселях совпадала с ними ровно при одной ширине окна и разъезжалась
   при любой другой, потому что дорожка тянется, а пиксель — нет.
   Одна величина, заданная в двух единицах, — это две величины. */
/* ПОРЯДОК В РАЗМЕТКЕ — НЕ ПОРЯДОК СЛОЁВ (D-154).
   Слой координат стоял в разметке ДО строк и без `z-index`, а строка
   непрозрачна. Кто окажется сверху, решал порядок в дереве, то есть
   величина, которой у языка нет: разметку пишет продукт. Линия опорной
   даты проглядывала точками в однопиксельных зазорах между строками
   (замер TN Gantt, 19.08.2026).

   Порядок объявлен, а не унаследован от разметки:

     0  строка и её фон
     1  календарная подложка: сетка, выходные, праздники
     2  полосы, вехи, слой связей
     3  линия опорной даты
     4  липкая колонка имён   (--z-sticky + 2)
     5  липкая шапка          (--z-sticky + 3)

   Числа выражены через `--z-sticky`, а не написаны: §«Слои» требует
   брать z-index только из токенов, и «1» рядом с `calc(--z-sticky + 2)`
   было бы двумя способами считать одну шкалу. */
.tn .tl-rail { position: absolute; top: 0; bottom: 0;
  left: var(--tl-table-w); right: 0; pointer-events: none;
  z-index: calc(var(--z-sticky) - 1);
}

/* РАЗДЕЛИТЕЛЬ — КРАСКА, А НЕ ГЕОМЕТРИЯ (D-180).
   `border-bottom` при `box-sizing: border-box` съедает пиксель высоты:
   строка 28, содержимое 27. Липкая колонка растёт по содержимому
   и оставляет полосу в один пиксель на каждую строку — сквозь неё
   виден слой связей, столбиком точек (замер TN Gantt, 16 связей,
   до пяти отрезков на прокрутке).

   Порядок слоёв здесь ни при чём, и они это проверили подстановкой:
   в зазоре нет ячейки, которой закрывать. Их формулировка сильнее
   моей: УРОВЕНЬ, СКВОЗЬ КОТОРЫЙ ВИДНО НИЖНИЕ УРОВНИ, — НЕ УРОВЕНЬ.

   `inset` тень рисует ту же линию и высоты не занимает. */
.tn .tl-row { height: var(--tl-row); box-sizing: border-box;
  box-shadow: inset 0 -1px 0 var(--n2); }

/* НЕПРОЗРАЧНАЯ ПОВЕРХНОСТЬ НЕСЁТ ЛИНИЮ САМА. Липкая колонка лежит
   ПОВЕРХ строки и закрывает её разделитель вместе со всем остальным;
   без собственной линии полотно оказалось бы расчерчено, а таблица
   нет. Тот же довод, что в D-107: поверхность, закрывающая то, что
   под неё уезжает, обязана взять на себя и его работу. */
.tn .tl-cells { box-shadow: inset 0 -1px 0 var(--n2); }

/* СТРОКА ТАБЛИЦЫ — СЕТКА КОЛОНОК, а не текстовый блок. Шапка называет
   «начало · конец · раб. дн.»; без колонок это обещание, которого
   разметка не выполняет. Числовые колонки по правому краю и табличными
   цифрами — их сравнивают между собой (D-007). */
.tn .tl-cells { display: grid; grid-template-columns: var(--tl-cols);
  align-items: center; gap: var(--s2); padding: 0 var(--s3); }
.tn .tl-cells > .nm { min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
/* КОЛОНКИ ИСПОЛНИТЕЛЯ И ЗАГРУЗКИ ОТМЕНЕНЫ В 0.41. Исполнитель
   и его нагрузка живут в ПОДСТРОКЕ назначения, а не в колонке строки:
   их у задачи произвольное число, а колонка — место ровно для одного
   значения. Колонка заставляла выбирать «кого показать», то есть
   отвечала на вопрос, которого нет (D-110). */
/* --- СТРОКА РАЗДЕЛА ---------------------------------------------------
   Раздел отличался от задачи только текстом — то есть ничем. Полоса
   на полотне у него намеренно скобка (D-046), поэтому вся выразительность
   обязана быть в строке: фон, полужирное имя, треугольник свёртки.
   Величины у раздела СУММАРНЫЕ, и прогресс — ВЗВЕШЕННЫЙ по трудоёмкости,
   а не среднее по задачам: среднее даёт однодневной задаче тот же вес,
   что трёхнедельной (D-063). */
.tn .tl-row--section { font-weight: 600; }
.tn .tl-row--section .nm { font-weight: 600; }
/* УРОВНИ ОТСТУПА. Задача — один, назначение — ДВА. До 0.22 назначение
   отступа не имело вовсе и вставало левее задачи — то есть выглядело
   как ещё один раздел, а не как её часть.

   Отступа мало: 14 px на строке 28 px глаз читает как неровность,
   а не как уровень. Подчинённость держит СВЯЗУЮЩАЯ ЛИНИЯ — та же
   вертикаль, что в любом дереве. Отступ говорит «глубже», линия —
   «глубже ЧЕГО» (D-088). */
.tn .tl-task .nm { padding-left: var(--tl-indent); }
/* НАЗНАЧЕНИЕ ВТОРОСТЕПЕННО ПО ОТНОШЕНИЮ К ЗАДАЧЕ, и отступа с линией
   для этого мало: при том же кегле и том же черниле строка читается
   как равная. Подчинённость держат ТРИ вещи сразу — отступ, линия
   и вес: место, связь и голос (D-112). */
.tn .tl-row--sub .nm { font-size: var(--f-small); color: var(--n7);
  padding-left: var(--tl-indent);
  margin-left: var(--tl-indent);
  border-left: 1px solid var(--n4); align-self: stretch;
  display: flex; align-items: center; }

/* Треугольник свёртки — кнопка, а не украшение: у него свой habitat
   в колонке, чтобы имена разделов и задач начинались от одной вертикали. */
/* ТРЕУГОЛЬНИК — КНОПКА С ВИДИМЫМ ГАБАРИТОМ. В 0.11 он был 10 px
   без фона: цель мнимого размера, в которую нужно целиться. Теперь
   у него своя площадь 20×20, подложка при наведении и полный габарит
   нажатия на тач-экране (--tap-min, D-030). */
.tn .tl-fold { width: 20px; height: 20px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r-sm);
  background: none; color: var(--n7); cursor: pointer; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; }
.tn .tl-fold::before { content: "\25be"; font-size: var(--tl-fold-mark);
  line-height: 1; }
.tn .tl-fold[aria-expanded="false"]::before { content: "\25b8"; }
.tn .tl-fold:hover { background: var(--n0); border-color: var(--n4); }
.tn .tl-fold:focus-visible { outline: 2px solid var(--accent);
  outline-offset: 1px; }
@media (any-pointer: coarse) { .tn .tl-fold { width: var(--tap-min);
  height: var(--tap-min); } }

/* --- ГРАНИЦА ПАНЕЛЕЙ ДВИГАЕТСЯ ---------------------------------------
   Ширина таблицы — величина языка (`--tl-table-w`), но не константа:
   имена задач у разных продуктов разной длины, и «подобрать раз
   и навсегда» — то же, что подбирать отступ на глаз. Захват меняет
   ТОТ ЖЕ токен, второго места для числа не появляется (D-065).

   Значение назначается через CSSOM (`setProperty`), а не атрибутом
   `style=`: атрибут отбрасывается политикой безопасности продуктов
   (D-022), а CSSOM ею не ограничен. */
/* ЗАХВАТ — РЕБЁНОК ТАБЛИЦЫ И ВНЕ ПОТОКА. Он стоял первым ребёнком
   полотна с `position: sticky`, а липкий элемент остаётся В ПОТОКЕ:
   кнопка занимала 26 px (`min-height` у всех кнопок языка) и опускала
   ВСЁ полотно на строку относительно таблицы. Панели разъезжались,
   и диаграмма показывала чужие даты — не «некрасиво», а ложно.
   
   Таблица и так липкая, то есть позиционированная: захват прижимается
   к её правому краю абсолютно. Абсолютный элемент высоты не занимает
   вовсе, поэтому занять её больше нечем (D-068). */
/* ЗАХВАТ ЖИВЁТ В ШАПКЕ. Прежде он тянулся во всю высоту таблицы,
   но таблицы как элемента больше нет, а строка знает только свою
   высоту. Шапка липкая, значит захват виден ВСЕГДА — в отличие
   от прежнего, который был виден только там, где ты сейчас.
   Плата названа честно: цель стала 40 px высотой вместо всей
   страницы (D-091). */
.tn .tl-grip { position: absolute; top: 0; bottom: 0; right: -2px;
  z-index: calc(var(--z-sticky) + 3); width: var(--tl-grip-w);
  min-height: 0; cursor: col-resize; background: none; border: none;
  padding: 0; border-radius: 0; }
/* ЗАХВАТ ТЯНЕТСЯ ВО ВСЮ ВИДИМУЮ ВЫСОТУ, а не только по шапке.
   В 0.25 он переехал в шапку, и плата была названа: цель стала 40 px
   вместо всей страницы. Плата оказалась лишней — шапка липкая, значит
   она всегда наверху области прокрутки, и полосе достаточно свисать
   от неё вниз на высоту окна.

   `100vh` здесь не «магическое число», а «не меньше видимого»:
   всё, что ниже видимого, недостижимо указателем по определению
   (D-112). */
/* ХВОСТ МЕРИТ ВИДИМУЮ ЧАСТЬ ПОЛОТНА, А НЕ ОКНО (D-155).
   `100vh` — высота ОКНА, и она верна ровно пока полотно не имеет
   своей границы. Как только продукт задал полотну высоту, хвост стал
   свисать НИЖЕ содержимого внутри области прокрутки и надул её.
   Замер TN Gantt на 2000 задач: содержимое 637 px, прокручиваемая
   высота 1146 px, разница 509 = высота окна минус содержимое.

   Довод D-112 при этом верен: свисать надо, и «всё, что ниже видимого,
   недостижимо указателем» — так и есть. Неверна была мера.
   `100cqh` — высота видимой части САМОГО полотна, ровно та величина,
   которую `100vh` приближала.

   Запасной выход оставлен намеренно: продукт, у которого полотно
   не ограничено по высоте, может задать величину сам (§4). */
.tn .tl-grip::after { content: ""; position: absolute; top: 0; left: 0;
  width: 100%; height: var(--tl-grip-h, 100cqh); cursor: col-resize; }
.tn .tl-grip:hover, .tn .tl-grip:focus-visible { background: var(--accent); }
.tn .tl-grip:hover::after { background: var(--accent); }

/* --- ЗАХВАТ ГРАНИЦЫ КОЛОНКИ -------------------------------------------
   Захват панели меняет ОСТАТОК, то есть ширину имени. Этого мало:
   на узкой панели числовые колонки забирают своё жёстко, и остатка
   не хватает. Захват колонки отнимает ширину у конкретной колонки —
   и имя, будучи резиновым, забирает освободившееся само.

   ЗАХВАТ ЖИВЁТ В ШАПКЕ, А НЕ В СТРОКАХ. Он один на колонку, а не сто:
   элемент, повторённый в каждой строке, — это сто целей нажатия
   на одну величину.

   Меняет ТОТ ЖЕ токен, из которого колонка и собрана: второго места
   для числа не появляется (D-090). */
.tn .col-grip { position: absolute; top: 0; bottom: 0; right: 0;
  width: var(--tl-grip-w); margin-right: calc(var(--tl-grip-w) / -2);
  min-height: 0; padding: 0; border: none; border-radius: 0;
  background: none; cursor: col-resize; z-index: 1; }
.tn .col-grip:hover, .tn .col-grip:focus-visible {
  background: var(--accent); }
.tn .tl-head .tier-2 { position: relative; }

/* --- ИМЯ У ПОЛОСЫ ПРИ НАВЕДЕНИИ ---------------------------------------
   Постоянные подписи на полосах — приём из референсов, где задач
   дюжина. На сотне строк это сто повторов имени, а однодневная задача
   на квартальном масштабе шириной 13 px подписи не вмещает вовсе.

   Но задача настоящая: уехав на 2700 px вправо, глаз не возвращается
   к имени. Подпись показывается при наведении на строку — тогда она
   отвечает ровно на вопрос «что это за полоса» и не шумит (D-065). */
.tn .tl-name { position: absolute; left: 100%; top: 50%;
  transform: translateY(-50%); padding-left: var(--s2);
  white-space: nowrap; font-size: var(--f-micro); color: var(--n7);
  pointer-events: none; opacity: 0; transition: opacity var(--t-fast, .1s); }
.tn .tl-track:hover .tl-name { opacity: 1; }

/* ПОДПИСЬ СНАРУЖИ ЗНАКА, А НЕ ВНУТРИ — И ЭТО НЕ ОФОРМЛЕНИЕ.
   TN Gantt предложил на телефоне «перенести имя внутрь полосы». Внутрь
   нельзя: у 12,5 % задач длительность — один день, на квартале это
   30 px, а у вехи ширины нет вовсе. Подпись внутри знака означала бы,
   что для восьмой части строк её нет.

   `left: 100%` ставит подпись сразу за правым краем ЛЮБОГО знака —
   полосы, засечки, вехи — и ширину знака знать не надо (D-028).

   ПОКАЗ ВСЕГДА — ЭТО РЕЖИМ, А НЕ ШИРИНА ЭКРАНА. Условие не «телефон»
   и не «узко», а ОТСУТСТВИЕ НАВЕДЕНИЯ: подпись по `:hover` на
   сенсорном экране недостижима вовсе, и её там нет ни при какой
   ширине. `@media (hover: none)` спрашивает ровно об этом (D-073). */
@media (hover: none) {
  .tn .tl-name { opacity: 1; }
}
.tn .tl--named .tl-name { opacity: 1; }

/* СТОРОНА ПОДПИСИ. Знак у правого края окна выталкивает подпись за экран,
   и подписи снова нет — только уже у другой восьмой части строк. Назвал
   TN Gantt: человек листает полотно вправо именно затем, чтобы смотреть,
   что дальше, поэтому знак у правого края — не редкость, а норма.

   Сторон ровно две, и третьей не нужно: знак не может одновременно
   упираться в оба края окна. Если справа не влезло — знак у правого
   края, значит слева свободно всё полотно, которое уже прокручено.

   ПОРОГ ЗАДАН ГЕОМЕТРИЕЙ ОКНА, А НЕ ДАННЫМИ. «Правее 90-го дня» —
   порог в данных: он меняется от масштаба, ширины и прокрутки, то есть
   это три величины, обязанные совпадать с одной. Правило одно: подпись
   переезжает влево, когда её правый край выходит за правый край ОКНА
   ПРОКРУТКИ. Считает продукт — он единственный, кто знает координаты
   (D-076). */
.tn .tl-name--left { left: auto; right: 100%;
  padding-left: 0; padding-right: var(--s2); }

/* ТРЕТЬЕ ПОЛОЖЕНИЕ: ЗНАК ШИРЕ ОКНА. Доказательство в D-076 было неверным:
   «знак не может упираться в оба края окна» — ложно для полосы ШИРЕ
   окна, а скобка раздела на квартальном масштабе именно такая. У неё
   подписи не было бы ни справа, ни слева.

   Условие здесь ОБРАТНО условию D-073: внутрь знака нельзя, когда знак
   короткий; внутрь можно именно потому, что знак длиннее окна — место
   гарантировано самим условием.

   Липнет к видимому краю полотна: смещение то же, что у заголовка
   периода, и по той же причине — первые `--tl-table-w` пикселей области
   прокрутки занимает липкая колонка (D-062).

   СОБСТВЕННЫЙ ФОН — НЕ ОФОРМЛЕНИЕ. Под подписью оказывается доля
   отклонения (`--st-unusable`), на которой тёмный текст нечитаем.
   Фон делает подпись независимой от того, что под ней (D-083). */
/* --- ИМЕНА НАЗНАЧЕНИЙ НА ПОЛОТНЕ --------------------------------------
   Строка назначения на полотне была пустой: подчинённость видна слева
   и не видна справа, а справа человек проводит взглядом больше времени.

   Имя ставится ПОД полосой задачи, то есть в её горизонтальном
   положении. Координату даёт продукт — ту же самую, что у полосы:
   второго числа не появляется, иначе это была бы ещё одна величина,
   обязанная совпадать (D-088). */
.tn .tl-subname { position: absolute; top: 50%;
  transform: translateY(-50%); white-space: nowrap;
  font-size: var(--f-micro); color: var(--n6);
  padding-left: var(--s2); pointer-events: none; }

/* ИМЯ РАЗДЕЛА СТОИТ В НАЧАЛЕ СВОЕЙ СКОБКИ. Не снаружи справа
   (иначе оно оторвано от начала области) и не липко к видимому краю
   (иначе два раздела на одном экране выглядят по-разному, а причина
   этого — ширина окна, о которой читатель не думает).

   Начало скобки — то место, где область начинается; имя стоит там,
   где начинается то, что оно называет (D-113).

   Плата названа: домотав вправо, имя раздела теряешь. Взамен два
   раздела всегда выглядят одинаково, а это на плотном полотне важнее:
   разное поведение одинаковых вещей читается как разница в них самих.

   Своё основание — потому что знак тонкий и под именем может
   оказаться что угодно (D-111).

   ИМЯ НАЧИНАЕТСЯ ЗА ОТКРЫВАЮЩИМ ТОРЦОМ, а не от нуля (D-196). Пока
   знак был полой скобкой, имя внутри неё не закрывало ничего. Знак
   стал литым — и непрозрачная плашка имени села ровно на торец,
   то есть на ту самую примету, ради которой торец и заведён.
   Смещение равно ширине торца: имя отступает настолько, насколько
   торец занимает, и ни на пиксель больше. */
.tn .tl-bracket > .tl-name { left: calc(var(--tl-bracket-cap) + var(--s1));
  right: auto;
  padding: 0 var(--s1); line-height: var(--tl-bar-h);
  border-radius: var(--r-sm);
  background: var(--n0); color: var(--n9); }

.tn .tl-name--in { position: sticky; left: var(--tl-table-w);
  right: auto; top: auto; transform: none;
  display: block; width: max-content;
  line-height: var(--tl-bar-h); padding: 0 var(--s1);
  border-radius: var(--r-sm); background: var(--n0); color: var(--n9); }

/* ПОДПИСЬ ЖИВЁТ ВНУТРИ ЗНАКА, а не рядом с ним, и `left: 100%` ставит её
   сразу за его правым краем — у полосы, засечки и вехи одинаково.
   Снаружи пришлось бы знать ширину знака, а это геометрия продукта
   (D-028). Ширина подписи в поток не входит: она абсолютна, значит
   не расширяет полотно и не сдвигает соседей (D-065). */

/* --- ПРОГРЕСС В ТАБЛИЦЕ ----------------------------------------------
   Полоса, а не кольцо: сто строк сравнивают по общей базе, а угол
   различается втрое хуже положения. Полоса НЕЙТРАЛЬНАЯ — цвет занят
   отклонением (D-010), и колонка прогресса не повод его тратить.
   Число рядом, потому что полоса не даёт точного значения (D-063). */
.tn .tl-prog { display: inline-flex; align-items: center; gap: var(--s2); }
.tn .tl-prog .bar { width: var(--tl-prog-w); height: 4px; flex: none;
  background: var(--n3); border-radius: 2px; overflow: hidden; }
.tn .tl-prog .bar > i { display: block; height: 100%;
  width: calc(var(--tl-done, 0) * 100%); background: var(--n6); }
.tn .tl-row--section .tl-prog .bar > i { background: var(--n7); }
.tn .tl-prog .pct { font-size: var(--f-micro); color: var(--n7);
  font-variant-numeric: tabular-nums lining-nums; min-width: 30px;
  text-align: right; white-space: nowrap; }
/* Полоса и число не переносятся: «100 %» в две строки ломало высоту
   строки раздела, а высота строки — общая величина двух панелей (D-059). */
.tn .tl-prog { white-space: nowrap; }

/* Подсветка строки ВО ВСЕЙ ШИРИНЕ, обеих панелей: на сотне строк глаз
   теряет соответствие между именем и полосой. */
/* ПОДСВЕТКА ИДЁТ ПО КЛАССУ, А НЕ ПО :hover.
   `:hover` подсвечивает только ту половину строки, над которой курсор:
   строка физически разрезана на две панели, и её единство держится
   на порядке и общей высоте, а не на тождестве. Класс `is-hot` ставит
   продукт обеим половинам сразу.

   ПЕРЕВЕРСТКА СДЕЛАНА В 0.25 (D-091): строка — один элемент, `:hover`
   работает сам, скрипт не нужен. Класс `is-hot` оставлен рабочим —
   продукту он может понадобиться для подсветки не по курсору
   (например, строки, к которой ведёт выбранная связь). */
.tn .tl-row:hover, .tn .tl-row.is-hot { --tl-row-bg: var(--n1); }
.tn .tl-row--section { --tl-row-bg: var(--n2); }
.tn .tl-row--section:hover, .tn .tl-row--section.is-hot {
  --tl-row-bg: var(--n3); }
.tn .tl-row:hover .tl-name, .tn .tl-row.is-hot .tl-name { opacity: 1; }

/* --- ВЫБРАННАЯ СТРОКА (D-222, заявка TN Gantt) ----------------------
   НАВЕДЕНИЕ ЭФЕМЕРНО, ВЫБОР ДЕРЖИТСЯ — и это разные утверждения,
   которые обязаны выглядеть по-разному. У мыши на экране оказываются
   две подсвеченные строки: под курсором и выбранная; какая из них
   устойчива, по одинаковой подложке не прочитать.

   TN Gantt взяли под выбор `is-hot`, и напоролись дважды. Второй раз —
   потому что `is-hot` УЖЕ ЗАНЯТ, и занят в моём же комментарии рядом:
   «строки, к которой ведёт выбранная связь». Слой связей снимал класс
   со всех строк разом, и первое движение мыши стирало их выбор.

   Их вывод точнее их же оправдания: «один класс на два смысла,
   и тот, что появился позже, проигрывает молча». Это D-184 в другом
   месте — одна величина на две работы.

   ЧЕМ ВЫБОР ОТЛИЧАЕТСЯ. Не оттенком подложки: два оттенка одного цвета
   различают только рядом, а строки бывают на разных концах экрана.
   Меткой на краю — знаком, которого у наведения нет вовсе. Величина
   для неё в языке была с самого начала и ровно с этим назначением:
   `--line-w-bar` — «полоса-метка, подчёркивание выбранного».

   Метка стоит на липкой части строки, а не на дорожке: дорожка
   уезжает, выбор — нет. */
.tn .tl-row.is-picked { --tl-row-bg: var(--accent-soft); }
.tn .tl-row.is-picked > .tl-cells { position: relative; }
.tn .tl-row.is-picked > .tl-cells::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: var(--line-w-bar); background: var(--accent); }
/* Наведение на выбранную строку не отменяет выбора и не спорит с ним:
   подложка чуть плотнее, метка на месте. */
.tn .tl-row.is-picked:hover { --tl-row-bg: var(--accent-soft-hover); }

.tn .tl-cells > .num { text-align: right; color: var(--n7);
  font-size: var(--f-small);
  font-variant-numeric: tabular-nums lining-nums; }

/* ЗАГОЛОВОК ВЫРАВНИВАЕТСЯ ПО ТОМУ, ЧТО ПОД НИМ. Числа прижаты вправо,
   значит и подпись колонки прижата вправо: иначе глаз ведёт от подписи
   к числу по диагонали, и колонка перестаёт читаться как колонка.

   Правило в языке БЫЛО с 0.2 — `th.num` и `td.num` в `table.tight`
   и `table.entry`. Оно не перенеслось на таймлайн, потому что было
   привязано к МЕХАНИЗМУ (`<table>`), а не к смыслу (числовая колонка),
   а таймлайн собран на сетке. Правило, привязанное к механизму,
   не переезжает на новый механизм — и это стоит помнить при шестом
   архетипе (D-057). Нашёл владелец на экране. */
.tn .tl-head .tl-cells .tier-2 { text-align: right; }
.tn .tl-head .tl-cells .tier-1 { text-align: left; }

/* СТРОКА ПОЛОТНА — СОДЕРЖАЩИЙ БЛОК. Без этой строки архетип не рисуется
   ВООБЩЕ: шесть абсолютно позиционированных элементов (полоса, скобка,
   засечка, веха, линия опорной даты, затенение) разрешались бы
   относительно окна и собрались бы в левом верхнем углу страницы —
   по одному разу на всех.
   
   Это не геометрия продукта и не его забота: `left` и `width` продукт
   назначает в ПРОЦЕНТАХ ОТ СТРОКИ и вправе рассчитывать, что строка
   позиционирована языком. Планировка — часть языка, и содержащий блок
   это планировка (D-052). */

.tn .tl-row--sub { height: var(--tl-row-sub); }

/* ШАПКА ДАТ ЛИПНЕТ К ВЕРХУ ПОЛОТНА, А НЕ СТРАНИЦЫ. Два яруса:
   крупная единица сверху, мелкая снизу. */
/* Шапка перекрывает содержимое ЦЕЛИКОМ: фон во всю ширину полотна,
   а не по ширине текста. Найдено на экране — под шапкой просвечивали
   строки (D-056). */
/* ШАПКА — ТАКАЯ ЖЕ СТРОКА, как все прочие: те же две колонки, та же
   ширина. Прежде она была двумя шапками в двух панелях, и их высоты
   были ДВУМЯ величинами, обязанными совпадать (D-059). Теперь высота
   одна и делить её не с кем. */
/* ЯЧЕЙКИ ШАПКИ РАСТЯНУТЫ НА ВСЮ ЕЁ ВЫСОТУ. При `align-items: end`
   они прижимались к низу, а над ними оставалась щель — сквозь неё
   было видно ярус праздников, проезжающий при прокрутке.
   Прижимается к низу СОДЕРЖИМОЕ ячеек (`align-content`), а не сами
   ячейки: непрозрачная поверхность обязана закрывать всё, что под неё
   уезжает (D-107, тот же разбор, что D-056). */
.tn .tl-head { position: sticky; top: 0; width: 100%;
  z-index: calc(var(--z-sticky) + 3);
  background: var(--n0); border-bottom: 1px solid var(--n3);
  min-height: var(--tl-head-h); box-sizing: border-box;
  align-items: stretch; }
/* Подписи колонок прижаты к НИЗУ шапки, к тем строкам, которые называют.
   Шкала растёт вверх ярусами, и подписи, выровненные по верху, ушли бы
   от своих колонок тем дальше, чем больше ярусов (D-095). */
.tn .tl-head > .tl-cells { display: grid; grid-template-rows: auto auto;
  align-content: end; align-items: center;
  padding-bottom: var(--s1); }
/* ШКАЛА — СТОПКА ЯРУСОВ, каждый со своей единицей. */
.tn .tl-head > .tl-track { display: flex; flex-direction: column;
  justify-content: flex-end; }
.tn .tl-tier { position: relative; height: var(--tl-tier-h);
  flex: none; font-size: var(--f-micro); color: var(--n6);
  font-variant-numeric: tabular-nums lining-nums; }
/* Подпись яруса стоит НА своей линии, а не рядом: центр подписи —
   середина того интервала, который она называет (D-095). */
.tn .tl-tier > span { position: absolute; top: 0;
  transform: translateX(-50%); white-space: nowrap; }
/* ПРЕДЕЛ ШИРИНЫ — ТОЛЬКО У ЯРУСОВ С МЕЛКИМ ДЕЛЕНИЕМ. У месяца
   и праздника деление широкое, и обрезать их имена нечем и незачем;
   у числа, дня недели и периода недели соседи стоят вплотную,
   и наложение обязано превращаться в обрезание: обрезание видно
   и чинится, наложение — нет (D-098). */
.tn .tl-tier--day > span, .tn .tl-tier--dow > span,
.tn .tl-tier--week > span {
  /* ПОТОЛОК ПОДПИСИ — ПОЛОВИНА ЕЁ ДЕЛЕНИЯ, а не «минимальное деление».
     До 0.56 здесь стоял `--tl-label-min`, то есть величина про ДЕЛЕНИЕ
     («уже этого — подписывать не каждое») в роли предела ПОДПИСИ.
     Две разные величины под одним именем: имя обещало пол, свойство
     ставило потолок, и оба раза мимо правила — 28 px вдвое больше
     половины самого узкого деления и никак не связаны с широким.

     Ярус недельной крупности подписывает `16 – 22`, а получал ширину
     одного двузначного числа: диапазон обрезался ПО ПОСТРОЕНИЮ,
     и поднятие плотности не помогало (TN Gantt, заявка 19.08).

     Ограничивает подпись РАССТОЯНИЕ ДО СОСЕДНЕЙ ПОДПИСИ, а не
     длительность названного интервала: подписывают не каждое деление.
     Знает это расстояние продукт — он его и рисует. Правило половины
     остаётся здесь: продукт сообщает факт, язык применяет норму.
     Умолчание — самое узкое законное деление, то есть прежнее
     поведение для тех, кто ничего не сказал (D-129). */
  max-width: calc(var(--tl-label-step, var(--tl-label-step-min)) / 2);
  overflow: hidden;
  text-overflow: clip; text-align: center; }
.tn .tl-tier--month { color: var(--n9); font-weight: 600; }
.tn .tl-tier--week { color: var(--n6); }
.tn .tl-tier--day { color: var(--n7); }
.tn .tl-tier--dow { color: var(--n6); }
/* Выходной день недели назван так же, как он закрашен, — цвет
   не единственный канал (§9). */
/* Имя с приставкой намеренно: `.is-off` — слово из общего оборота,
   и совпадение при разном смысле вероятнее, чем кажется (D-031). */
.tn .tl-tier--dow > span.tl-dow-off { color: var(--n4); }
/* ЯРУС ПРАЗДНИКОВ ПУСТ, ПОКА ПРАЗДНИКОВ НЕТ, но высоту занимает:
   иначе шкала прыгает при прокрутке в месяц с праздником и обратно. */
.tn .tl-tier--holiday { color: var(--n7); }

/* --- ШАПКА СВОРАЧИВАЕТСЯ ----------------------------------------------
   Пять ярусов — около 80 px липкой шапки, и это плата, названная
   в D-095. Свёрнутая шкала оставляет три: праздник, месяц и единицу,
   которую ищут глазами (число на квартале, период недели на годе).
   Прячутся номер недели и день недели — они отвечают на вопросы,
   которые задают реже.

   ЩЕЛЧОК ПО ВСЕЙ ШАПКЕ РАБОТАЕТ, НО КНОПКА ОБЯЗАНА БЫТЬ ВИДИМОЙ.
   Поверхность, которая что-то делает и ничем об этом не сообщает, —
   это жест, о котором узнают случайно или никогда. Тот же разбор,
   что у треугольника свёртки в 0.12 (D-030): габарит нажатия был,
   знака не было, и цель считалась мнимой.

   ЯРУС ПРАЗДНИКОВ НЕ ПРЯЧЕТСЯ НИКОГДА. Он меняет смысл всего, что
   под ним, — как и `.page-note` (D-080). Экономить высоту на нём
   значит экономить на утверждении, а не на оформлении (D-096). */
/* СОСТОЯНИЕ ЖИВЁТ НА КОНТЕЙНЕРЕ, А НЕ НА ШАПКЕ: свёртка шкалы —
   свойство таймлайна, а не его шапки.

   СВЁРТКА УБИРАЕТ ЯРУСЫ, А НЕ ЗАЛИВКУ. Заливка отвечает на вопрос
   о СВОЙСТВЕ ДНЯ и от подробности шкалы не зависит: убрав её вместе
   с ярусом, мы поменяли бы данные вместе с оформлением (D-100).

   Прячутся день недели и ИМЯ праздника. Сам праздник заливкой виден
   всегда и на всех масштабах: «нерабочий, потому что праздник»
   из дня недели не выводится, а из ширины окна тем более. */
/* Компактный вид оставляет ТРИ яруса: месяц, единица масштаба и место
   праздника. Прячутся номер недели, день недели и имя праздника —
   всё, что уточняет, но не отвечает на «какое это число». Сам праздник
   заливкой виден всегда. */
.tn .tl--scale-short .tl-tier--week,
.tn .tl--scale-short .tl-tier--dow,
.tn .tl--scale-short .tl-tier--holiday { display: none; }

/* --- ТАБЛИЦА СВОРАЧИВАЕТСЯ ЦЕЛИКОМ -----------------------------------
   Сценарий мобильного просмотра (OPEN §10) и узкого экрана: таблицы
   нет вовсе, остаётся полотно. Тогда имя задачи обязано быть видно
   ВСЕГДА — иначе строка не содержит ничего (это и был второй пробел,
   названный в ответе TN Gantt по альбомному режиму).

   Даты переезжают ВНУТРЬ знака, и это тот же случай, что D-083:
   внутрь можно, когда знак их вмещает. Не вмещает — не показываются
   вовсе; не переносятся и не обрезаются, потому что обрезанная дата
   читается как другая дата (D-108).

   У ВЕХИ ДАТ НЕТ. Веха — нулевая длительность: «с 14.06 по 14.06»
   не уточняет, а повторяет. */
.tn .tl--no-table { --tl-table-w: 0px; }
.tn .tl--no-table .tl-cells { display: none; }
.tn .tl--no-table .tl-name { opacity: 1; }
/* ДАТА ВЫШЕ ЗАЛИВКИ И СО СВОИМ ОСНОВАНИЕМ. Два дефекта в одном месте,
   и оба про то, что под ней:

   1. Доля отклонения (`.late`) объявлена в разметке ПОСЛЕ дат, поэтому
      рисовалась поверх — начальная дата пряталась под красным.
   2. Даты были белыми. На красной доле белое читается, на серой
      части полосы — нет, и одна из двух дат всегда оказывалась
      нечитаемой.

   Оба чинятся одним: дата не должна зависеть от того, что под ней.
   Своё основание и слой выше — тот же разбор, что у подписи внутри
   знака (D-083), и та же причина: элемент, который может лечь
   на что угодно, обязан нести своё основание (D-111). */
/* ДАТЫ СТОЯТ В ПОДПИСИ, ПЕРЕД ИМЕНЕМ: «23.05 – 03.06 · Сценарий».
   Не внутри знака — тогда короткий знак становится отдельным случаем,
   а отдельный случай приходится опознавать порогом, которого никто
   не измерял.

   Одно правило вместо двух и порога: у всех знаков одинаково,
   и «мало места» перестаёт существовать как понятие (D-116).

   У знака нулевой длительности дата ОДНА: «с 17.08 по 17.08»
   не уточняет, а повторяет — то же, что у вехи (D-108).

   НО ОДНА ДАТА НУЖНА ТАМ, ГДЕ ЕЁ НЕОТКУДА ПРОЧЕСТЬ. Правило D-108
   («у вехи дат нет») выведено из повтора: две одинаковые даты не
   уточняют. Одна дата — не повтор, и на годовом масштабе, где деление
   шкалы — неделя, положение знака не говорит, какой это день.

   Отсюда мера, а не вкус: ЗНАК, ДЛИТЕЛЬНОСТЬ КОТОРОГО МЕНЬШЕ ДЕЛЕНИЯ
   ШКАЛЫ, ОБЯЗАН НЕСТИ ДАТУ. На квартале день — одно деление, и веха
   читается по положению; на годе — не читается ничем (D-117). */
/* Имя с приставкой намеренно: `.dt` — два знака из общего оборота,
   и совпадение при разном смысле вероятнее, чем кажется (D-031). */
.tn .tl-name > .tl-dt { color: var(--n7);
  font-variant-numeric: tabular-nums lining-nums; }
.tn .tl-name > .tl-dt::after { content: " · "; color: var(--n4); }

/* Кнопка свёртки таблицы живёт в шапке рядом с кнопкой свёртки шкалы:
   оба сворачивают, оба видимы, оба на своём месте (D-030, D-096). */
.tn .tl-fold-table { width: 20px; height: 20px; padding: 0;
  border: 1px solid transparent; border-radius: var(--r-sm);
  background: none; color: var(--n7); cursor: pointer; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; }
.tn .tl-fold-table::before { content: "\25c0"; font-size: 11px; }
.tn .tl--no-table .tl-fold-table::before { content: "\25b6"; }
.tn .tl-fold-table:hover { background: var(--n0); border-color: var(--n4); }
.tn .tl-fold-table:focus-visible { outline: 2px solid var(--accent);
  outline-offset: 1px; }
/* Свёрнутая таблица прячет свои ячейки, но кнопка обязана остаться —
   иначе развернуть нечем. Она переезжает в шкалу. */
.tn .tl--no-table .tl-head .tl-track { position: relative; }
/* Кнопка липнет к левому краю области прокрутки, а не стоит
   в координатах полотна: иначе она уезжает вместе с датами, и развернуть
   таблицу можно только домотав обратно (D-115). */
.tn .tl--no-table .tl-fold-table { position: sticky; left: 0; top: 0;
  z-index: 3; background: var(--n0); float: left; }

/* ЗНАК СВЁРТКИ ШКАЛЫ — ТА ЖЕ СКОБКА, ЧТО ВЕЗДЕ (D-204). D-162 отменил
   бургер, а поверхность за решением не пошла: `content: "≡"` дожил
   до 0.77 в двух правилах сразу, и продукту приходилось гасить его
   двумя селекторами. Решение, объявленное и не исполненное в CSS,
   для продукта не существует. */
.tn .tl-tiers { width: var(--tl-fold-btn); height: var(--tl-fold-btn);
  padding: 0;
  border: 1px solid transparent; border-radius: var(--r-sm);
  background: none; color: var(--n7); cursor: pointer; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; }
.tn .tl-tiers::before { content: "\00AB"; display: block;
  font-size: var(--tl-fold-mark); transform: rotate(90deg); }
.tn .tl-tiers[aria-expanded="false"]::before { transform: rotate(-90deg); }
.tn .tl-tiers:hover { background: var(--n0); border-color: var(--n4); }
.tn .tl-tiers:focus-visible { outline: 2px solid var(--accent);
  outline-offset: 1px; }
@media (any-pointer: coarse) { .tn .tl-tiers { width: var(--tap-min);
  height: var(--tap-min); } }
/* Щелчок по самой шапке — второй путь к тому же действию, поэтому
   курсор указывает на него по всей поверхности шкалы. Кнопка при этом
   остаётся: путь без знака не заменяет знак. */
.tn .tl-head > .tl-track { cursor: pointer; }
/* Ячейки шапки выше её дорожки: деление шкалы центрируется по своей дате
   и крайнее левое вылезает за край дорожки. Липкая часть обязана
   перекрывать всё, что уезжает под неё (D-061). Значение местное:
   шапка сама создаёт контекст наложения своим `z-index`. */
.tn .tl-head > .tl-cells { z-index: 1; }
/* Шапка таблицы двухъярусна ТАК ЖЕ, как шапка дат: первый ярус —
   название панели во всю ширину, второй — подписи колонок по своим
   колонкам. Иначе высоты панелей расходятся (D-059). */

.tn .tl-head .tl-cells .tier-1 { grid-column: 1 / -1; }
.tn .tl-head .tl-cells .tier-2 { display: none; }
/* ПОДПИСИ КОЛОНОК ЗАНИМАЮТ ВСЮ ШИРИНУ ЯЧЕЕК. Без `grid-column`
   вложенная сетка попадала в ПЕРВУЮ колонку — 24 px под треугольник
   свёртки, — и подписи вставали над чем угодно, кроме своих колонок.
   Отказ бесшумный: сетка разложила подписи честно, просто внутри
   одной ячейки (D-107). */
.tn .tl-head .tl-cells .tiers { grid-column: 1 / -1;
  display: grid; grid-template-columns: var(--tl-cols); gap: var(--s2);
  align-items: center; }
.tn .tl-head .tl-cells .tiers .tier-2 { display: block; text-align: right; }
.tn .tl-head .tl-cells .tiers .tier-2:first-child { text-align: left; }
/* Своего `z-index` у шапки таблицы нет: `.tl-table` — липкий элемент
   с числовым `z-index`, то есть СОЗДАЁТ контекст наложения, и всё внутри
   упорядочивается только между собой. Наружу колонка выступает одним
   значением. Написанное «+2» означало бы не то, что читается. */
.tn .tl-head .tier-1 { font-size: var(--f-micro); color: var(--n7);
  border-bottom: 1px solid var(--n2); }
/* ВТОРОЙ ЯРУС ШАПКИ — ШКАЛА, А НЕ ПОДПИСЬ. Деления размещаются той же
   геометрией, что и полосы: `left` в процентах от полотна. Пока это была
   строка текста, она не двигалась вместе с содержимым при горизонтальной
   прокрутке — то есть ВРАЛА о том, под какой датой стоит полоса.
   Найдено на экране владельцем (D-060). */
.tn .tl-head .tier-2 { font-size: var(--f-micro); color: var(--n6);
  font-variant-numeric: tabular-nums lining-nums; }

/* ЗАГОЛОВОК ПЕРИОДА ПРИЖИМАЕТСЯ К ПРАВОМУ КРАЮ КОЛОНКИ, А НЕ К НУЛЮ.
   `sticky left: 0` считает от края ОБЛАСТИ ПРОКРУТКИ, а там стоит липкая
   колонка задач — заголовок уезжал под неё и пропадал. Смещение равно
   ширине колонки, то есть той же величине, из которой колонка и сделана:
   взять её дважды снова неоткуда (D-062). */

/* `.tl-date` удалён в 0.28: подпись шкалы больше не отдельный элемент,
   её располагает правило яруса (`.tl-tier > span`). Два места для
   одного правила расположения — это два места. */

/* ЛИНИЯ ОПОРНОЙ ДАТЫ. Не «сегодня»: в режиме снимка это дата фиксации
   (TN Gantt §6.4). Линия и граница внутри полосы — ОДНА величина,
   а не две независимые; иначе на снимке они разойдутся. */
/* ЛИНИЯ И ЗАТЕНЕНИЕ — ДЕТИ ПОЛОТНА, А НЕ СТРОКИ. Внутри строки они
   получили бы её содержащий блок и пересекали бы одну строку вместо
   всей высоты. Найдено на экране: линия «сегодня» оказалась огрызком
   высотой 28 px (D-054). */
/* Z-INDEX У ЛИНИИ НЕТ НАМЕРЕННО. Он был равен `--z-sticky`, то есть
   значению липкой таблицы, а `.tl-canvas` контекста наложения не создаёт —
   значит при равенстве решал порядок в дереве, и линия проехала бы
   ПОВЕРХ колонки названий, как только появится горизонтальная прокрутка.
   Поверх затенения `.tl-off` она идёт и так: оба абсолютны, линия ниже
   по разметке (D-058). */
.tn .tl-asof { position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--accent); z-index: calc(var(--z-sticky) + 1); }

/* Нерабочие интервалы — фоном, а не рамкой: задача, приколотая
   на выходной, начинается ВНУТРИ затенения, и это верно, а не ошибка.

   ВЫХОДНОЙ И ПРАЗДНИК — РАЗНЫЕ ВЕЩИ, и различаются глубиной тона
   одной шкалы, а не оттенком: оттенок закреплён за отклонением
   (D-010), и потратить его на календарь значит перекрасить полотно
   вокруг полос, которые этим же оттенком и кодируются.

   ТОНА ОДНОГО НЕДОСТАТОЧНО, ЧТОБЫ СКАЗАТЬ, КАКОЙ ЭТО ПРАЗДНИК,
   и §9 запрещает цвет как единственный канал: праздник обязан быть
   назван в ярусе праздников — `.tl-holiday-name` (D-092, D-095). */
.tn .tl-off { position: absolute; top: 0; bottom: 0;
  background: var(--tl-off-c); }
/* На годовом масштабе праздник — один день из 365, то есть меньше
   пикселя. Полоса нулевой ширины не полоса: минимум удерживает знак
   видимым, и он превращается в тонкую линию сам собой (D-095). */
.tn .tl-off--holiday { background: var(--tl-holiday-c); min-width: 1px; }
/* ИМЯ ПРАЗДНИКА — В ЯРУСЕ ПЕРИОДА, А НЕ В ЯРУСЕ ДАТ. В ярусе дат оно
   вставало на своё же число и на соседнее: два текста в одной точке
   спорят, и проигрывает тот, что нужнее, — само число. Ярус выше
   свободен, и там подпись ничего не перекрывает (D-093). */
/* Имя праздника живёт в своём ярусе и центрируется по своей дате.
   Общее правило яруса его уже располагает; здесь только начертание. */
.tn .tl-holiday-name { font-weight: 600; }

/* --- ПОДПИСИ ИНТЕРВАЛОВ, СТОЯЩИХ ВПЛОТНУЮ (D-223, заявка TN Gantt) ---
   D-098 отказался ограничивать ширину подписи праздника: «у месяца
   и праздника деление широкое, обрезать их имена нечем и незачем».
   Для ОДИНОЧНОГО праздника верно. Для нескольких подряд — нет:
   1–8 января, каникулы разрезаны Рождеством на два отрезка, три
   подписи в восьми днях, все с `nowrap` и центрированием по своей
   дате. Наложение по построению.

   РАЗВОДИТСЯ ПО СОСЕДСТВУ, А НЕ ПО ШИРИНЕ ТЕКСТА. Ширину текста
   на сервере не измерить, а считать по числу символов — то самое
   «предполагать вместо мерить», которое ошибается тихо и только
   у части читателей (D-133). Их формулировка, и она верна.

   Но разводить и не нужно по ширине: СОСЕДСТВО ОТРЕЗКОВ — это данные,
   и они у продукта есть. Подписи соприкасающихся интервалов идут
   через один уровень: чётные наверху, нечётные ниже. Ярус получает
   двойную высоту ТОЛЬКО когда развод применён — иначе шкала растёт
   у всех ради случая, который бывает в январе.

   ЧЕГО ЭТО НЕ РЕШАЕТ, И ЭТО СКАЗАНО ВСЛУХ: подпись, которая шире
   своего отрезка ВДВОЕ, наложится и через уровень. Такой случай
   требует измерения, измерения нет, и правило на него не
   распространяется. */
.tn .tl-tier--holiday.is-stacked { height: calc(var(--tl-tier-h) * 2); }
.tn .tl-tier--holiday.is-stacked > .tl-holiday-name--low { top: var(--tl-tier-h); }
