/* ============================================================================
   UI-KIT — слой компонентов поверх слоя токенов (--t-* из style.css).
   Витрина: /kit (templates/kit.html), проверка: tools/ui_lint.py.

   ДВА ПРАВИЛА ЭТОГО ФАЙЛА, их проверяет линтер и tests/test_ui_kit.py:

   1. Внутри правил .u-* НЕТ литералов цвета (#hex / rgb / rgba). Только
      var(--…). Литералы живут исключительно в блоке токенов ниже.
   2. Внутри .u-* НЕТ дублей body.light. Тема живёт в токенах; компонент
      о теме не знает. Исключение — не цвет, а другая механика (например
      filter: invert() для чёрно-белых <img>-иконок).

   Почему так: в style.css 346 правил (17%) — ручные body.light-дубли, и оба
   уже случившихся бага (иконка «Сменить роль» чёрная в тёмной теме; «Отмена»
   белым по белому в светлой) родились там, где цвет писали руками дважды.
   Компонент, не знающий о теме, не может рассинхронизироваться.

   Значения не выдуманы: сняты с фактических правил style.css по частоте.
   Например размеры кнопок-иконок 28/26/50/36px и радиусы 6px/50% — это моды
   49 существующих правил; padding текстовых кнопок 8px 16px и радиус 8px —
   моды 90 правил. Где мода расходилась на 1–2px, взята она, а не среднее.
   ========================================================================= */

/* ---- Токены кит-слоя. Единственное место с литералами. ------------------ */
:root {
    /* Размеры контролов — по частоте в существующих правилах. */
    --u-control-2xs: 22px;
    --u-control-xs: 28px;
    --u-control-sm: 36px;
    --u-control-md: 44px;
    --u-control-lg: 50px;

    /* Радиусы. 6px — мода квадратных кнопок, 8px — мода текстовых. */
    --u-radius-xs: 4px;
    --u-radius-sm: 6px;
    --u-radius-md: 8px;
    --u-radius-lg: 12px;
    --u-radius-xl: 16px;
    --u-radius-pill: 999px;

    /* Отступы. */
    --u-gap-xs: 4px;
    --u-gap-sm: 8px;
    --u-gap-md: 12px;
    --u-gap-lg: 18px;
    --u-gap-xl: 24px;

    /* Типографика — по частоте: 13px основной размер интерфейса. */
    --u-font-xs: 11px;
    --u-font-sm: 12px;
    --u-font-md: 13px;
    --u-font-lg: 14px;
    --u-font-xl: 16px;

    --u-speed: .15s;
    --u-speed-slow: .3s;

    /* Слои. Модалки в проекте живут в диапазоне 10000–31500; кит фиксирует
       шкалу, чтобы новые слои не назначались наугад. Подтверждения — выше
       всей этой стопки, см. --u-z-confirm. */
    --u-z-sticky: 50;
    --u-z-dropdown: 5000;
    --u-z-modal: 10000;
    /* Оффканвас выше модалки намеренно: из панели узла открываются свои
       модалки, и они всплывают ещё выше (31000). Так этот слой и сложился. */
    --u-z-offcanvas: 30000;
    /* Список селектора рисуется в слое над страницей, а открывают его и из
       модалки, и из оффканваса — значит он выше обоих. Но НЕ выше уведомлений:
       тост показывают поверх всего, и перекрывать его списком нельзя. */
    --u-z-select-list: 30100;
    /* Подтверждение — поверх ВСЕГО, из чего его можно позвать. Самая высокая
       карточка контента — #member-profile-modal (31500), и вопрос «снять
       связку?», заданный из неё, обязан лечь выше: подтверждают то, что видят.
       Ниже уведомлений и сканера: те перекрывают и подтверждение. */
    --u-z-confirm: 32000;
    --u-z-toast: 40000;
    --u-z-critical: 45000;

    /* Тени. */
    --u-shadow-sm: 0 4px 18px rgba(0, 0, 0, .18);
    --u-shadow-md: 0 10px 30px rgba(0, 0, 0, .4);
    --u-shadow-lg: 0 12px 48px rgba(0, 0, 0, .5);
    /* Тень боковой панели: падает ВЛЕВО, на страницу под ней — поэтому
       отдельный токен, а не --u-shadow-md со смещением вниз. */
    --u-shadow-panel: -8px 0 30px rgba(0, 0, 0, .4);

    /* Тоны состояний. Начинались как пара из существующего решения проекта
       (.gc-header-status.is-online: #36d67b тёмная / #1e9a56 светлая).

       ТОН ТЕКСТА И ТОН ЗАЛИВКИ — РАЗНЫЕ РОЛИ, и разведены они не для
       красоты: текст меряется на подложке страницы, заливка — на собственном
       глифе, и одно значение не проходит обе нормы. Светлая половина той
       исходной пары как раз и уехала в -fill: под цифрой она держит 4.82:1,
       а текстом на белом листе давала 3.61. Где значение совпало (success
       тёмной темы), пара всё равно объявлена: роль читается по имени, а не
       угадывается по совпадению. */
    --u-tone-success: #36d67b;
    --u-tone-success-fill: #36d67b;
    /* Зелёный ПОД БЕЛОЙ надписью — отдельный тон, а не тот же светлее/темнее:
       белым по #36d67b 1.90, надпись пропадает; по #177f47 — 5.04. */
    --u-tone-success-deep: #177f47;
    --u-tone-warn: #ff8c00;
    --u-tone-danger: #e6423e;
    --u-tone-danger-fill: #e22621;
    --u-tone-live: #b08fff;
    --u-tone-live-rgb: 176, 143, 255;
    --u-tone-muted: #6b7480;
    /* Кадр загружаемой фотографии. Тон здесь НЕ тематический: подложку целиком
       перекрывает чужой снимок, а обводку выбирают против него — белая линия с
       тёмной тенью внутрь читается и на светлом кадре, и на тёмном. Поэтому
       значения одни на обе темы. */
    --u-imgmask-bg: #000000;
    --u-imgmask-frame: rgba(255, 255, 255, .9);
    --u-imgmask-frame-edge: rgba(0, 0, 0, .45);
    /* Текст на залитой акцентом/тоном подложке. Акцент тёмный в обеих темах
       (#f5322f / #bd0503), поэтому белый здесь — не тема, а контраст. */
    --u-on-accent: #ffffff;
    /* Глиф на СВЕТЛОЙ заливке (жёлтой, зелёной): белый на ней не читается.
       Тем же #1a1a1a волна 12 починила счётчик на оранжевом — 2.33 -> 7.46. */
    --u-on-tone: #1a1a1a;
    /* Значок календаря/часов в полях даты и времени рисует САМ браузер, чёрным,
       и покрасить его нельзя — только инвертировать. Отсюда не цвет, а признак:
       на тёмной теме инвертируем (значок становится светлым), на светлой нет. */
    --u-date-icon-invert: 1;
    /* Акцент как ПОДПИСЬ на собственной заливке. Не сам --t-accent: он даёт
       на подложке 4.76:1 без заливки вовсе, и тинт любой плотности уводит
       подпись ниже нормы. Полтона светлее в тёмной теме и полтона темнее в
       светлой; оттенок тот же. Проверено на трёх плотностях тинта (.10 у
       --ghost, .18 и .26 у выбранного дня недели): 5.65 / 5.16 / 4.62. */
    --u-tone-accent-text: #f76360;
    /* Заливка плашки под белым текстом. Не сам --t-accent: на #f5322f белый
       даёт 3.9:1, а счётчик это 11px полужирным — норма 4.5. Полтона темнее
       акцента, оттенок тот же. */
    --u-tone-accent: #dc2b28;
    /* Наведение УГЛУБЛЯЕТ заливку, а не осветляет: --t-accent-hover (#ff6360)
       под белой подписью даёт 2.8:1. Светлее акцент стать не может, поэтому
       направление у обеих тем одно — темнее. */
    --u-tone-accent-hover: #ce2422;
    --u-overlay: rgba(0, 0, 0, .7);

}
body.light {
    --u-date-icon-invert: 0;
    /* В светлой теме акцент уже тёмный (#bd0503, белый даёт 6.6:1). */
    --u-tone-accent: var(--t-accent);
    --u-tone-accent-hover: #a90403;
    --u-tone-accent-text: #ab0503;
    --u-tone-success: #1a864b;
    --u-tone-success-fill: #1e9a56;
    --u-tone-success-deep: #1a864b;   /* белым 4.61 */
    --u-tone-warn: #c96a00;
    --u-tone-danger: #c43333;
    --u-tone-danger-fill: #c43333;
    --u-tone-live: #7c4dff;
    --u-tone-live-rgb: 124, 77, 255;
    --u-tone-muted: #6e767f;
    --u-shadow-sm: 0 4px 18px rgba(0, 0, 0, .08);
    --u-shadow-md: 0 10px 30px rgba(0, 0, 0, .12);
    --u-shadow-lg: 0 12px 48px rgba(0, 0, 0, .16);
    --u-shadow-panel: -8px 0 30px rgba(0, 0, 0, .16);
    --u-overlay: rgba(0, 0, 0, .45);
}

/* ---- .u-icon-btn — кнопка-иконка ---------------------------------------- *
   Самый массовый скелет проекта: 132 правила «кнопка», один и тот же набор
   свойств повторён 26 раз.

   ОБЯЗАТЕЛЬНОЕ color: у button UA-таблица браузера ставит color: buttontext,
   наследование от body обрывается — и инлайновый SVG со stroke="currentColor"
   становится чёрным. Ровно так пропадала кнопка «Сменить роль» в тёмной теме.

   Настройка — через локальные переменные, а не переопределением свойств:
   потомку достаточно задать --u-icon-btn-color-hover, состояния :hover/:active
   продолжают работать сами.

   Размер:  --2xs (22) | --xs (28) | по умолчанию --sm (36) | --md (44) | --lg (50)
   Форма:   --round (круг) | --flat (без скругления и подложки)
   Тон:     --tone-accent | --tone-success | --tone-live | --tone-danger
   ------------------------------------------------------------------------ */
/* Псевдонимов у кнопки-значка не осталось: класс .u-icon-btn несёт разметка —
   тринадцать мест в шаблоне и двенадцать в js.js. Список был реестром долга и
   закрылся. Легаси-правила сохранили только СВОЁ: размер, где он не совпадает
   с токеном, фон, цвет и скругление; шпильки box-sizing: content-box, которые
   стояли ровно затем, чтобы отменить border-box компонента, сняты — правила
   теперь объявляют внешний размер честно.
   Два имени из списка (.tokens-buy-btn, .dash-tokens-info-btn) оказались
   мёртвыми: разметки для них нет нигде, правила удалены. */
.u-icon-btn {
    --u-icon-btn-size: var(--u-control-sm);
    --u-icon-btn-radius: var(--u-radius-sm);
    --u-icon-btn-color: var(--t-text-muted);
    --u-icon-btn-color-hover: var(--t-text);
    --u-icon-btn-bg: transparent;
    --u-icon-btn-bg-hover: var(--t-input-bg-focus);
    --u-icon-btn-border: transparent;

    position: relative;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: var(--u-icon-btn-size);
    height: var(--u-icon-btn-size);
    padding: 0;
    border: 1px solid var(--u-icon-btn-border);
    border-radius: var(--u-icon-btn-radius);
    background: var(--u-icon-btn-bg);
    color: var(--u-icon-btn-color);
    cursor: pointer;
    transition: color var(--u-speed), background var(--u-speed),
                border-color var(--u-speed), box-shadow var(--u-speed);
}
/* Размер иконки НЕ навязываем: у инлайновых SVG он задан атрибутами
   width/height на месте вызова. Правило здесь перебило бы их и сдвинуло
   вёрстку при переводе существующих кнопок на компонент. */
.u-icon-btn > img { object-fit: contain; }

.u-icon-btn:hover:not(:disabled) {
    background: var(--u-icon-btn-bg-hover);
    color: var(--u-icon-btn-color-hover);
}
.u-icon-btn:focus-visible {
    outline: 2px solid rgba(var(--t-accent-rgb), .5);
    outline-offset: 1px;
}
.u-icon-btn:disabled {
    opacity: .45;
    cursor: default;
}
.u-icon-btn.is-active,
.u-icon-btn[aria-pressed="true"] {
    color: var(--u-icon-btn-color-hover);
    background: var(--u-icon-btn-bg-hover);
}

.u-icon-btn--2xs { --u-icon-btn-size: var(--u-control-2xs); }
.u-icon-btn--xs  { --u-icon-btn-size: var(--u-control-xs); }
.u-icon-btn--md  { --u-icon-btn-size: var(--u-control-md); }
.u-icon-btn--lg  { --u-icon-btn-size: var(--u-control-lg); }

/* Форма и подложка — РАЗНЫЕ вещи, и модификаторы у них разные. Волна 9
   отказалась от прежнего общего --round на семи голых значках именно из-за
   подложки, а круг им был нужен; волне 22 понадобилась подложка без круга —
   квадратные кнопки строки ввода. Круглый контрол с заливкой собирается
   композицией: --circle --filled. */
.u-icon-btn--circle { --u-icon-btn-radius: 50%; }
.u-icon-btn--filled {
    --u-icon-btn-border: var(--t-border);
    --u-icon-btn-bg: var(--t-surface);
}
.u-icon-btn--filled:hover:not(:disabled) { border-color: var(--t-border-strong); }
.u-icon-btn--flat { --u-icon-btn-radius: 0; --u-icon-btn-bg-hover: transparent; }

.u-icon-btn--tone-accent  { --u-icon-btn-color-hover: var(--t-accent); }
.u-icon-btn--tone-success { --u-icon-btn-color-hover: var(--u-tone-success); }
.u-icon-btn--tone-live    { --u-icon-btn-color-hover: var(--u-tone-live); }
.u-icon-btn--tone-danger  { --u-icon-btn-color-hover: var(--u-tone-danger); }
/* ---- .u-btn — текстовая кнопка ------------------------------------------ *
   90 правил в style.css. Моды: padding 8px 16px, радиус 8px, шрифт 13px,
   фон прозрачный (41 из 90) — поэтому базовый вариант «тихий», а заливка
   включается модификатором.

   Вид:    по умолчанию (тихая) | --primary | --ghost | --danger
   Размер: --sm | по умолчанию | --lg
   Ширина: --block (на всю строку)
   ------------------------------------------------------------------------ */
.u-btn {
    --u-btn-pad: var(--u-gap-sm) var(--u-gap-lg);
    --u-btn-font: var(--u-font-md);
    --u-btn-color: var(--t-text);
    --u-btn-bg: transparent;
    --u-btn-bg-hover: var(--t-surface-strong);
    --u-btn-border: var(--t-border);

    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--u-gap-sm);
    padding: var(--u-btn-pad);
    border: 1px solid var(--u-btn-border);
    border-radius: var(--u-radius-md);
    background: var(--u-btn-bg);
    color: var(--u-btn-color);
    font-family: inherit;
    font-size: var(--u-btn-font);
    font-weight: 500;
    line-height: 1.2;
    cursor: pointer;
    transition: background var(--u-speed), border-color var(--u-speed),
                color var(--u-speed), box-shadow var(--u-speed);
}
.u-btn:hover:not(:disabled) { background: var(--u-btn-bg-hover); }
.u-btn:focus-visible {
    outline: 2px solid rgba(var(--t-accent-rgb), .5);
    outline-offset: 1px;
}
.u-btn:disabled { opacity: .45; cursor: default; }

/* Заливка — НЕ --t-accent: белая подпись на нём даёт 3.90:1 в тёмной теме.
   Полтона темнее, оттенок тот же — тот самый --u-tone-accent, который для
   плашки-счётчика в ките уже был заведён, а самой заметной кнопке продукта
   не достался. */
.u-btn--primary {
    --u-btn-color: var(--u-on-accent);
    --u-btn-bg: var(--u-tone-accent);
    --u-btn-bg-hover: var(--u-tone-accent-hover);
    --u-btn-border: transparent;
}
.u-btn--ghost {
    --u-btn-color: var(--u-tone-accent-text);
    --u-btn-bg: rgba(var(--t-accent-rgb), .1);
    --u-btn-bg-hover: rgba(var(--t-accent-rgb), .2);
    --u-btn-border: rgba(var(--t-accent-rgb), .35);
}
/* Утвердительное действие, не акцентное: «Активировать». Тон тот же, что у
   счётчика успеха, и берётся из токена — в светлой теме он темнее. */
.u-btn--success {
    --u-btn-color: var(--u-tone-success);
    --u-btn-bg: transparent;
    --u-btn-bg-hover: rgba(var(--t-accent-rgb), .12);
    --u-btn-border: var(--u-tone-success);
}
.u-btn--danger {
    --u-btn-color: var(--u-tone-danger);
    --u-btn-bg: transparent;
    --u-btn-bg-hover: rgba(var(--t-accent-rgb), .12);
    --u-btn-border: var(--u-tone-danger);
}
/* «Добавить ещё один»: пунктир читается как пустое место под следующий
   элемент, а не как готовое действие. Четыре места изобрели его порознь. */
.u-btn--add {
    --u-btn-color: var(--t-accent);
    --u-btn-bg: transparent;
    --u-btn-bg-hover: rgba(var(--t-accent-rgb), .1);
    --u-btn-border: rgba(var(--t-accent-rgb), .45);
    border-style: dashed;
}
.u-btn--sm { --u-btn-pad: var(--u-gap-xs) var(--u-gap-md); --u-btn-font: var(--u-font-sm); }
.u-btn--lg { --u-btn-pad: var(--u-gap-md) var(--u-gap-xl); --u-btn-font: var(--u-font-lg); }
.u-btn--block { display: flex; width: 100%; }
/* Таблетка. Кнопка, которая всплывает поверх страницы (плашка обновления) или
   стоит отдельной парой (ответить/отклонить звонок), читается как самостоятельный
   предмет, а не как часть формы. */
.u-btn--pill { border-radius: var(--u-radius-pill); }

/* Значок слева от подписи. line-height: 0 — иначе строка svg добавляет
   лишний пиксель снизу и подпись перестаёт стоять по центру. */
.u-btn__ico { display: inline-flex; line-height: 0; flex: 0 0 auto; }
.u-btn__ico > svg { display: block; }

/* ---- .u-field / .u-input — поля формы ------------------------------------ *
   14 правил ввода. Моды: padding 10px 14px, радиус 8px, шрифт 14px.
   Цвет и фон — из токенов ввода (--t-input-*), они уже есть в style.css.
   ------------------------------------------------------------------------ */
.u-field { display: flex; flex-direction: column; gap: var(--u-gap-xs); }
.u-field__label {
    font-size: var(--u-font-sm);
    color: var(--t-text-muted);
}
.u-field__hint { font-size: var(--u-font-xs); color: var(--t-text-muted); }
.u-field__error { font-size: var(--u-font-xs); color: var(--u-tone-danger); }

/* Псевдонимов у поля ввода не осталось: класс .u-input несёт разметка — 27
   мест в шаблоне и 42 в js.js. Два имени из списка были селекторами-ПОТОМКОВ
   (.profile-field input, .auth-field input, .group-chat-input-area textarea):
   у них класс пришлось поставить на каждый элемент внутри контейнера, включая
   те, что строит js.js. Свои padding, фон, рамку, размер шрифта и цвет поля
   сохранили — это их отличия; от компонента идут box-sizing, ширина, семейство
   шрифта и переход. */
.u-input {
    box-sizing: border-box;
    width: 100%;
    padding: var(--u-gap-sm) var(--u-gap-md);
    border: 1px solid var(--t-input-border);
    border-radius: var(--u-radius-md);
    background: var(--t-input-bg);
    color: var(--t-text);
    font-family: inherit;
    font-size: var(--u-font-lg);
    transition: border-color var(--u-speed), background var(--u-speed);
}
.u-input::placeholder { color: var(--t-text-muted); }
/* Значок вызова календаря: браузерный, чёрный, на тёмной теме сливался с полем.
   Инверсия — единственный способ его перекрасить, поэтому она и в токене. */
.u-input::-webkit-calendar-picker-indicator {
    filter: invert(var(--u-date-icon-invert, 1));
    opacity: .65;
    cursor: pointer;
}
.u-input::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.u-input:focus {
    outline: none;
    border-color: var(--t-input-border-focus);
    background: var(--t-input-bg-focus);
}
.u-input:disabled { opacity: .5; cursor: default; }
.u-input--invalid { border-color: var(--u-tone-danger); }
textarea.u-input { min-height: 84px; resize: vertical; }

/* ---- .u-modal — модалка -------------------------------------------------- *
   В проекте минимум четыре независимые реализации (companies-modal,
   gcal-cyclical-modal, image-modal, role-picker). Кит сводит их к одной
   раскладке: оверлей → коробка → шапка / тело / действия.
   ------------------------------------------------------------------------ */
/* Псевдонимов у модалки не осталось: разметка ВСЕХ оверлеев проекта несёт
   .u-modal сама — часть ставит фабрика, часть шаблон. Список был реестром
   долга и закрылся. Свои z-index, фон и display:none места сохранили в
   style.css и перебивают компонент; blur теперь у всех, и это намеренное
   выравнивание вида (у .image-modal и .call-overlay он был свой и остался). */
.u-modal {
    position: fixed;
    inset: 0;
    z-index: var(--u-z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--u-gap-lg);
    background: var(--u-overlay);
    backdrop-filter: blur(6px);
}
.u-modal__box {
    box-sizing: border-box;
    width: 100%;
    max-width: 480px;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--t-border);
    border-radius: var(--u-radius-lg);
    background: var(--t-panel);
    color: var(--t-text);
    box-shadow: var(--u-shadow-lg);
    overflow: hidden;
}
.u-modal__box--wide { max-width: 760px; }
/* Коробка под список: реестры участников и админка тренеров. 560px — между
   диалогом (480) и широкой коробкой (760): строка «аватар + имя + статус»
   в 480 уже жмётся, а 760 для одной колонки избыточны. */
.u-modal__box--list { max-width: 560px; }
/* Подзаголовок под шапкой — название мероприятия под «Участники». */
.u-modal__sub {
    margin: 0 0 var(--u-gap-md);
    font-size: var(--u-font-sm);
    color: var(--t-text-muted);
}
.u-modal__head {
    display: flex;
    align-items: center;
    gap: var(--u-gap-md);
    flex-shrink: 0;
    padding: var(--u-gap-lg);
    border-bottom: 1px solid var(--t-border);
    font-size: var(--u-font-xl);
    font-weight: 500;
}
/* Крестик модалки — 28px кнопка и глиф 20px. Это размер закрывающего крестика
   в панелях («События», «Профиль» — .closed) и в окне чата
   (.group-chat-header-close); модалка закрывается тем же движением, значит и
   выглядеть должна так же. Своим шрифт задан ЯВНО: без него глиф наследовал
   размер заголовка шапки (--u-font-xl, 16px) и был заметно мельче остальных. */
.u-modal__head .u-icon-btn {
    margin-left: auto;
    --u-icon-btn-size: var(--u-control-xs);
    font-size: 20px;
    line-height: 1;
}
/* Коробка — флекс-колонка с потолком 90vh, и части делят её высоту. Шапка и
   строка действий НЕ сжимаются: flex-shrink у флекс-элемента по умолчанию 1, и
   при длинном теле их съедало бы вместе с ним. Тело, наоборот, обязано уметь
   стать НИЖЕ своего содержимого: без min-height: 0 флекс-элемент с overflow не
   сжимается ниже него — прокрутка не появляется, а коробка вылезает за экран.
   Легаси-раскладка модалки события знала оба правила, компонент их не знал. */
.u-modal__body {
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--u-gap-lg);
    overflow-y: auto;
    font-size: var(--u-font-md);
    line-height: 1.6;
}
.u-modal__actions {
    display: flex;
    gap: var(--u-gap-sm);
    flex-shrink: 0;
    justify-content: flex-end;
    padding: var(--u-gap-md) var(--u-gap-lg) var(--u-gap-lg);
}

/* ---- .u-notice — короткое уведомление ------------------------------------ *
   Модалка с одной кнопкой: акцентный заголовок, текст, «Закрыть». Отличий от
   диалога всего два, поэтому здесь только они — остальное делает .u-modal.
   Уже диалога: это уведомление, длинной строки в нём не бывает.
   Слой (z-index) остаётся за местом вызова: уведомление всплывает поверх той
   модалки, из которой пришло, а «поверх» у каждой своё.                     */
.u-modal__box--notice { max-width: 380px; }
.u-notice__title {
    margin: 0;
    font-size: var(--u-font-md);
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--u-tone-accent);
    font-weight: 700;
}
.u-notice__msg { color: var(--t-text); }
/* Нумерованные шаги в уведомлении: раньше стояли инлайновым cssText. */
.u-notice__steps { margin: 0; padding-left: 1.2em; line-height: 1.6; }
/* Пояснение под заголовком шага: шаг называется коротко, а объяснять
   иногда приходится в две строки. */
.u-notice__steps > li + li { margin-top: var(--u-gap-sm); }
.u-notice__step-text { color: var(--t-text-muted); font-size: var(--u-font-sm); }

/* ---- .u-offcanvas — выезжающая панель ------------------------------------ *
   Четыре места показывают не диалог, а панель, приезжающую справа: карточка
   узла федерации, создание сущности, сводка по сущности, карточка тренера.
   От модалки её отличает не оформление, а поведение — она УЕЗЖАЕТ, и пока
   едет, узел ещё в документе. Поэтому фон гасится opacity, а не display:
   с display переходу не с чего начинать.
   ------------------------------------------------------------------------ */
.u-offcanvas {
    position: fixed;
    inset: 0;
    z-index: var(--u-z-offcanvas);
    background: var(--u-overlay);
    backdrop-filter: blur(6px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--u-speed-slow) ease;
}
.u-offcanvas.is-open { opacity: 1; pointer-events: auto; }
/* Панель позиционируется absolute от padding-box оверлея — отступа у него
   поэтому нет вовсе: он отодвинул бы панель от края экрана. */
.u-offcanvas__panel {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    box-sizing: border-box;
    width: 380px;
    max-width: 92vw;
    padding: var(--u-gap-xl);
    overflow-x: hidden;
    overflow-y: auto;
    border-left: 1px solid var(--t-border);
    background: var(--t-panel);
    box-shadow: var(--u-shadow-panel);
    transform: translateX(100%);
    transition: transform var(--u-speed-slow) ease;
}
.u-offcanvas.is-open .u-offcanvas__panel { transform: translateX(0); }
/* Крестик стоит в углу панели, а не в строке-шапке, как у модалки: панель
   прокручивается целиком, и шапка уехала бы вместе с содержимым.
   Размер — общий для всего проекта: кнопка 28px, глиф 20px. Так закрываются
   панели «События» и «Профиль» (.closed), окно чата и модалки. Прежние 36/26
   делали кит-оффканвасы единственным местом с крупным крестиком. */
.u-offcanvas__close {
    position: absolute;
    top: var(--u-gap-sm);
    right: var(--u-gap-sm);
    z-index: 1;
    --u-icon-btn-size: var(--u-control-xs);
    font-size: 20px;
    line-height: 1;
}

/* ---- .u-badge — счётчик и метка ------------------------------------------ *
   17 правил. Моды: радиус 999px/10px, padding 0 6px, шрифт 10px,
   min-width 20px, белый текст на заливке.
   ------------------------------------------------------------------------ */
/* Псевдонимов у счётчика не осталось: класс .u-badge несёт разметка — два
   места в шаблоне и шесть в js.js. Свои размеры, фон и кегль легаси-правила
   сохранили (это их отличия) и перебивают компонент: он подключён РАНЬШЕ. */
.u-badge {
    --u-badge-bg: var(--u-tone-accent);
    --u-badge-color: var(--u-on-accent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    min-width: 20px;
    height: 20px;
    padding: 0 var(--u-gap-xs);
    border-radius: var(--u-radius-pill);
    background: var(--u-badge-bg);
    color: var(--u-badge-color);
    font-size: var(--u-font-xs);
    font-weight: 700;   /* мода 8 легаси-правил, не 500 */
    line-height: 1;
}
/* Цвет цифры выбирается ВМЕСТЕ с заливкой, а не задаётся раз навсегда: на
   жёлтой и зелёной белая цифра не читается (2.33 и 1.90), на красной и серой
   не читается тёмная. Пары замерены, каждая держит норму. */
.u-badge--warn    { --u-badge-bg: var(--u-tone-warn);
                    --u-badge-color: var(--u-on-tone); }
.u-badge--success { --u-badge-bg: var(--u-tone-success-fill);
                    --u-badge-color: var(--u-on-tone); }
.u-badge--danger  { --u-badge-bg: var(--u-tone-danger-fill); }
.u-badge--muted   { --u-badge-bg: var(--u-tone-muted); }
/* Скрытый счётчик. Без этого правила display: inline-flex компонента
   перебивает [hidden] из UA-таблицы, и пустая плашка остаётся видимой —
   ui.setBadge прячет счётчик именно так, а не инлайновым display. */
.u-badge[hidden] { display: none; }
.u-badge--dot { min-width: 10px; width: 10px; height: 10px; padding: 0; }

/* ---- .u-chip — фильтр/метка с действием ---------------------------------- */
.u-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--u-gap-xs);
    /* Метка не переносится: таблетка — пилюля, и «не / хватает» в две строки
       превращает её в кляксу, а строку рядом растягивает по высоте. Все метки
       в проекте короткие (состояния документов, фильтры), так что запрет
       переноса ничего не обрезает. */
    white-space: nowrap;
    padding: var(--u-gap-xs) var(--u-gap-md);
    border: 1px solid var(--t-border);
    border-radius: var(--u-radius-pill);
    background: var(--t-surface);
    color: var(--t-text-muted);
    font-size: var(--u-font-sm);
    cursor: pointer;
    transition: color var(--u-speed), border-color var(--u-speed),
                background var(--u-speed);
}
.u-chip:hover { color: var(--t-text); border-color: var(--t-border-strong); }
.u-chip.is-active {
    color: var(--u-tone-accent-text);
    border-color: rgba(var(--t-accent-rgb), .5);
    background: rgba(var(--t-accent-rgb), .1);
}
/* Метка состояния: то же тело, но без действия. Курсор-рука и подсветка на
   наведении обещают нажатие, которого нет, — и человек его ищет. */
.u-chip--static { cursor: default; }
.u-chip--static:hover { color: var(--t-text-muted); border-color: var(--t-border); }
/* Тон повторён для :hover намеренно: правило .u-chip:hover иначе перебивает
   цвет метки при наведении и все состояния сереют в одно. */
.u-chip--success, .u-chip--success:hover {
    color: var(--u-tone-success); border-color: var(--u-tone-success);
}
.u-chip--warn, .u-chip--warn:hover {
    color: var(--u-tone-warn); border-color: var(--u-tone-warn);
}
.u-chip--danger, .u-chip--danger:hover {
    color: var(--u-tone-danger); border-color: var(--u-tone-danger);
}
/* Короткое состояние («в ожидании») — заливкой, а не обводкой: одним тоном и
   белой надписью оно читается с одного взгляда и не спорит с именем рядом. */
.u-chip--sm { padding: 1px var(--u-gap-sm); font-size: var(--u-font-xs); }
.u-chip--solid, .u-chip--solid:hover {
    background: var(--u-chip-fill, var(--u-tone-muted));
    border-color: transparent;
    color: var(--u-on-accent);
}
/* Тон заливки выбирается ВМЕСТЕ с белой надписью — как у счётчика. Зелёный идёт
   не тот, которым пишут (#36d67b, белым 1.90), а глубокий: состояния «в ожидании»
   и «подтверждён» стоят в одной колонке и обязаны читаться одинаково. Оранжевого
   среди заливок нет: белым по нему 2.33, и глубокого тона под него не мерили. */
.u-chip--solid.u-chip--muted   { --u-chip-fill: var(--u-tone-muted); }
.u-chip--solid.u-chip--success { --u-chip-fill: var(--u-tone-success-deep); }
.u-chip--solid.u-chip--danger  { --u-chip-fill: var(--u-tone-danger-fill); }
.u-chip--solid.u-chip--accent  { --u-chip-fill: var(--u-tone-accent); }

/* ---- .u-select — выпадающий список --------------------------------------- *
   Кнопка со стрелкой и свой список: нативную выпадашку браузеры рисуют каждый
   по-своему и мимо темы. Нативный <select> остаётся внутри и держит значение.
   ------------------------------------------------------------------------ */
.u-select { position: relative; }
.u-select__native { display: none !important; }
.u-select__button {
    width: 100%;
    padding-right: 28px;
    text-align: left;
    font-family: inherit;
    font-size: var(--u-font-md);
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.u-select__button::after {
    content: '';
    position: absolute;
    top: 50%;
    right: 14px;
    width: 6px;
    height: 6px;
    border-right: 1px solid currentColor;
    border-bottom: 1px solid currentColor;
    transform: translateY(-70%) rotate(45deg);
    opacity: .6;
    pointer-events: none;
    transition: transform var(--u-speed);
}
.u-select.is-open .u-select__button::after { transform: translateY(-30%) rotate(-135deg); }
/* Список рисуется в слое НАД страницей (position: fixed + координаты по
   кнопке): внутри обёртки его резал ближайший предок с прокруткой — в модалке
   это её тело, и список обрывался краем окна. */
.u-select__list {
    display: none;
    position: fixed;
    z-index: var(--u-z-select-list);
    /* Ширину списку задаёт JS по кнопке — и она обязана быть ПОЛНОЙ, вместе с
       рамкой и отступами. Без этого список выходил шире кнопки ровно на них:
       на странице без общего сброса (панель не подключает style.css) свойство
       по умолчанию content-box. */
    box-sizing: border-box;
    /* Ширину задаёт JS переменной — правило места вызова может её перебить
       (например, растянуть список под самый длинный пункт). */
    width: var(--u-select-list-w, auto);
    max-height: 260px;
    margin: 0;
    padding: 6px;
    overflow-y: auto;
    list-style: none;
    border: 1px solid var(--t-border-strong);
    border-radius: var(--u-radius-lg);
    background: var(--t-panel);
    box-shadow: var(--u-shadow-md);
}
/* Показ — по классу САМОГО списка: открытый он лежит в слое над страницей,
   а не внутри обёртки, и потомком .u-select уже не является. */
.u-select__list.is-floating { display: block; }
.u-select__option {
    box-sizing: border-box;
    padding: 8px 12px;
    border-radius: var(--u-radius-md);
    color: var(--t-text-muted);
    font-size: var(--u-font-md);
    cursor: pointer;
    transition: background var(--u-speed), color var(--u-speed);
}
.u-select__option:hover,
.u-select__option.is-selected {
    background: var(--t-surface-strong);
    color: var(--t-text);
}
/* «Пилюля» цвета у пункта: сложность события и уровень аттестации узнают по
   цветному кружку раньше, чем по надписи. Сам цвет — дело продукта, он задаётся
   правилом на [data-value]; кит отвечает за форму и место. */
.u-select__bullet {
    display: inline-block;
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    margin-right: 8px;
    border-radius: var(--u-radius-pill);
    background: var(--t-border-strong);
    vertical-align: middle;
}
/* Пункт в две строки: название и мелкая подпись (кю/дан, расписание). */
.u-select__text { display: inline-flex; flex-direction: column; vertical-align: middle; min-width: 0; }
.u-select__button .u-select__text { max-width: calc(100% - 4px); }
.u-select__main { line-height: 1.2; overflow: hidden; text-overflow: ellipsis; }
.u-select__sub {
    color: var(--t-text-muted);
    font-size: var(--u-font-xs);
    line-height: 1.2;
}
/* Запертый селектор: клик по кнопке уже погашен в JS, здесь только вид. */
.u-select.is-disabled { opacity: .65; }
.u-select.is-disabled .u-select__button { cursor: not-allowed; }

/* ---- .u-date — дата и время в едином формате ------------------------------ *
   Нативные input[type=date|time] браузер рисует по своей локали: в одном окне
   встречались 12/15/2030 и 15.12.2030, а на телефоне ещё и «07:30 PM». Поле
   остаётся внутри и держит значение, наружу выходит кнопка ДД.ММ.ГГГГ / ЧЧ:ММ
   и свой календарь. Всё на токенах — вид одинаков в обеих темах.
   -------------------------------------------------------------------------- */
.u-date { position: relative; }
.u-date__native { display: none !important; }
/* Поле, а не кнопка: дату быстрее набрать с клавиатуры, чем долистать
   календарём. Календарь открывается по клику в это же поле. */
.u-date__field {
    width: 100%;
    text-align: left;
    font-family: inherit;
    font-size: var(--u-font-md);
    cursor: text;
}
.u-date.is-disabled { opacity: .65; }
.u-date.is-disabled .u-date__field { cursor: not-allowed; }
/* Всплывашка рисуется в слое НАД страницей: внутри обёртки её резал бы
   ближайший предок с прокруткой — в модалке это её тело. */
.u-date__pop {
    display: none;
    position: fixed;
    z-index: var(--u-z-select-list);
    box-sizing: border-box;
    padding: 8px;
    border: 1px solid var(--t-border-strong);
    border-radius: var(--u-radius-lg);
    background: var(--t-panel);
    box-shadow: var(--u-shadow-md);
}
.u-date__pop.is-floating { display: block; }
.u-cal { width: 268px; }
.u-cal__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
}
/* Заголовок — кнопка: клик по нему переключает на выбор месяца и года.
   Долистывать до давней даты по месяцу за клик — это десятки кликов. */
.u-cal__title {
    flex: 1 1 auto;
    padding: 4px 6px;
    border: 0;
    border-radius: var(--u-radius-md);
    background: transparent;
    color: var(--t-text);
    font: inherit;
    font-size: var(--u-font-md);
    text-align: center;
    cursor: pointer;
}
.u-cal__title:hover { background: var(--t-surface-strong); }
.u-cal__nav {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: var(--u-radius-md);
    background: transparent;
    color: var(--t-text);
    font: inherit;
    font-size: var(--u-font-lg);
    line-height: 1;
    cursor: pointer;
}
.u-cal__nav:hover { background: var(--t-surface-strong); }
.u-cal__wd,
.u-cal__grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
}
.u-cal__wd {
    margin-bottom: 4px;
    color: var(--t-text-muted);
    font-size: var(--u-font-xs);
    text-align: center;
}
.u-cal__day {
    padding: 7px 0;
    border: 0;
    border-radius: var(--u-radius-md);
    background: transparent;
    color: var(--t-text);
    font: inherit;
    font-size: var(--u-font-sm);
    cursor: pointer;
}
.u-cal__day:hover { background: var(--t-surface-strong); }
/* Дни соседних месяцев показываем, но приглушаем: без них сетка «рвётся», а
   в полную силу они читаются как дни текущего. */
.u-cal__day.is-out { color: var(--t-text-muted); opacity: .55; }
.u-cal__day.is-today { box-shadow: inset 0 0 0 1px var(--t-border-strong); }
.u-cal__day.is-selected {
    background: var(--t-accent);
    color: var(--t-on-accent);
}
/* Двенадцать месяцев года — три в ряд: столько же кликов до нужного, сколько
   до дня, и подпись помещается целиком. */
.u-cal__grid--months { grid-template-columns: repeat(3, 1fr); gap: 4px; }
.u-cal__month {
    padding: 10px 0;
    border: 0;
    border-radius: var(--u-radius-md);
    background: transparent;
    color: var(--t-text);
    font: inherit;
    font-size: var(--u-font-sm);
    cursor: pointer;
}
.u-cal__month:hover { background: var(--t-surface-strong); }
.u-cal__month.is-selected {
    background: var(--t-accent);
    color: var(--t-on-accent);
}
.u-cal__foot,
.u-time__foot {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    margin-top: 6px;
    padding-top: 6px;
    border-top: 1px solid var(--t-border);
}
.u-cal__act {
    padding: 5px 8px;
    border: 0;
    border-radius: var(--u-radius-md);
    background: transparent;
    color: var(--t-text-muted);
    font: inherit;
    font-size: var(--u-font-sm);
    cursor: pointer;
}
.u-cal__act:hover { background: var(--t-surface-strong); color: var(--t-text); }
/* «Ввести» стоит слева и заметнее прочих действий: на телефоне это
   единственный способ набрать дату руками, и искать его глазами не должны. */
.u-cal__act--kb { margin-right: auto; color: var(--t-text); }
/* Время: два столбца-барабана. Часы и минуты списком, а не вводом с клавиш —
   на телефоне попасть пальцем проще, чем печатать. */
/* Раскладку задаём вместе с признаком «всплыла»: показ включает
   .u-date__pop.is-floating (два класса), и одиночная .u-time проигрывала ему
   по весу — столбцы вставали друг под друга вместо двух колонок. */
.u-date__pop.u-time.is-floating { display: flex; }
.u-time { flex-wrap: wrap; gap: 6px; width: 156px; }
.u-time__group { flex: 1 1 0; display: flex; flex-direction: column; min-width: 0; }
.u-time__cap {
    margin-bottom: 4px;
    color: var(--t-text-muted);
    font-size: var(--u-font-xs);
    text-align: center;
}
.u-time__col {
    flex: 1 1 auto;
    max-height: 210px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.u-time__cell {
    padding: 6px 0;
    border: 0;
    border-radius: var(--u-radius-md);
    background: transparent;
    color: var(--t-text);
    font: inherit;
    font-size: var(--u-font-sm);
    text-align: center;
    cursor: pointer;
}
.u-time__cell:hover { background: var(--t-surface-strong); }
.u-time__cell.is-selected {
    background: var(--t-accent);
    color: var(--t-on-accent);
}
.u-time__foot { flex: 1 0 100%; justify-content: flex-end; }

/* ---- .u-password — поле пароля с «глазом» -------------------------------- *
   Кнопка стоит ВНУТРИ поля: отдельной строкой она отодвигала бы форму, а рядом
   — ломала бы ширину поля на узком экране.
   ------------------------------------------------------------------------ */
.u-password { position: relative; display: block; }
.u-password__input { width: 100%; padding-right: 40px; }
.u-password__eye {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: none;
    border-radius: var(--u-radius-sm);
    background: none;
    color: var(--t-text-muted);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    opacity: .65;
}
.u-password__eye:hover { opacity: 1; }
.u-password__eye.is-on { opacity: 1; color: var(--t-text); }

/* ---- .u-imgmask — маска на загружаемой картинке --------------------------- *
   Рамка НЕПОДВИЖНА и держит соотношение места назначения, двигают и приближают
   саму картинку. Так это устроено в мессенджерах: человек уже знает, что
   делать, и это работает пальцем так же, как мышью.
   ------------------------------------------------------------------------ */
.u-imgmask__stage {
    position: relative;
    width: 100%;
    overflow: hidden;
    border-radius: var(--u-radius-lg);
    background: var(--u-imgmask-bg);
    cursor: grab;
    /* Без этого браузер на телефоне забирает жест себе и прокручивает
       страницу вместо того, чтобы двигать картинку. */
    touch-action: none;
    user-select: none;
}
.u-imgmask__stage:active { cursor: grabbing; }
.u-imgmask__img {
    position: absolute;
    top: 0;
    left: 0;
    /* max-width: none — иначе общее правило «картинка не шире контейнера»
       ужимает кадр, и приближение перестаёт работать за пределами рамки. */
    max-width: none;
    transform-origin: 0 0;
    pointer-events: none;
}
/* Обводка кадра: показывает, что попадёт в результат. Тень внутрь — чтобы
   белая линия читалась и на светлом снимке. */
.u-imgmask__frame {
    position: absolute;
    inset: 0;
    border: 2px solid var(--u-imgmask-frame);
    border-radius: var(--u-radius-lg);
    box-shadow: inset 0 0 0 1px var(--u-imgmask-frame-edge);
    pointer-events: none;
}
.u-imgmask__tools {
    display: flex;
    align-items: center;
    gap: var(--u-gap-md);
    margin-top: var(--u-gap-md);
}
.u-imgmask__zoom { flex: 1 1 auto; width: 100%; }
.u-imgmask__hint {
    margin-top: var(--u-gap-sm);
    color: var(--t-text-muted);
    font-size: var(--u-font-sm);
}

/* ---- .u-avatar — аватар и логотип ---------------------------------------- *
   18 правил, 16 из них с border-radius: 50% и object-fit: cover.
   ------------------------------------------------------------------------ */
/* Псевдонимов у аватара не осталось: класс .u-avatar несёт разметка — десять
   мест в js.js и два в шаблоне. Размеры легаси-правила держат сами (они и есть
   отличие), а круг, object-fit и подложку даёт компонент. Одиннадцатое имя,
   .dash-news-modal-avatar, оказалось мёртвым: модалка, которая его ставила,
   удалена ещё в волне 5. */
.u-avatar {
    --u-avatar-size: 32px;
    display: inline-block;
    box-sizing: border-box;
    width: var(--u-avatar-size);
    height: var(--u-avatar-size);
    border-radius: 50%;
    background: var(--t-surface-strong);
    object-fit: cover;
    flex-shrink: 0;
}
.u-avatar--sm { --u-avatar-size: 24px; }
.u-avatar--md { --u-avatar-size: 44px; }
.u-avatar--lg { --u-avatar-size: 60px; }
.u-avatar--xl { --u-avatar-size: 90px; }
.u-avatar--square { border-radius: var(--u-radius-md); }

/* ---- .u-card — контейнер-панель ------------------------------------------ */
.u-card {
    box-sizing: border-box;
    border: 1px solid var(--t-border);
    border-radius: var(--u-radius-lg);
    background: var(--t-panel);
    color: var(--t-text);
    padding: var(--u-gap-lg);
}
.u-card--quiet { background: var(--t-surface); }
.u-card__head {
    display: flex;
    align-items: center;
    gap: var(--u-gap-sm);
    margin-bottom: var(--u-gap-md);
    font-size: var(--u-font-lg);
    font-weight: 500;
}
.u-card__sub { color: var(--t-text-muted); font-size: var(--u-font-sm); }

/* ---- .u-tile — строка списка -------------------------------------------- */
.u-tile {
    display: flex;
    align-items: center;
    gap: var(--u-gap-md);
    box-sizing: border-box;
    width: 100%;
    padding: var(--u-gap-sm) var(--u-gap-md);
    border: 1px solid transparent;
    border-radius: var(--u-radius-md);
    background: var(--t-surface);
    color: var(--t-text);
    font-size: var(--u-font-md);
    text-align: left;
    cursor: pointer;
    transition: background var(--u-speed), border-color var(--u-speed);
}
.u-tile:hover { background: var(--t-surface-strong); border-color: var(--t-border); }
.u-tile.is-active { border-color: rgba(var(--t-accent-rgb), .5); }
.u-tile__main { flex: 1; min-width: 0; }
.u-tile__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.u-tile__sub { color: var(--t-text-muted); font-size: var(--u-font-sm); }

/* ---- .u-tabs — переключатель разделов ------------------------------------ */
.u-tabs { display: flex; gap: var(--u-gap-xs); }
.u-tab {
    display: inline-flex;
    align-items: center;
    gap: var(--u-gap-xs);
    padding: var(--u-gap-sm) var(--u-gap-md);
    border: none;
    border-bottom: 2px solid transparent;
    background: transparent;
    color: var(--t-text-muted);
    font-family: inherit;
    font-size: var(--u-font-md);
    cursor: pointer;
    transition: color var(--u-speed), border-color var(--u-speed);
}
.u-tab:hover { color: var(--t-text); }
.u-tab.is-active {
    color: var(--u-tone-accent-text);
    border-bottom-color: var(--t-accent);
}

/* ---- .u-switch — переключатель ------------------------------------------- */
.u-switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    width: 38px;
    height: 22px;
    border-radius: var(--u-radius-pill);
    background: var(--t-surface-strong);
    border: 1px solid var(--t-border);
    cursor: pointer;
    padding: 0;
    transition: background var(--u-speed), border-color var(--u-speed);
}
.u-switch::after {
    content: "";
    position: absolute;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--t-text-muted);
    transition: transform var(--u-speed), background var(--u-speed);
}
.u-switch[aria-checked="true"],
.u-switch.is-active {
    background: rgba(var(--t-accent-rgb), .25);
    border-color: rgba(var(--t-accent-rgb), .5);
}
.u-switch[aria-checked="true"]::after,
.u-switch.is-active::after {
    transform: translateX(16px);
    background: var(--t-accent);
}

/* ---- .u-empty — пустое состояние ----------------------------------------- */
.u-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--u-gap-sm);
    padding: var(--u-gap-xl);
    color: var(--t-text-muted);
    font-size: var(--u-font-md);
    text-align: center;
}

/* ---- .u-spinner — индикатор загрузки ------------------------------------- */
.u-spinner {
    display: inline-block;
    width: 18px;
    height: 18px;
    border: 2px solid var(--t-border-strong);
    border-top-color: var(--t-accent);
    border-radius: 50%;
    animation: u-spin .8s linear infinite;
}
@keyframes u-spin { to { transform: rotate(360deg); } }

/* ---- .u-pill — плавающая панель кнопок ----------------------------------- *
   Раскладка таблетки #side-panel-btns: контейнер круглых кнопок.
   ------------------------------------------------------------------------ */
.u-pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--u-gap-sm);
    padding: var(--u-gap-sm);
    border: 1px solid var(--t-border);
    border-radius: var(--u-radius-xl);
    background: var(--t-panel);
    box-shadow: var(--u-shadow-md);
    z-index: var(--u-z-sticky);
}
.u-pill--column { flex-direction: column; }

/* ---- .u-stack / .u-row — раскладка --------------------------------------- */
.u-stack { display: flex; flex-direction: column; gap: var(--u-gap-md); }
.u-row { display: flex; align-items: center; gap: var(--u-gap-sm); }
.u-row--between { justify-content: space-between; }
.u-spacer { flex: 1; }

/* ---- .u-text — типографика ----------------------------------------------- */
.u-text-muted { color: var(--t-text-muted); }
.u-text-accent { color: var(--t-accent); }
.u-text-danger { color: var(--u-tone-danger); }
.u-text-success { color: var(--u-tone-success); }
.u-text-sm { font-size: var(--u-font-sm); }
.u-text-xs { font-size: var(--u-font-xs); }
.u-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- ширина оффканваса тянется за левый край ------------------------------
   Полоска лежит ВНУТРИ панели по её левому краю: панель прижата к правому краю
   экрана, и снаружи ей места нет. На узком экране панель и так во всю ширину —
   там полоска скрыта, тянуть нечего. */
.u-offcanvas__resizer {
    position: absolute;
    top: 0;
    left: 0;
    width: 6px;
    height: 100%;
    cursor: ew-resize;
    background: transparent;
    transition: background var(--u-speed) ease;
}
.u-offcanvas__resizer:hover,
.u-offcanvas.is-resizing .u-offcanvas__resizer {
    background: rgba(var(--t-accent-rgb), 0.18);
}
/* Во время тяги переход выключаем: иначе ширина догоняет курсор рывками. */
.u-offcanvas.is-resizing .u-offcanvas__panel { transition: none; }
@media (max-width: 768px) {
    .u-offcanvas__resizer { display: none; }
}
