/* ============================================================================
   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;

    /* Слои. Шкалу фиксирует кит, чтобы новые не назначались наугад. Порядок
       снизу вверх: прилипшая шапка, выпадашка, панель-оффканвас, модалка,
       список селектора, просмотр картинки, подтверждение, уведомление.
       Правило одно: КАЖДЫЙ СЛОЙ ВЫШЕ ТОГО, ИЗ КОТОРОГО ЕГО ОТКРЫВАЮТ. */
    --u-z-sticky: 50;
    --u-z-dropdown: 5000;
    /* Окно (.u-window) живёт НАД страницей, но НИЖЕ модалки: подтверждение,
       вызванное из окна, обязано лечь поверх него. Между оффканвасом и
       модалкой места хватает — окна поднимаются друг над другом шагом
       --u-z-modal-step и до 31000 не достают. */
    --u-z-window: 30500;
    --u-z-offcanvas: 30000;
    /* МОДАЛКА ВЫШЕ ОФФКАНВАСА. Раньше было наоборот (10000 против 30000), и
       каждое окно, открытое из панели узла, уезжало ПОД панель: снаружи это
       выглядело как «кнопка не работает». Чинили поштучно — карточка участника,
       окно массовой загрузки, окно оплаты, окно чатов получили свои 31000
       руками, — и каждое следующее окно начинало с той же поломки, пока кто-то
       не вспоминал про слой. Порядок «модалка поверх того, из чего её позвали»
       и есть правило; держать его должен кит, а не память автора.
       Поштучные подъёмы (31400, 31500) остались там, где окна спорят МЕЖДУ
       СОБОЙ: карточка человека обязана лечь выше окна чата, из которого её
       открыли, — это про них, а не про оффканвас. */
    --u-z-modal: 31000;
    /* Шаг между этажами модалок. Окно, открытое ИЗ окна, обязано лечь выше
       того, из которого его позвали, — иначе человек жмёт кнопку и не видит
       ничего. Этаж считает кит при открытии (ui-kit.js, shell), поэтому под
       модалки отведена целая полоса 31000–31900, а слои над ними начинаются с
       32000: девяносто этажей — больше, чем бывает в жизни, и запас тут дешевле
       разбирательства «почему второе окно под первым». */
    --u-z-modal-step: 10;
    /* Список селектора рисуется в слое над страницей (открытый он лежит в
       document.body, рядом с оверлеем, а не внутри него) — значит он обязан
       быть выше ЛЮБОЙ поверхности, из которой его открывают, а не только выше
       модалки и оффканваса. Здесь стояло 30100, и этого хватало ровно до тех
       пор, пока карточки не начали подниматься над оффканвасом: карточка
       участника (.fedmember-overlay, 31000) и карточка человека
       (#member-profile-modal, 31500) накрывали список собой. Снаружи это
       выглядело так, что селектор есть, а выпадающего списка нет — ровно как
       «кнопка не работает» у окна массовой загрузки этажом выше.
       32000 — выше всей полосы модалок (31000–31900) и ниже подтверждений и
       уведомлений: тост показывают поверх всего. */
    --u-z-select-list: 32000;
    /* Просмотр картинки — самая верхняя КАРТИНКА контента: его открывают из
       чата, из карточки человека (31500) и из новостей, и он обязан лечь
       поверх того, откуда его позвали. Здесь он брал общий модальный слой, а
       тот был ниже окна чатов: человек жмёт на фото в переписке и не видит
       ничего. Выше списка селектора намеренно: раскрытая выпадашка под
       полноэкранным фото — не то, что нужно видеть. Ниже подтверждений: вопрос
       «удалить фото?» задают поверх самого фото. */
    --u-z-lightbox: 32100;
    /* Подтверждение — поверх ВСЕГО, из чего его можно позвать. Самая высокая
       карточка контента — #member-profile-modal (31500), и вопрос «снять
       связку?», заданный из неё, обязан лечь выше: подтверждают то, что видят.
       Ниже уведомлений и сканера: те перекрывают и подтверждение. */
    --u-z-confirm: 32500;
    --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-window-tool-bg: rgba(0, 0, 0, .42);
    --u-window-tool-bg-hover: rgba(0, 0, 0, .62);
    --u-window-tool-fg: #ffffff;
    --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);
    /* Без размытия подложка — единственное, что отделяет окно от страницы,
       поэтому в светлой теме она плотнее прежнего (.45 на белом листе
       читалась как лёгкая тень, а не как слой). */
    /* В светлой теме подложка светлая, а глиф тёмный: цветные шапки
       события (#38803b, #1976d2, #7b1fa2) все средне-тёмные, и белая
       плашка на них читается так же уверенно, как тёмная в тёмной теме. */
    --u-window-tool-bg: rgba(255, 255, 255, .86);
    --u-window-tool-bg-hover: #ffffff;
    --u-window-tool-fg: #1a1a1a;
    --u-overlay: rgba(0, 0, 0, .55);
}

/* ---- .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);
/* НА ПОЛНОЭКРАННЫХ ПОДЛОЖКАХ РАЗМЫТИЯ НЕТ — намеренно.
   backdrop-filter на слое во весь экран заставляет композитор пересобирать и
   размывать ВСЁ, что под ним, и делать это заново на каждый кадр, пока под
   подложкой что-то движется (список, видео, анимация открытия). На телефоне и
   в TWA это видно как подтормаживание интерфейса — жалоба владельца
   15.09.2026. Разделение слоёв даёт затемнение, оно стоит одного кадра.
   Мелкие «стеклянные» поверхности (карточка входа, тост обновления, чипы)
   размытие сохраняют: там оно накрывает сотню пикселей, а не экран. */
    background: var(--u-overlay);
}
.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;
}
/* Подвал существует ради действий. Пустой не показываем: рамка и отступы под
   ничем — это полоса, которую человек читает как «здесь что-то должно быть».
   Собирает строку фабрика диалога, а наполняет вызывающий — поэтому решает
   CSS, а не JS: кнопку могут добавить и после открытия. */
.u-modal__actions:empty,
.u-window__actions:empty {
    display: none;
}
.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);
    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; }
}

/* ---- .u-window — окно ---------------------------------------------------- *
   Окно как в операционной системе: двигают за шапку, тянут за края,
   разворачивают на весь экран, положение запоминается (решение владельца
   15.09.2026).

   ЧЕМ ОТЛИЧАЕТСЯ ОТ МОДАЛКИ. У модалки есть подложка во весь экран, и пока она
   открыта, работать больше нечем. У окна подложки НЕТ: страница под ним живая,
   окон бывает несколько. Поэтому окно само стоит по координатам, а не
   центрируется во флекс-контейнере.

   СЛОЙ НИЖЕ МОДАЛЬНОГО намеренно: подтверждение, вызванное ИЗ окна, обязано
   лечь поверх него. Окна поднимаются друг над другом внутри своего диапазона
   (--u-z-modal-step), но модалку не перебивают.

   ТОЛЬКО ШИРОКИЙ ЭКРАН: на узком фабрика отдаёт модалку и до этих правил дело
   не доходит. Медиазапрос ниже — страховка на случай, если окно уже открыто, а
   браузер сузили.                                                             */
.u-window {
    position: fixed;
    z-index: var(--u-z-window);
    box-sizing: border-box;
    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-window.is-dragging {
    transition: none;
    user-select: none;
}
/* Активное окно отличается не только слоем, но и на вид: несколько одинаковых
   рамок поверх друг друга не говорят, какая из них сейчас слушает клавиатуру.
   Разница — в тени и рамке, а не в цвете: цветом в продукте отмечают состояние
   данных, а не фокус. */
.u-window.is-active {
    border-color: var(--t-border-strong, var(--t-border));
    box-shadow: var(--u-shadow-lg), 0 0 0 1px rgba(var(--t-accent-rgb), .18);
}
.u-window:not(.is-active) .u-window__head { color: var(--t-text-muted); }
.u-window.is-max {
    border-radius: 0;
    border-left: none;
    border-right: none;
}

.u-window__head {
    display: flex;
    align-items: center;
    gap: var(--u-gap-md);
    flex-shrink: 0;
    padding: var(--u-gap-md) var(--u-gap-lg);
    border-bottom: 1px solid var(--t-border);
    font-size: var(--u-font-xl);
    font-weight: 500;
    /* Шапка — ручка окна. Курсор об этом говорит до того, как человек
       попробует: иначе про перетаскивание просто не узнают. */
    cursor: move;
    touch-action: none;
}
/* Строка заголовка окна, чью роль взяла переселённая шапка (adoptHead),
   прячется атрибутом hidden. Атрибут слабее display: flex выше — без этого
   правила над шапкой модалки оставалась пустая полоса, и окно выглядело
   модалкой, засунутой в окно. */
.u-window__head[hidden] { display: none; }
.u-window__title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.u-window__tools {
    display: flex;
    align-items: center;
    gap: var(--u-gap-xs);
    margin-left: auto;
    /* Кнопки в шапке — не ручка: нажатие на них не должно начинать перетаскивание
       (в JS это тоже проверяется, здесь — курсор). */
    cursor: default;
}
/* Кнопки строки заголовка стоят НА ЧУЖОМ ФОНЕ: шапку окна события красит
   сложность, и приглушённый глиф на ней пропадал (жалоба владельца
   15.09.2026). Своя подложка решает это раз и навсегда — она держит контраст,
   не зная, какого цвета шапка под ней, и будет работать с любой следующей. */
.u-window__tools .u-icon-btn {
    --u-icon-btn-size: var(--u-control-xs);
    --u-icon-btn-bg: var(--u-window-tool-bg);
    --u-icon-btn-bg-hover: var(--u-window-tool-bg-hover);
    --u-icon-btn-color: var(--u-window-tool-fg);
    --u-icon-btn-color-hover: var(--u-window-tool-fg);
    font-size: 18px;
    line-height: 1;
}
.u-window__body {
    flex: 1 1 auto;
    /* min-height: 0 — иначе флекс-элемент с прокруткой не сжимается ниже
       своего содержимого, и тело вылезает за нижний край окна. Та же причина,
       что у .u-modal__body. */
    min-height: 0;
    padding: var(--u-gap-lg);
    overflow: auto;
}
/* Содержимое окна считает размеры вместе с рамками — как и в панели
   оффканваса (.u-offcanvas__panel *). Правило продублировано, а не вынесено в
   общий селектор, намеренно: панель и окно живут в разных разделах кита, и
   общий селектор связал бы их правкой друг через друга. */
.u-window__body * { box-sizing: border-box; }
/* Фотография-шапка внутри тела растягивается на всю ширину, съедая отступы —
   так же, как в панели оффканваса. Без этого снимок стоит в рамке из полей и
   выглядит вставленным, а не шапкой. */
.u-window__body > .person-hero {
    margin: calc(-1 * var(--u-gap-lg)) calc(-1 * var(--u-gap-lg)) var(--u-gap-lg);
    border-radius: 0;
}
.u-window__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--u-gap-sm);
    flex-shrink: 0;
    padding: var(--u-gap-md) var(--u-gap-lg);
    border-top: 1px solid var(--t-border);
}

/* Коробка модалки, ПЕРЕСЕЛЁННАЯ в окно (ui.openStatic). Своей рамки, тени и
   потолка ширины у неё здесь быть не должно: рамку даёт окно, ширину задаёт
   человек, а потолок 480/760px превратил бы растянутое окно в поле с колонкой
   посередине. Шапка прячется: её роль играет строка заголовка окна, и второй
   заголовок с крестиком под первым читался бы как две разные формы. */
/* Оверлей модалки, ПЕРЕСЕЛЁННЫЙ в окно (ui.openStatic). Он перестаёт быть слоем
   во весь экран и становится обычным блоком: рамку, положение и размер даёт
   окно.

   display НУЖЕН с !important: место вызова ставит его строкой стиля, а
   строка стиля сильнее любого класса. Центрирование флексом внутри окна не
   нужно — содержимое обязано занимать всю ширину. */
.u-window .u-modal--hosted {
    position: static;
    inset: auto;
    z-index: auto;
    /* Флекс-колонка во всю высоту окна: коробке внутри нужна высота окна, а
       не своего содержимого, — иначе прокручивается окно целиком вместе с
       шапкой и кнопками. */
    display: flex !important;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    background: none;
    padding: 0;
}
/* Коробка внутри: своей рамки, тени и потолка ширины здесь быть не должно —
   рамку даёт окно, ширину задаёт человек, а потолок 480/760px превратил бы
   растянутое окно в поле с колонкой посередине. */
.u-window .u-modal--hosted > .u-modal__box {
    max-width: none;
    width: 100%;
    max-height: none;
    /* Своя высота модалки (80vh у хаба чатов) в окне не действует: высоту
       задаёт окно, коробка его заполняет. Шапка и строка кнопок — отдельные
       блоки коробки и стоят на месте, прокручивается .u-modal__body. */
    height: auto;
    flex: 1 1 auto;
    min-height: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
}
/* Экраны без .u-modal__body (календари) прокручивают коробку сами — окно
   этого больше не делает. */
.u-window .u-modal--hosted > .u-modal__box:not(:has(> .u-modal__body)) {
    overflow: auto;
}
/* Чужая шапка, ставшая строкой заголовка окна: тянут за неё, значит и курсор
   тот же, что у собственной шапки окна. */
.u-window__drag { cursor: move; touch-action: none; }
.u-window .u-modal--hosted > .u-modal__box > .u-window__drag .u-window__tools {
    margin-left: auto;
}
/* ПРЯМОЙ потомок, а не любой: инструменты окна (разворот и закрытие)
   переезжают в эту же шапку, и селектор без прямого потомка прятал крестик
   САМОГО ОКНА — на экране оставались два одинаковых значка и ни одного
   закрытия.
   Свой крестик переселённой модалки прячем: у окна он уже есть в строке
   заголовка, и два крестика подряд — это вопрос «какой из них закроет что».
   Селектор по шапке, а не по имени кнопки: имена у этих крестиков разные
   (.companies-modal-close, .image-modal-close), и список отстал бы от кода. */
.u-window .u-modal--hosted > .u-modal__box > .u-modal__head > .u-icon-btn[aria-label="Close"],
.u-window .u-modal--hosted > .u-modal__box > .u-modal__head > .u-icon-btn[aria-label="Закрыть"] {
    display: none;
}
/* Отступы уже есть у .u-modal__body внутри коробки — вторые дали бы двойную
   рамку из пустого места. */
/* Свой крестик экрана без .u-modal__head (календари: крестик лежит прямо в
   коробке, а в их шапке — навигация по месяцам). Закрывает окно его же
   крестик в строке заголовка — второй подряд спрашивал бы «какой что закроет». */
.u-window .u-modal--hosted > .u-modal__box:not(:has(> .u-modal__head)) > .companies-modal-close {
    display: none;
}
.u-window__body--hosted {
    padding: 0;
    /* Прокручивает содержимое модалки, а не окно: иначе вместе с текстом
       уезжали бы шапка и кнопки «Сохранить»/«Отмена». */
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* ---- ручки растяжки ------------------------------------------------------ *
   Восемь штук: четыре стороны и четыре угла. Полоса 6px по краю и квадрат 12px
   в углу — меньше человек не поймает мышью, больше начинает перехватывать
   нажатия у содержимого.                                                      */
.u-window__grip { position: absolute; touch-action: none; }
.u-window.is-max .u-window__grip { display: none; }
.u-window__grip--n { top: -3px; left: 12px; right: 12px; height: 6px; cursor: ns-resize; }
.u-window__grip--s { bottom: -3px; left: 12px; right: 12px; height: 6px; cursor: ns-resize; }
.u-window__grip--w { left: -3px; top: 12px; bottom: 12px; width: 6px; cursor: ew-resize; }
.u-window__grip--e { right: -3px; top: 12px; bottom: 12px; width: 6px; cursor: ew-resize; }
.u-window__grip--nw { top: -3px; left: -3px; width: 12px; height: 12px; cursor: nwse-resize; }
.u-window__grip--se { bottom: -3px; right: -3px; width: 12px; height: 12px; cursor: nwse-resize; }
.u-window__grip--ne { top: -3px; right: -3px; width: 12px; height: 12px; cursor: nesw-resize; }
.u-window__grip--sw { bottom: -3px; left: -3px; width: 12px; height: 12px; cursor: nesw-resize; }

/* Узкий экран: окон здесь не бывает (фабрика отдаёт модалку), но если браузер
   сузили при открытом окне — оно становится листом во весь экран, а не
   остаётся коробкой, торчащей за край. */
@media (max-width: 899px) {
    .u-window {
        left: 0 !important;
        top: 0 !important;
        width: 100% !important;
        height: 100% !important;
        border-radius: 0;
        border: none;
    }
    .u-window__grip { display: none; }
    .u-window__head { cursor: default; }
}
