/* =========================================================
   MURRLINK — основной stylesheet
   Переменные и порядок слоёв: static/css/tokens.css
   Слои: tokens, base, layout, components, features, pages.
   Оглавление с номерами строк — ниже; генерируется tools/gen_toc.py.
   ========================================================= */

/* =========================================================
   ОГЛАВЛЕНИЕ (сгенерировано tools/gen_toc.py)
   =========================================================
   Внутри этого комментария НЕ должно быть закрывающей пары
   «звёздочка-слэш»: она закрыла бы комментарий, текст оглавления
   стал бы правилами, и браузер молча выбросил бы часть листа
   (такой случай уже ловился — см. историю файла).

- СЛОИ. Порядок объявлен в static/css/tokens.css:
    tokens -> base -> layout -> components -> features -> pages
  (этот файл подключается после tokens.css, поэтому порядок слоёв
  фиксируется до появления правил).

- ПЕРЕМЕННЫЕ вынесены в static/css/tokens.css (значения не менялись).

- СТРАНИЧНЫЕ ФАЙЛЫ наполняют @layer pages:
    tokens.css        переменные + порядок слоёв
    app.css           каркас приложения, компоненты, фичи
    profile.css       личный кабинет (/profile/)
    settings.css      страница настроек (/settings/)
    auth.css          вход и регистрация (/login/, /register/)
    home.css          список серверов
    voice-page.css    отдельная страница голосового канала
    channel-page.css  отдельная страница текстового канала
    user-profile.css  публичный профиль (/user/<id>/)

- ЧТО ГДЕ ЛЕЖИТ: правила разложены по слоям по смыслу, а не по
  исходному порядку в файле. Если правило «проигрывает», проверяйте
  в первую очередь слой, а не специфичность.

   строки  секция
   ------  ------
   60      @layer base
           reset, типографика, скроллбары
   133     @layer layout
           каркас: body, сайдбар серверов, панель каналов, основная
           область, панель участников, голосовая область и controlbar,
           сетка #video-grid
   549     @layer components
           кнопки, поля, меню, модалки, контекстное меню, иконки,
           сообщения чата, карточка профиля, настройки окном,
           .video-placeholder
   1663    общая форма настроек
           accounts/_settings_form.html
   1806    @layer features
           голосовая сетка и плитки, статусы и индикаторы (status-dot,
           speaker-dot, unread-badge, vm-*), демонстрация экрана,
           reply/mentions/unread, поиск участников, mic-volume-wrap,
           voice-empty

   ===== конец оглавления ===== */
@layer base {



/* --- Reset --- */
    * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
    }
    html,
    body {
        height: 100%;
    }

/* ★ Один выключатель движения на весь интерфейс.
   Требование доступности: человек, попросивший «меньше движения»
   (`prefers-reduced-motion: reduce`), сейчас получал только скрытое видео на
   страницах входа — остальные 46 переходов и 2 анимации продолжали играть
   (шторки, тосты, появление меню, открытие окон).

   Почему в слое `base` и почему с `!important`. Для `!important` порядок слоёв
   ОБРАТНЫЙ: побеждает самый РАННИЙ слой, а не самый поздний. `base` объявлен
   сразу после `tokens`, то есть раньше всех слоёв, где живут переходы
   (`layout`, `components`, `features`, `pages` и страничные файлы). Поэтому
   правило отсюда гасит движение во всём приложении, включая страничные файлы
   (`auth.css`, `profile.css`, …), и его не перебивает ни один переход проекта.

   Почему 0.01ms, а не `none`: переход с нулевой длительностью браузер может не
   завершить событием `transitionend`, а на нём в проекте ничего не построено —
   но 0.01ms оставляет поведение «мгновенно, но событие приходит».
   `animation-iteration-count: 1` останавливает конечные анимации
   (`statusMenuIn`, `modalFadeIn`) на первом проигрывании: в проекте
   бесконечных анимаций нет, но если появятся — они не зациклятся.

   Что НЕ ломается: шторки прячутся парой `transform` + `visibility`
   (`mobile.css`), а `visibility` переключается мгновенно вместе с переходом —
   панель так же открывается и закрывается, только без выезда. Проверка —
   `tools/_reduced_motion_check.py` (эмуляция `Emulation.setEmulatedMedia`):
   шторки открываются, окна видны, `transition-duration` = 0.01ms. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}



/* --- Scrollbars --- */
    ::-webkit-scrollbar {
        width: 8px;
        height: 8px;
    }
    ::-webkit-scrollbar-track {
        background: transparent;
    }
    ::-webkit-scrollbar-thumb {
        background: var(--scrollbar-thumb);
        border-radius: 4px;
    }
    ::-webkit-scrollbar-thumb:hover {
        background: var(--scrollbar-thumb);
    }

    /* --- Фокус с клавиатуры: одно видимое кольцо на все кнопки ---
       Раньше у большинства кнопок был только `outline: none` от браузера
       или узкое свечение, не отличимое от ореола акцента. Кольцо рисуется
       СНАРУЖИ и отделено от тела кнопки, поэтому видно и на акцентной,
       и на вторичной, и на прозрачной иконочной кнопке. */
    :where(button, a.btn-primary, a.btn-secondary, a.lp-btn,
           .btn-pill, .icon-btn):focus-visible {
        outline: 2px solid var(--focus-outline);
        outline-offset: 2px;
    }

    /* --- Недоступная кнопка: своя поверхность, а не «мёртвая» прозрачность ---
       opacity: 0.4 делала подпись нечитаемой, а кнопку — неотличимой от
       выключенной подписи. Здесь disabled остаётся элементом: своя заливка,
       граница и подпись с контрастом ≥3:1, но заметно тише активной. */
    button:disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        opacity: 1;
        cursor: not-allowed;
    }
}

/* =========================================================
   СЛОЙ layout
   ========================================================= */
@layer layout {



/* =========================================================
   2. LAYOUT
   ========================================================= */
    body {
        font-family: var(--font-ui);
        /* Фон приложения: картинка дизайнера (соты со светящимися гранями)
           и два мягких зарева акцента поверх неё. Всё это видно СКВОЗЬ
           полупрозрачные панели — на этом и строится ощущение стекла. */
        background-color: var(--bg-pattern-base);
        background-image:
            radial-gradient(1100px 720px at 12% 6%, var(--accent-bleed), transparent 68%),
            radial-gradient(900px 620px at 88% 94%, var(--accent-bleed), transparent 70%),
            var(--bg-pattern);
        /* contain, а не cover: картинка показывается ЦЕЛИКОМ, без растяжения и
           обрезки краёв. С cover она масштабировалась под экран и часть
           композиции уходила за границу. Поля, которые остаются при несовпадении
           пропорций, залиты базовым цветом самой картинки, поэтому границы не
           видно. */
        background-size: auto, auto, contain;
        background-repeat: no-repeat, no-repeat, no-repeat;
        background-position: center, center, center;
        background-attachment: fixed, fixed, fixed;
        color: var(--text-normal);
        display: flex;
        /* Зазор и отступ: панели разведены и читаются отдельными блоками. */
        gap: var(--panel-gap);
        padding: var(--panel-gap);
        overflow: hidden;
    }



/* --- Серверы (левая колонка иконок) --- */
    .servers-rail {
        width: 72px;
        /* Полупрозрачное стекло: сквозь панель видно фон приложения. Контур
           теперь со всех сторон, а не линия справа: панель должна читаться
           отдельным блоком, а не частью общей поверхности. */
        background: var(--glass-bg);
        border: 1px solid var(--rim);
        border-radius: var(--radius-lg);
        box-shadow: var(--panel-shadow), var(--inset-highlight);
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: 12px 0;
        gap: 8px;
        overflow-y: auto;
        flex-shrink: 0;
    }



/* --- Панель каналов --- */
    .channels-panel {
        width: 240px;
        background: var(--glass-bg);
        border: 1px solid var(--rim);
        border-radius: var(--radius-lg);
        box-shadow: var(--panel-shadow), var(--inset-highlight);
        display: flex;
        flex-direction: column;
        /* Обрезаем содержимое по скруглённым углам: панель пользователя внизу
           иначе вылезала бы за них квадратом. */
        overflow: hidden;
        flex-shrink: 0;
    }
    .server-header {
        height: 48px;
        padding: 0 16px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        border-bottom: 1px solid var(--border);
        cursor: pointer;
        color: var(--text-primary);
        font-weight: 600;
        font-size: 15px;
    }
    .server-header:hover {
        background: var(--bg-hover);
    }
    .server-header .name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .channels-list {
        flex: 1;
        overflow-y: auto;
        padding: 8px;
    }



/* --- Основная область --- */
    .main-area {
        flex: 1;
        display: flex;
        flex-direction: column;
        /* Основная область — такая же стеклянная панель, как боковые: каркас
           собран из четырёх отдельных блоков, а не из одной поверхности. */
        background: var(--glass-bg);
        border: 1px solid var(--rim);
        border-radius: var(--radius-lg);
        box-shadow: var(--panel-shadow), var(--inset-highlight);
        min-width: 0;
        overflow: hidden;
    }
    .main-header {
        height: 48px;
        padding: 0 16px;
        display: flex;
        align-items: center;
        border-bottom: 1px solid var(--rim);
        gap: 8px;
        /* Шапка отделена от сообщений светлой линией: раньше граница была
           тёмной и на стекле читалась слабо. */
        background: var(--bg-panel);
        color: var(--text-primary);
        font-size: 15px;
    }
    .main-header .hash {
        color: var(--text-icon);
        font-size: 22px;
    }



/* --- Участники (правая колонка) --- */
    .members-panel {
        width: 240px;
        background: var(--glass-bg);
        border: 1px solid var(--rim);
        border-radius: var(--radius-lg);
        box-shadow: var(--panel-shadow), var(--inset-highlight);
        padding: 16px 8px;
        overflow-y: auto;
        flex-shrink: 0;
    }

    /* Общее для всех панелей каркаса: верхний блик и размытие фона.
       Размытие вместе с прозрачностью и даёт стекло: сквозь панель видно фон
       со светящимися дорожками, но он размыт, поэтому текст по нему читается.
       Правило общее, поэтому лежит в раннем слое layout, а не в components.

       Модальные окна и подложки — СОСЕДИ панелей, а не их потомки (проверено
       по разметке), поэтому backdrop-filter не смещает их position: fixed. */
    .servers-rail,
    .channels-panel,
    .main-area,
    .members-panel {
        background-image: var(--glass-gloss);
        -webkit-backdrop-filter: var(--glass-blur);
        backdrop-filter: var(--glass-blur);
    }
    .members-group-label {
        padding: 8px;
        font-size: 11px;
        color: var(--text-dim);
        text-transform: uppercase;
        font-weight: 600;
    }
    .member {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 6px 8px;
        border-radius: var(--radius-sm);
        cursor: pointer;
        /* ★ `contain: layout paint` на СТРОКЕ длинного списка.
           Что это даёт: инвалидация и перерисовка ограничиваются одной строкой,
           а не всем списком участников (в нём бывает несколько сотен строк).
           Раскладка снаружи не может повлиять на строку, а строка — на
           соседей, поэтому движку не нужно пересчитывать список целиком.
           Почему безопасно именно здесь: у строки нет ни вылетов за границы
           (аватар 40 px, имя, точка статуса — всё внутри), ни фиксированных
           потомков, ни отрицательных отступов; высота задаётся содержимым.
           Замер: `scrollHeight` списка и высота строки не изменились (см.
           отчёт), проверка — tools/run_smoke.py (маркеры участников). */
        contain: layout paint;
    }
    .member:hover {
        background: var(--bg-hover);
    }



/* --- Empty state --- */
    .empty-state {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        color: var(--text-dim);
        gap: 12px;
        padding: 24px;
    }
    .empty-state h2 {
        color: var(--text-primary);
        font-weight: 600;
    }



/* =========================================================
   3. CHANNELS
   ========================================================= */
    .channel-group-label {
        padding: 16px 8px 4px;
        font-size: 11px;
        color: var(--text-dim);
        text-transform: uppercase;
        letter-spacing: 0.02em;
        font-weight: 600;
    }



/* =========================================================
   4. USER PANEL
   ========================================================= */
    .user-panel {
        height: 52px;
        /* Панель пользователя — тот же блок, что и каналы над ней, но на тон
           плотнее: так она читается отдельной полосой внизу стеклянной панели,
           а не её продолжением. */
        background: var(--bg-sidebar-dark);
        padding: 0 8px;
        display: flex;
        align-items: center;
        gap: 8px;
        /* Светлая линия сверху отделяет её от списка каналов: тёмная граница
           на стекле читалась слабо. */
        border-top: 1px solid var(--rim);
        position: relative;
    }
    .user-info {
        flex: 1;
        overflow: hidden;
    }
    /* Имя пользователя — кнопка меню статуса (role="button" в разметке).
       Фокус с клавиатуры: тот же токен, что у пунктов меню. */
    #user-status-trigger:focus-visible {
        outline: none;
        border-radius: var(--radius-sm);
        box-shadow: var(--focus-ring);
    }
    .user-name {
        color: var(--text-primary);
        font-size: 14px;
        font-weight: 600;
    }
    .user-tag {
        color: var(--text-dim);
        font-size: 12px;
    }
    .user-actions {
        display: flex;
        gap: 4px;
    }



/* =========================================================
   5. CHAT
   ========================================================= */
    .messages-area {
        flex: 1;
        overflow-y: auto;
        padding: 16px;
    }



/* --- Поле ввода --- */
    .input-area {
        padding: 0 16px 24px;
        position: relative;
    }


/* --- Голосовая область --- */
    .voice-area {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: stretch;    /* ★ вместо center — растягиваем по ширине */
        justify-content: flex-start;
        gap: 12px;               /* ★ уменьшаем gap */
        padding: 12px;           /* ★ меньше паддинг */
        min-height: 0;           /* ★ важно для flex-детей */
    }



/* =========================================================
   ГОЛОСОВАЯ ОБЛАСТЬ — оформление в стиле Discord
   ========================================================= */
    #voice-view {
        position: relative;
    }
    .voice-header-meta {
        margin-left: auto;
        font-size: 12px;
        color: var(--text-dim);
        font-weight: 400;
    }


/* Нижняя панель управления голосовым каналом */
    .voice-controlbar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 16px;
        flex: 0 0 auto;
        padding: 10px 16px;
        background: var(--bg-panel);
        border-top: 1px solid var(--border);
    }
    .voice-controlbar-info {
        display: flex;
        flex-direction: column;
        gap: 2px;
        min-width: 0;
    }



/* =========================================================
   ГОЛОСОВАЯ СЕТКА — плитки участников, как в Discord
   ========================================================= */
    #video-grid-container {
        flex: 1 1 auto;
        min-height: 0;
        /* flex-колонка: сетка внутри тянется на всю оставшуюся высоту */
        display: flex;
        flex-direction: column;
        overflow: hidden;
        background: transparent;
        padding: 0;
    }


/* Геометрию сетки (размер плитки 16:9, число колонок и строк, размер самой
   сетки и центрирование) считает static/js/voice/grid.js, а применяет раздел
   «РАСКЛАДКА ГОЛОСОВОЙ ОБЛАСТИ» в слое features. Здесь остаётся каркас:
   сетка внутри голосовой области и зазор между плитками. */
    #video-grid {
        display: grid;
        grid-template-columns: repeat(var(--voice-cols, 1), minmax(0, 1fr));
        grid-template-rows: none;
        grid-auto-rows: minmax(0, 1fr);
        gap: 8px;
        width: 100%;
        /* сетка — flex-элемент: занимает всю оставшуюся высоту области */
        flex: 1 1 auto;
        min-height: 0;
        margin: 0;
    }


/* Демонстрация на сцене — плитки уезжают в компактную полосу снизу */
    #video-grid-container.screen-active {
        flex: 0 0 auto;
    }
    #video-grid-container.screen-active #video-grid {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: 168px;
        grid-auto-rows: auto;
        max-width: none;
        max-height: none;
        height: auto;
        overflow-x: auto;
        padding-bottom: 4px;
    }
    #video-grid-container.screen-active .voice-tile { aspect-ratio: 16 / 10; }
    #video-grid-container.screen-active .voice-tile.pinned { grid-column: auto; }



/* =========================================================
   12. VIDEO GRID (видео-сетка, как в Discord)
   ========================================================= */

/* Разметку контейнера и плиток задаёт блок «ГОЛОСОВАЯ СЕТКА» выше
   (единая сетка #video-grid). */

/* ★ Признак «идёт демонстрация» — ОДИН: класс `.screen-active` на
   `#video-grid-container` (правило в начале этого слоя). Его ставит и снимает
   `voice/screen.js: setGridCompactForScreen()`, и зовётся он строго парой с
   показом/скрытием сцены (screen.js:1712+1717, 1938+1939, 1953+1956,
   1996+1999), а раскладка читает тот же класс (`voice/grid.js:204`).
   Здесь стояли четыре резервных селектора
   `.voice-area:has(#screen-stage[style*="display:flex"]) #video-grid-container`
   (и три близнеца с пробелом и `block`) — они удалены:
     * поведения они не добавляли (то же `flex: 0 0 auto`, что у `.screen-active`),
       но заводили ВТОРОЙ источник правды;
     * признак брался из строки инлайнового стиля, а `mobile.css:139-147` прямо
       объясняет, почему так делать нельзя: строка зависит от того, как её
       сериализует браузер, а снятие инлайн-стиля вообще убирает атрибут
       (авторы уже лечили это копированием селектора под два варианта пробела);
     * `:has()` на `#screen-stage` заставлял перепроверять совпадение при любой
       записи в атрибут `style` внутри `.voice-area`. */
}

/* =========================================================
   СЛОЙ components
   ========================================================= */
@layer components {
    .server-icon {
        width: 48px;
        height: 48px;
        border-radius: 16px;
        /* Каждая иконка сервера — отдельный стеклянный блок со своей светлой
           гранью, а не плоский квадрат: по замечанию дизайнера иконки сервера,
           «плюс» и «приглашение» должны читаться отдельными блоками. */
        background: var(--glass-bg);
        border: 1px solid var(--rim);
        box-shadow: var(--inset-highlight);
        color: var(--text-normal);
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 600;
        font-size: 16px;
        cursor: pointer;
        /* ★ Конкретные свойства вместо `all`. С `all` движок наблюдал за всеми
           свойствами элемента, а ховер `.server-icon:hover` меняет ещё и
           `border-radius` (16px -> 14px) — то есть вдобавок анимировалось
           свойство РИСОВАНИЯ, и иконка сервера перерисовывалась каждый кадр
           0.15 с. Форма теперь меняется мгновенно, цвет и фон — плавно. */
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    color var(--transition-base);
        position: relative;
        overflow: visible;
    }
    .server-icon:hover {
        border-radius: 14px;
        background: var(--accent);
        border-color: var(--accent-hover);
        color: var(--on-accent);
    }
    /* Активный сервер подсвечен. Свечение — только на активных состояниях,
       спокойный элемент не светится. Обводка мягкая, без плотного кольца:
       плотное кольцо читалось как грубая рамка. */
    .server-icon.active {
        border-radius: 14px;
        background: var(--accent);
        border-color: var(--accent-hover);
        color: var(--on-accent);
        box-shadow: var(--neon-glow), var(--glow-accent-lg), var(--inset-highlight);
    }
    .server-icon img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        border-radius: inherit;
    }


/* Индикатор непрочитанных на иконке сервера */
    .server-icon.has-unread::before {
        content: '';
        position: absolute;
        left: -14px;
        top: 50%;
        transform: translateY(-50%);
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: var(--text-primary);
        transition: height var(--transition-base);
    }


/* При наведении точка растягивается в полоску — как в Discord */
    .server-icon.has-unread:hover::before {
        height: 20px;
        border-radius: 4px;
    }
    .server-icon.add {
        background: var(--bg-app);
        color: var(--success);
        font-size: 24px;
    }
    .server-icon.add:hover {
        background: var(--success);
        color: var(--on-strong);
    }
    .server-icon.join:hover {
        background: var(--link);
        color: var(--on-strong);
    }
    .divider {
        width: 32px;
        height: 2px;
        background: var(--border-soft);
        border-radius: 1px;
        margin: 4px 0;
    }
    .invite-btn {
        background: none;
        border: 1px solid transparent;
        color: var(--btn-icon);
        cursor: pointer;
        padding: 4px 8px;
        border-radius: var(--radius-sm);
        font-size: 14px;
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    color var(--transition-base);
    }
    .invite-btn:hover {
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
        color: var(--btn-fg);
    }
    .channel {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 6px 8px;
        border-radius: var(--radius-md);
        color: var(--text-dim);
        cursor: pointer;
        font-size: 15px;
        user-select: none;
        position: relative;
        /* Прозрачная рамка вместо отсутствующей: без неё капсула при
           активации «толстела» на 2 px и список дёргался. */
        border: 1px solid transparent;
    }
    .channel:hover {
        background: var(--bg-hover);
        color: var(--text-normal);
    }
    /* Активный канал — капсула с ореолом, как активный пункт меню в
       референсах. Раньше это была полоска слева (inset 3px). От неё
       отказались по референсам: там активный пункт светится целиком, а
       заливка акцентом читается на тёмном фоне лучше полоски. */
    .channel.active {
        background: var(--accent-soft);
        border-color: var(--border-strong);
        color: var(--text-primary);
        /* Ореол убран по замечанию дизайнера: активный канал читается
           капсулой и гранью, свечение вокруг оказалось лишним. */
        box-shadow: none;
    }
    .channel .icon {
        font-size: 18px;
        color: var(--text-icon);
        flex-shrink: 0;
    }
    /* Активная иконка канала — акцентом. Раньше правило дублировалось и
       нижнее (--text-normal) перебивало верхнее (--accent), поэтому значок
       активного канала не подсвечивался. */
    .channel.active .icon {
        color: var(--accent);
    }
    .channel .name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .icon-btn {
        /* Иконочная кнопка — ПРОЗРАЧНОЕ тело: на панели её держит значок,
           а не заливка. Видимый ховер обязателен: без него кнопка «пропадает»
           и человек не понимает, что на неё можно нажать. */
        background: transparent;
        /* Прозрачная грань в покое: при наведении она загорается, а
           размер кнопки не меняется — иначе элемент дёргался бы на 2 px. */
        border: 1px solid transparent;
        color: var(--btn-icon);
        cursor: pointer;
        padding: 6px;
        border-radius: var(--radius-sm);
        font-size: 16px;
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    color var(--transition-base);
    }
    .icon-btn:hover {
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
        color: var(--btn-fg);
    }
    a.icon-btn {
        text-decoration: none;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        line-height: 1;
    }



/* --- Свой аватар и статус --- */
    .user-avatar {
        width: 32px;
        height: 32px;
        border-radius: var(--radius-round);
        background: var(--accent);
        color: var(--on-accent);
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 600;
        font-size: 14px;
        flex-shrink: 0;
        position: relative;
    }
    .user-avatar-link {
        text-decoration: none;
        color: inherit;
        display: block;
        transition: opacity var(--transition-base);
    }
    .user-avatar-link:hover {
        opacity: 0.8;
    }



/* --- Меню выбора статуса --- */
    .status-menu {
        position: absolute;
        left: 8px;
        bottom: 60px;
        width: 200px;
        background: var(--bg-menu);
        border-radius: var(--radius-md);
        box-shadow: var(--shadow-modal);
        padding: 6px 4px;
        z-index: 500;
        animation: statusMenuIn var(--transition-fast) ease-out;
    }
    @keyframes statusMenuIn {
        from { opacity: 0; transform: translateY(4px); }
        to   { opacity: 1; transform: translateY(0); }}
    .status-item {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 8px 10px;
        border-radius: 3px;
        color: var(--text-muted);
        font-size: 14px;
        cursor: pointer;
        user-select: none;
        transition: background var(--transition-fast), color var(--transition-fast);
    }
    .status-item:hover {
        background: var(--accent);
        color: var(--on-accent);
    }
    .status-item.active {
        background: var(--bg-hover);
        color: var(--text-primary);
    }
    /* Фокус с клавиатуры. Пункт меню — div с role="menuitemradio": без явного
       правила браузерная рамка на тёмном стекле почти не видна. Токен
       --focus-ring — тот же, что у полей ввода, ползунков и вкладок окна
       управления (roles.css). */
    .status-item:focus-visible {
        outline: none;
        box-shadow: var(--focus-ring);
    }


/* Кружок статуса внутри меню */
    .status-item .status-dot {
        width: 10px;
        height: 10px;
        border-radius: var(--radius-round);
        flex-shrink: 0;
        position: static;
        bottom: auto;
        right: auto;
        border: none;
    }
    .status-item .status-dot.online { background: var(--success); }
    .status-item .status-dot.idle { background: var(--warning); }
    .status-item .status-dot.dnd { background: var(--danger-solid); }
    .status-item .status-dot.offline { background: var(--offline); }


/* --- Одно сообщение --- */
    .msg {
        display: flex;
        flex-direction: column;
        gap: 4px;
        padding: 4px 0;
        margin-bottom: 2px;
        /* ★ `contain: layout` (БЕЗ `paint`) на строке переписки.
           Инвалидация ограничивается одним сообщением: вставка нового
           сообщения или смена состояния строки не заставляет движок
           пересчитывать раскладку всей истории.
           ПОЧЕМУ БЕЗ `paint`: у `.msg .msg-avatar` есть `box-shadow:
           var(--neon-ring-glow)` (tokens.css:165 — два размытых свечения
           радиусом 7 и 16 px), и они выходят за коробку аватара; `contain:
           paint` их обрезал бы. `contain: layout` ничего не обрезает.
           Почему без `content-visibility`: восстановление позиции прокрутки в
           чате и кнопка «вниз» считаются через
           `scrollHeight` (features/chat-core.js:350, 366, 389, 459, 470), а
           `content-visibility: auto` меняет измеренную высоту у ещё не
           отрисованных строк — позиция уехала бы. Поэтому здесь этого
           свойства нет: риск сломать прокрутку выше выигрыша (см. отчёт). */
        contain: layout;
    }
    .msg .msg-main {
        display: flex;
        gap: 12px;
        width: 100%;
    }
    .msg .msg-avatar {
        width: 40px;
        height: 40px;
        border-radius: var(--radius-round);
        background: var(--accent);
        color: var(--on-accent);
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 600;
        font-size: 16px;
        flex-shrink: 0;
    }
    .msg .msg-body {
        flex: 1;
        min-width: 0;
    }
    .msg .msg-head {
        display: flex;
        align-items: baseline;
        gap: 8px;
        margin-bottom: 2px;
    }
    .msg .msg-author {
        color: var(--text-primary);
        font-weight: 600;
        font-size: 15px;
    }
    .msg .msg-time {
        color: var(--text-dim);
        font-size: 12px;
    }
    .msg .msg-text {
        color: var(--text-normal);
        font-size: 15px;
        line-height: 1.4;
        word-wrap: break-word;
        white-space: pre-wrap;
    }


/* Группировка сообщений от одного автора подряд */
    .msg.continued .msg-avatar {
        visibility: hidden;
    }
    .msg.continued .msg-head {
        display: none;
    }
    /* ★ Закреплённое сообщение показывает шапку, даже если оно «продолжение»
       того же автора: иначе метка «Закреплено» оказывалась в скрытом
       .msg-head и её не было видно (нашлось на живом 1440). */
    .msg.pinned .msg-head {
        display: flex;
    }



/* --- Индикатор «печатает…» --- */
    .typing-indicator {
        height: 24px;
        padding: 0 16px;
        font-size: 13px;
        color: var(--text-dim);
    }
    .typing-indicator .dots {
        color: var(--text-muted);
    }
    /* Кромка и свечение этого поля живут в блоке «градиентная грань» ниже
       (`.input-wrapper` и `.input-wrapper:focus-within` в слое components):
       здесь остаётся только фон. Раньше объявления `box-shadow` были и
       тут, но ниже в том же слое стояли `box-shadow: none` и кольцо фокуса —
       то есть эти два были мёртвыми.

       ★ Переход по `box-shadow` убран. Тень — свойство РИСОВАНИЯ, а не
       композиции: переход по ней перерисовывает элемент на каждом кадре
       (у кольца фокуса ещё и размытие). Состояние теперь появляется мгновенно,
       как уже сделано для `.speaker-dot` и `.voice-tile` после истории с
       пульсацией индикатора речи и для `.vcb-btn` (ниже в этом файле). */
    .input-wrapper {
        background: var(--bg-input);
        border-radius: var(--radius-lg);
        display: flex;
        align-items: flex-end;
    }
    .input-wrapper input {
        flex: 1;
        padding: 12px 16px;
        background: none;
        color: var(--text-normal);
        border: none;
        outline: none;
        font-size: 15px;
    }
    .input-wrapper input::placeholder {
        /* Был --text-faint (#4a4659): на тёмном фоне подсказка почти не
           читалась. --text-dim даёт тот же тон, что у подписей. */
        color: var(--text-dim);
    }



/* --- Видеосетка — см. секцию «ГОЛОСОВАЯ СЕТКА» ниже --- */

/* =========================================================
   7. CONTEXT MENU
   ========================================================= */
    .context-menu {
        position: fixed;
        background: var(--bg-menu);
        border-radius: var(--radius-sm);
        box-shadow: var(--shadow-menu);
        padding: 6px 4px;
        min-width: 180px;
        z-index: 10000;
        display: none;
    }
    .context-menu.show {
        display: block;
    }
    .context-menu-item {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 6px 10px;
        border-radius: 2px;
        color: var(--text-muted);
        cursor: pointer;
        font-size: 14px;
        user-select: none;
    }
    .context-menu-item:hover {
        background: var(--accent);
        color: var(--on-accent);
    }
    /* Иконка пункта — из общего набора; цвет наследует текст пункта,
       поэтому на опасном/наведённом пункте перекрашивается вместе с ним. */
    .context-menu-icon {
        display: inline-flex;
        flex: 0 0 auto;
        color: var(--text-icon);
    }
    .context-menu-item:hover .context-menu-icon,
    .context-menu-item.danger .context-menu-icon {
        color: inherit;
    }
    .context-menu-item.danger {
        color: var(--danger);
    }
    .context-menu-item.danger:hover {
        background: var(--bg-danger);
        color: var(--on-danger);
    }
    .context-menu-sep {
        height: 1px;
        background: var(--bg-sidebar);
        margin: 4px 0;
    }
    /* ★ Режим уведомлений: отметка текущего. Пункты — `menuitemradio`
       (aria-checked ставит lib/context-menu.js), поэтому состояние видно не
       только глазами: «Все сообщения / Только упоминания / Ничего». */
    .context-menu-item.checked {
        color: var(--text-primary);
        font-weight: 600;
    }
    /* Отметка выбранного режима — SVG-галочка из набора (lib/context-menu.js),
       а не текстовый «✓» из CSS. */
    .context-menu-check {
        display: inline-flex;
        margin-left: auto;
        color: var(--accent);
    }
    .context-menu-item.checked:hover .context-menu-check {
        color: var(--on-accent);
    }
    /* Подсказка меню (например, «сервер главнее канала»). Это НЕ пункт:
       стрелки её пропускают, клик ничего не делает. Перенос строк обязателен —
       фраза длиннее ширины меню. */
    .context-menu-hint {
        padding: 4px 10px 6px;
        max-width: 250px;
        color: var(--text-dim);
        font-size: 11px;
        line-height: 1.35;
        white-space: normal;
    }
    /* Пункт со своей разметкой (ползунок громкости участника). Это «место
       настройки», а не команда: подсветки пункта при наведении у него нет —
       иначе ползунок выглядел бы кнопкой, и было бы неясно, что меню
       остаётся открытым (см. lib/context-menu.js, keepOpen). */
    .context-menu-item--custom,
    .context-menu-item--custom:hover {
        display: block;
        background: none;
        color: var(--text-muted);
        cursor: default;
    }
    .context-menu-volume {
        min-width: 190px;
    }
    .context-menu-volume-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        font-size: 13px;
        color: var(--text-muted);
    }
    .context-menu-volume-value {
        color: var(--text-icon);
        font-variant-numeric: tabular-nums;
    }
    .context-menu-volume-slider {
        width: 100%;
        height: 4px;
        margin: 6px 0 2px;
        accent-color: var(--accent);
        cursor: pointer;
    }
    .context-menu-volume-reset {
        width: 100%;
        padding: 4px 0;
        border: none;
        background: none;
        border-radius: var(--radius-sm);
        color: var(--text-dim);
        font-size: 12px;
        cursor: pointer;
    }
    .context-menu-volume-reset:hover {
        background: var(--btn-icon-hover-bg);
        color: var(--btn-fg);
    }



/* =========================================================
   8. MODALS (профиль)
   ========================================================= */
    .modal {
        position: fixed;
        inset: 0;
        z-index: 10000;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .modal-backdrop {
        position: absolute;
        inset: 0;
        background: var(--overlay-modal);
    }
    .modal-content {
        position: relative;
        background: var(--bg-sidebar);
        border-radius: var(--radius-xl);
        padding: 28px 32px;
        max-width: 520px;
        width: 90%;
        max-height: 90vh;
        overflow-y: auto;
        box-shadow: var(--shadow-modal);
        animation: modalFadeIn var(--transition-base) ease-out;
    }
    @keyframes modalFadeIn {
        from { opacity: 0; transform: scale(0.96); }
        to   { opacity: 1; transform: scale(1); }}
    .modal-close {
        position: absolute;
        top: 16px;
        right: 16px;
        background: none;
        border: none;
        color: var(--text-muted);
        font-size: 20px;
        line-height: 1;
        cursor: pointer;
        padding: 6px 10px;
        border-radius: var(--radius-md);
        transition: background var(--transition-base), color var(--transition-base);
        font-family: inherit;
        z-index: 1;
    }
    .modal-close:hover {
        background: var(--btn-icon-hover-bg);
        color: var(--btn-fg);
    }
    .modal-close:active {
        background: var(--btn-danger-bg);
        color: var(--btn-danger-fg);
    }
    .modal-content::-webkit-scrollbar {
        width: 8px;
    }
    .modal-content::-webkit-scrollbar-track {
        background: transparent;
    }
    .modal-content::-webkit-scrollbar-thumb {
        background: var(--scrollbar-thumb);
        border-radius: 4px;
    }
    .modal-content::-webkit-scrollbar-thumb:hover {
        background: var(--scrollbar-thumb);
    }



/* --- Форма профиля --- */
    .profile-modal-form {
        display: flex;
        flex-direction: column;
        gap: 20px;
    }
    .profile-modal-form h2 {
        color: var(--text-primary);
        font-size: 20px;
        font-weight: 600;
        margin: 0 0 8px;
        padding-right: 32px;
    }
    .profile-modal-form .form-group {
        display: flex;
        flex-direction: column;
        gap: 6px;
    }
    .profile-modal-form label {
        color: var(--text-muted);
        font-size: 12px;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.02em;
    }
    .profile-modal-form .form-input,
    .profile-modal-form input[type="text"],
    .profile-modal-form input[type="email"],
    .profile-modal-form input[type="password"],
    .profile-modal-form textarea {
        background: var(--bg-sidebar-dark);
        border: 1px solid var(--bg-sidebar-dark);
        color: var(--text-normal);
        padding: 10px 14px;
        border-radius: var(--radius-md);
        font-size: 15px;
        font-family: inherit;
        outline: none;
        transition: border-color var(--transition-base), background var(--transition-base);
        width: 100%;
        resize: vertical;
    }
    .profile-modal-form .form-input:hover,
    .profile-modal-form input[type="text"]:hover,
    .profile-modal-form input[type="email"]:hover,
    .profile-modal-form textarea:hover {
        border-color: var(--bg-hover);
    }
    .profile-modal-form .form-input:focus,
    .profile-modal-form input[type="text"]:focus,
    .profile-modal-form input[type="email"]:focus,
    .profile-modal-form textarea:focus {
        border-color: var(--accent);
        background: var(--bg-menu);
    }
    .profile-modal-form .form-input:disabled,
    .profile-modal-form input:disabled {
        opacity: 0.5;
        cursor: not-allowed;
        background: var(--bg-panel);
    }
    .profile-modal-form textarea {
        min-height: 80px;
        line-height: 1.4;
    }
    .profile-modal-form .field-help {
        color: var(--text-faint);
        font-size: 12px;
        margin: 0;
    }
    .profile-modal-form .field-error {
        color: var(--danger);
        font-size: 13px;
        margin: 4px 0 0;
        display: flex;
        align-items: flex-start;
        gap: 6px;
    }
    .profile-modal-form .field-error-icon {
        flex: 0 0 auto;
        margin-top: 1px;
    }



/* --- Блок с аватаром в форме --- */
    .profile-modal-form .profile-avatar-block {
        display: flex;
        gap: 20px;
        align-items: center;
        padding: 16px;
        background: var(--bg-panel);
        border-radius: var(--radius-lg);
        border: 1px solid var(--border);
    }
    .profile-modal-form .avatar-preview {
        width: 80px;
        height: 80px;
        border-radius: var(--radius-round);
        overflow: hidden;
        flex-shrink: 0;
        background: var(--accent);
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
        border: 2px solid var(--border);
        transition: border-color var(--transition-base);
    }
    .profile-modal-form .avatar-preview:hover {
        border-color: var(--accent);
    }
    .profile-modal-form .avatar-preview img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }
    /* .avatar-initial — тот же инициал, но нарисованный запасным вариантом,
       когда картинка аватара не отдалась (см. templates/_avatar.html) */
    .profile-modal-form .avatar-initials,
    .profile-modal-form .avatar-initial {
        color: var(--on-avatar);
        font-size: 32px;
        font-weight: 600;
        user-select: none;
    }
    .profile-modal-form .avatar-info {
        display: flex;
        flex-direction: column;
        gap: 8px;
        flex: 1;
    }
    .profile-modal-form .avatar-upload-btn {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 8px 16px;
        background: var(--bg-neutral);
        color: var(--text-primary);
        border-radius: var(--radius-sm);
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: background var(--transition-base);
        width: fit-content;
        user-select: none;
    }
    .profile-modal-form .avatar-upload-btn svg {
        width: 16px;
        height: 16px;
        flex-shrink: 0;
    }
    .profile-modal-form .avatar-upload-btn:hover { background: var(--bg-neutral-hover); }
    .profile-modal-form .avatar-upload-btn:active { background: var(--accent); }
    .profile-modal-form input[type="file"] {
        display: none;
    }



/* --- Кнопки формы --- */
    .profile-modal-form .form-actions {
        display: flex;
        gap: 12px;
        margin-top: 8px;
        padding-top: 16px;
        border-top: 1px solid var(--border);
    }
    .profile-modal-form .btn-primary,
    .profile-modal-form .btn-secondary {
        padding: 10px 24px;
        border: none;
        border-radius: var(--radius-md);
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        font-family: inherit;
        transition: background var(--transition-base), transform 0.05s;
        flex: 0 0 auto;
    }
    .profile-modal-form .btn-primary {
        background: var(--accent);
        color: var(--on-accent);
    }
    .profile-modal-form .btn-primary:hover {
        background: var(--accent-hover);
    }
    .profile-modal-form .btn-primary:active {
        transform: scale(0.98);
    }
    .profile-modal-form .btn-secondary {
        background: transparent;
        color: var(--text-muted);
    }
    .profile-modal-form .btn-secondary:hover {
        background: var(--bg-hover);
        color: var(--text-normal);
    }
    .profile-modal-form .modal-toast {
        padding: 10px 14px;
        border-radius: var(--radius-md);
        background: var(--bg-panel);
        font-size: 14px;
        margin-bottom: 8px;
    }
    .profile-modal-form .modal-toast.success { border-left: 3px solid var(--success); }
    .profile-modal-form .modal-toast.error { border-left: 3px solid var(--danger); }



/* =========================================================
   9. AVATARS (везде — общие правила)
   ========================================================= */

/* --- Аватар в чате (.msg-avatar) --- */
    .msg .msg-avatar img {
        width: 100%;
        height: 100%;
        border-radius: var(--radius-round);
        object-fit: cover;
        display: block;
    }


/* Убираем синий фон, если внутри <img> */
    .msg .msg-avatar:has(img) {
        background: transparent;
    }



/* =========================================================
   10. ICONS (SVG-иконки, разные размеры)
   ========================================================= */
    .icon {
        display: inline-block;
        vertical-align: middle;
        flex-shrink: 0;
        pointer-events: none;
    }
    /* Иконки, вставленные прямо в разметку: крестики, стрелка вниз, громкость,
       галочка. Имя отличается от .icon намеренно: .icon — якорь, который ищут
       модули JS (на нём строятся иконки каналов), и подмешивать в него свои
       элементы нельзя. Размер задаётся атрибутами width/height в 1em, поэтому
       иконка встаёт ровно того размера, что и заменённый символ: font-size у
       этих кнопок уже прописан в CSS. */
    .ico {
        display: inline-block;
        vertical-align: middle;
        flex-shrink: 0;
        pointer-events: none;
        /* Размер — от шрифта места: ровно как прежний width="1em" у
           встроенных иконок. Конкретные компоненты могут переопределить. */
        width: 1em;
        height: 1em;
    }
    .vqc-btn svg {
        width: 18px;
        height: 18px;
    }
    .server-icon svg {
        width: 24px;
        height: 24px;
    }
    .icon-btn svg {
        display: block;
        width: 16px;
        height: 16px;
        pointer-events: none;
    }
    .channel .icon svg {
        width: 16px;
        height: 16px;
        color: inherit;
    }


/* ===== EDIT / DELETE ===== */

/* Пометка "(изменено)" */
    .msg-edited-mark {
        color: var(--text-faint);
        font-size: 11px;
        font-style: italic;
        user-select: none;
    }


/* Textarea при редактировании */
    .edit-textarea {
        width: 100%;
        min-height: 40px;
        max-height: 200px;
        background: var(--bg-input);
        color: var(--text-normal);
        border: 1px solid var(--accent);
        border-radius: 6px;
        padding: 8px 12px;
        font-family: inherit;
        font-size: 15px;
        line-height: 1.4;
        resize: vertical;
        outline: none;
        overflow-y: auto;
    }


/* Удалённое сообщение */
    .msg.deleted .msg-text {
        color: var(--text-faint);
        font-style: italic;
        user-select: none;
    }
    .msg.deleted .msg-avatar {
        opacity: 0.3;
    }
    .msg.deleted .msg-author {
        opacity: 0.5;
    }


/* Пометка "(изменено)" должна быть видна даже в .msg.continued */
    .msg.continued .msg-edited-mark {
        display: inline;
    }


/* Если у сообщения есть пометка — показываем голову */
    .msg:has(.msg-edited-mark) .msg-head {
        display: flex;
    }



/* =========================================================
   ЧАТ: многострочный ввод и разметка сообщений
   ========================================================= */
    .input-wrapper textarea {
        flex: 1;
        padding: 12px 16px;
        background: none;
        color: var(--text-normal);
        border: none;
        outline: none;
        font-size: 15px;
        line-height: 22px;
        font-family: inherit;
        resize: none;
        max-height: 196px;
        overflow-y: hidden;
    }
    .input-wrapper textarea::placeholder {
        color: var(--text-faint);
    }



/* =========================================================
   КАРТОЧКА ПРОФИЛЯ
   ========================================================= */
    .user-card-backdrop {
        position: fixed;
        inset: 0;
        display: none;
        align-items: center;
        justify-content: center;
        background: var(--overlay-soft);
        z-index: 60;
    }
    .user-card-backdrop.visible { display: flex; }
    .user-card {
        position: relative;
        width: 340px;
        max-width: calc(100vw - 32px);
        max-height: calc(100vh - 40px);
        overflow-y: auto;
        background: var(--bg-sidebar);
        border-radius: var(--radius-xl);
        padding: 24px 20px 18px;
        text-align: center;
        box-shadow: var(--shadow-modal);
    }
    .user-card-close {
        position: absolute;
        top: 8px;
        right: 10px;
        background: none;
        border: none;
        color: var(--text-dim);
        font-size: 16px;
        cursor: pointer;
    }
    .user-card-close:hover { color: var(--text-primary); }
    .user-card-avatar {
        position: relative;
        width: 96px;
        height: 96px;
        margin: 0 auto 12px;
        border-radius: var(--radius-round);
        background: var(--accent);
        color: var(--on-accent);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 38px;
        font-weight: 600;
    }
    .user-card-avatar img { border-radius: var(--radius-round); }
    .user-card-avatar .status-dot {
        position: absolute;
        right: 2px;
        bottom: 2px;
        width: 18px;
        height: 18px;
        border: 4px solid var(--bg-sidebar);
    }
    .user-card-name { font-size: 18px; font-weight: 600; color: var(--text-primary); }
    .user-card-username { font-size: 13px; color: var(--text-dim); margin-top: 2px; }
    .user-card-bio {
        margin: 12px 0 0;
        font-size: 13px;
        line-height: 1.45;
        color: var(--text-normal);
        white-space: pre-wrap;
    }
    .user-card-facts {
        margin-top: 14px;
        padding: 12px 14px;
        background: var(--bg-panel);
        border-radius: var(--radius-lg);
        display: grid;
        gap: 8px;
        text-align: left;
    }
    .user-card-facts > div {
        display: flex;
        justify-content: space-between;
        gap: 10px;
        font-size: 12px;
    }
    .user-card-key { color: var(--text-dim); }
    .user-card-value { color: var(--text-primary); text-align: right; }
    .user-card-actions {
        margin-top: 14px;
        display: flex;
        gap: 8px;
        justify-content: center;
    }
    .user-card-btn {
        padding: 8px 14px;
        border: none;
        border-radius: var(--radius-md);
        background: var(--accent);
        color: var(--on-accent);
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
        text-decoration: none;
    }
    .user-card-btn.ghost { background: var(--bg-neutral); }
    .user-card-btn:hover { filter: var(--hover-filter); }
    .msg-author { cursor: pointer; }
    .msg-author:hover { text-decoration: underline; }


/* =========================================================
   НАСТРОЙКИ ВСПЛЫВАЮЩИМ ОКНОМ
   ========================================================= */
    .settings-modal-backdrop {
        position: fixed;
        inset: 0;
        display: none;
        align-items: center;
        justify-content: center;
        background: var(--overlay-soft);
        z-index: 70;
    }
    .settings-modal-backdrop.visible { display: flex; }
    .settings-modal {
        width: 1240px;
        max-width: calc(100vw - 48px);
        max-height: calc(100vh - 48px);
        display: flex;
        flex-direction: column;
        background: var(--bg-sidebar);
        border-radius: var(--radius-xl);
        box-shadow: var(--shadow-modal);
        overflow: hidden;
    }
    .settings-modal-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 14px 18px;
        background: var(--bg-sidebar-dark);
        font-size: 16px;
        font-weight: 600;
        color: var(--text-primary);
    }


/* Крестик модалок этого семейства: прозрачная кнопка, иконка из набора.
   Правило общее, потому что у окна управления сервером своего класса нет:
   без него кнопка оставалась нативной (светлый прямоугольник вокруг X).
   Ниже — поправка для #settings-modal, где кнопки по умолчанию акцентные. */
    .settings-modal-close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        color: var(--text-dim);
        line-height: 1;
        padding: 4px 8px;
        border-radius: var(--radius-sm);
        cursor: pointer;
        transition: color var(--transition-base);
    }
    .settings-modal-close:hover {
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
        color: var(--btn-fg);
        filter: none;
    }
    .settings-modal-close:focus-visible {
        outline: none;
        box-shadow: var(--focus-ring);
    }
    /* Размер иконки задаёт CSS, а не атрибуты разметки: 16 px — как у
       остальных крестиков приложения. Цвет наследуется (currentColor). */
    .settings-modal-close .ico {
        display: block;
        width: 16px;
        height: 16px;
    }
    #settings-modal .settings-modal-close {
        background: transparent;
        border: 1px solid transparent;
        /* ★ Цвет крестика обязан быть задан ЗДЕСЬ. Ниже по файлу
           `#settings-modal button` задаёт `color: var(--on-accent)` —
           почти чёрный; фон крестика при этом прозрачный (`background:
           none` выше), и на тёмной шапке (#0e0d14) иконка исчезала.
           Специфичность (1,1,0) у этого селектора выше, чем (1,0,1) у
           `#settings-modal button`, поэтому цвет побеждает. */
        color: var(--text-dim);
        font-size: 18px;
        /* Область нажатия 44×44 при иконке 16 px. */
        min-width: 44px;
        min-height: 44px;
        padding: 0;
    }
    #settings-modal .settings-modal-close:hover {
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
        color: var(--btn-fg);
        filter: none;
    }
    #settings-modal .settings-modal-close:disabled {
        background: var(--btn-disabled-bg);
        border-color: var(--btn-disabled-border);
        color: var(--btn-disabled-fg);
    }
    /* На широком экране шапка была 49 px: отрицательные поля возвращают
       ей прежнюю высоту (кнопка 44 px, её вклад в поток — 20 px). На
       узком экране 44 px уже задаёт mobile.css, и высоту шапки не трогаем. */
    @media (min-width: 769px) {
        #settings-modal .settings-modal-close { margin: -12px 0; }
    }


/* Многоколоночная раскладка: карточки распределяются по колонкам поровну,
   поэтому окно не растягивается по высоте из-за одного длинного раздела */
    .settings-modal-body {
        padding: 16px 18px 18px;
        overflow-y: auto;
        columns: 330px;
        column-gap: 12px;
    }
    /* Карточка раздела формы настроек. */
    #settings-modal section {
        background: var(--bg-panel);
        border-radius: var(--radius-lg);
        padding: 12px 14px;
        /* карточка не разрывается между колонками */
        break-inside: avoid;
        display: inline-block;
        width: 100%;
        margin: 0 0 12px;
        vertical-align: top;
    }
    #settings-modal h2 {
        margin: 0 0 12px;
        font-size: 14px;
        color: var(--text-normal);
    }
    #settings-modal label.field {
        display: flex;
        align-items: center;
        gap: 10px;
        margin-bottom: 10px;
        font-size: 13px;
    }
    /* --- Форма настроек: один набор правил для окна ---
       Разметка settings/_tabs.html рендерится только в #settings-modal:
       отдельной страницы настроек больше нет (accounts/views.py:settings_page
       отвечает 302 на /app/?settings=1). */
    #settings-modal label.field > span:first-child {
        width: 140px;
        color: var(--text-dim);
        flex: 0 0 auto;
    }
    
    #settings-modal select,
    #settings-modal input[type="range"] { flex: 1; min-width: 0; }
    
    #settings-modal select {
        background: var(--bg-sidebar-dark);
        color: var(--text-primary);
        border: 1px solid var(--bg-sidebar-dark);
        border-radius: var(--radius-md);
        padding: 7px 9px;
        font-size: 13px;
    }
    
    #settings-modal input[type="range"] { accent-color: var(--accent); }
    
    #settings-modal .checkbox {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 8px;
        font-size: 13px;
        color: var(--text-normal);
        cursor: pointer;
    }
    
    #settings-modal .checkbox input { accent-color: var(--accent); cursor: pointer; }
    
    #settings-modal button:not(.settings-modal-close) {
        background: var(--btn-primary-bg);
        color: var(--btn-primary-fg);
        border: 1px solid var(--btn-primary-border);
        border-radius: var(--radius-md);
        padding: 7px 13px;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    color var(--transition-base);
    }
    
    /* Вторичная кнопка окна настроек: раньше здесь была только заливка
       --bg-neutral, а цвет оставался акцентным (--on-accent) — тёмная подпись
       на тёмной заливке, контраст 1.24:1. Теперь поверхность, граница и
       подпись — из общего набора кнопок. */
    #settings-modal button.ghost {
        background: var(--btn-bg);
        color: var(--btn-fg);
        border-color: var(--btn-border);
    }
    
    #settings-modal button:not(.settings-modal-close):disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        opacity: 1;
        cursor: not-allowed;
    }
    #settings-modal button.ghost:hover:not(:disabled) {
        background: var(--btn-bg-hover);
        border-color: var(--btn-border-hover);
        color: var(--btn-fg);
        filter: none;
    }
    #settings-modal button:not(.settings-modal-close):not(.ghost):hover:not(:disabled) {
        background: var(--btn-primary-bg-hover);
        border-color: var(--btn-primary-bg-hover);
        color: var(--btn-primary-fg);
        filter: none;
    }
    
    /* ★ Крестик окна исключён из «подсветки ховера».
       Правило `#settings-modal .settings-modal-close:hover { filter: none; }`
       выше (app.css:1542-1545) НЕ работало никогда: у этого селектора
       специфичность (1,2,0), а у этого — (1,2,1), и он ещё и ниже по файлу.
       Крестик — это `<button class="settings-modal-close">`, поэтому при
       наведении он получал `brightness(1.1)`, хотя комментарий рядом прямо
       говорит «крестик — не акцентная кнопка». Исключение возвращает замысел в
       силу там, где он и записан, и заодно убирает мёртвое объявление: теперь
       `#settings-modal .settings-modal-close:hover` не проигрывает. */
    #settings-modal button:hover:not(:disabled):not(.settings-modal-close) {
        filter: var(--hover-filter);
    }
    
    #settings-modal .row {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: wrap;
        margin-bottom: 8px;
    }
    
    #settings-modal .test-row { display: flex; align-items: center; gap: 10px; }
    
    #settings-modal .meter {
        flex: 1;
        min-width: 140px;
        height: 10px;
        background: var(--bg-sidebar-dark);
        border-radius: 5px;
        overflow: hidden;
    }
    
    #settings-modal .meter-fill {
        height: 100%;
        /* ★ Раскрытие — `transform: scaleX()`, а не `width` (значение пишет
           static/js/settings.js в каждом кадре пробы микрофона).
           Замер на этой же разметке, окно 10 с: было `style.width` в кадре —
           LayoutCount 667, Paint 1188, RasterTask 2312, TaskDuration 0.48 с;
           стало `transform: scaleX()` — LayoutCount 0, Paint 0, TaskDuration
           0.30 с (−37 %). `width` — свойство раскладки: движок пересчитывал
           `.meter`, строку `.test-row` и перерисовывал градиент на каждом
           кадре пробы (60 раз в секунду).
           Вид НЕ изменился: снимок полосы при 20/50/100 % до и после совпал
           до байта (тот же градиент, та же длина заливки). */
        width: 100%;
        transform: scaleX(0);
        transform-origin: 0 50%;
        background: linear-gradient(90deg, var(--success), var(--warning) 70%, var(--danger));
    }
    
    #settings-modal .keybtn { min-width: 120px; font-family: Consolas, monospace; }
    
    #settings-modal .keybtn.capturing { background: var(--danger-solid); }
    
    #settings-modal .hint { color: var(--text-dim); font-size: 11px; line-height: 1.45; margin: 4px 0 0; }
    
    #settings-modal .value {
        color: var(--text-dim);
        font-size: 12px;
        min-width: 48px;
        text-align: right;
        flex: 0 0 auto;
    }
    
    #settings-modal .muted-section { opacity: 0.55; }
}

/* =========================================================
   СЛОЙ features
   ========================================================= */
@layer features {
    .member .m-name {
        color: var(--text-normal);
        font-size: 14px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .member .m-name.owner {
        color: var(--text-primary);
    }


/* --- Непрочитанные сообщения --- */
    .channel.has-unread {
        color: var(--text-primary);
        font-weight: 600;
    }
    .channel.has-unread .name {
        color: var(--text-primary);
    }
    .unread-badge {
        margin-left: auto;
        background: var(--danger-solid);
        color: var(--on-strong);
        font-size: 11px;
        font-weight: 700;
        min-width: 18px;
        height: 18px;
        padding: 0 5px;
        border-radius: 9px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        line-height: 1;
        flex-shrink: 0;
    }
    /* ★ Сумма непрочитанного на иконке сервера — ЧИСЛО, как у личных
       переписей. Показывается всегда, в том числе у приглушённого сервера:
       режимы «Все сообщения / Только упоминания / Ничего» управляют только
       звуком (решение владельца). Кольцо цвета панели отделяет бейдж от
       картинки сервера. */
    .unread-badge.server-unread-badge {
        position: absolute;
        right: -5px;
        bottom: -5px;
        margin: 0;
        min-width: 20px;
        height: 20px;
        padding: 0 6px;
        border-radius: 10px;
        box-shadow: 0 0 0 2px var(--bg-sidebar);
        z-index: 2;
    }



/* --- Участники голосового канала в сайдбаре --- */
    .voice-member-in-sidebar {
        padding: 2px 8px 2px 28px;
        font-size: 13px;
        color: var(--text-dim);
    }
    .voice-member-in-sidebar .vm-row {
        display: flex;
        align-items: center;
        gap: 6px;
    }
    .voice-member-in-sidebar .vm-name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* Ползунка громкости под строкой участника больше нет: он переехал в
       контекстное меню (пункт «Громкость», стили .context-menu-volume*).
       Здесь остаётся только сама строка: аватар, имя и точка «говорит». */
    .speaker-dot {
        width: 6px;
        height: 6px;
        border-radius: var(--radius-round);
        background: var(--offline);
        flex-shrink: 0;
        /* ★ box-shadow из перехода убран: это свойство рисования, и переход по
           нему перерисовывает точку на каждом кадре. Кольцо «говорит»
           появляется сразу — индикатор от этого только отзывчивее.
           Замер: tools/cpu_speaking_bench.py. */
        transition: background var(--transition-base);
    }
    /* ★ Бесконечной анимации box-shadow здесь больше нет. Замер в живом
       канале (Chrome, 12 ядер): пока говорил один участник, она держала
       ~0.73 ядра из 1.04 всей нагрузки страницы, почти всё — в процессе GPU
       (кольцо перерисовывалось 60 раз в секунду). Кольцо и цвет остались:
       «говорит» видно так же, движения убраны. */
    .speaker-dot.active {
        background: var(--success);
        box-shadow: var(--ring-success-soft);
    }
    .speaker-dot.idle {
        background: var(--bg-neutral);
    }
    #self-status-dot {
        position: absolute;
        bottom: -2px;
        right: -2px;
        width: 12px;
        height: 12px;
        border-radius: var(--radius-round);
        border: 3px solid var(--bg-panel);
        transition: background var(--transition-base);
        background: var(--offline);
    }
    #self-status-dot.online { background: var(--success); }
    #self-status-dot.idle { background: var(--warning); }
    #self-status-dot.dnd { background: var(--danger-solid); }
    #self-status-dot.offline { background: var(--offline); }



/* --- Reply: превью над input --- */
    #reply-preview {
        background: var(--bg-sidebar);
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        padding: 8px 12px;
        align-items: center;
        gap: 8px;
        border-left: 3px solid var(--accent);
        margin-bottom: -1px;
    }
    #reply-preview #btn-cancel-reply:hover {
        color: var(--danger);
    }



/* --- Reply: упоминание @author в заголовке --- */
    .reply-mention {
        color: var(--accent);
        background: var(--accent-soft);
        padding: 0 4px;
        border-radius: 3px;
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: background var(--transition-base), color var(--transition-base);
        user-select: none;
    }
    .reply-mention:hover {
        background: var(--accent);
        color: var(--on-accent);
    }



/* --- Reply: цитата над сообщением --- */
    .msg-reply-quote {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 3px 8px;
        margin-bottom: 4px;
        margin-left: 52px;             /* выравниваем под текстом (аватар 40px + gap 12px) */
        max-width: calc(100% - 52px);
        font-size: 13px;
        color: var(--text-dim);
        cursor: pointer;
        background: var(--bg-neutral-soft);
        border-left: 3px solid var(--accent);
        border-radius: 3px;
        transition: background var(--transition-fast);
    }
    .msg-reply-quote:hover {
        background: var(--accent-soft);
    }
    .msg-reply-quote .reply-arrow {
        color: var(--accent);
        font-size: 14px;
        flex-shrink: 0;
        font-weight: bold;
    }
    .msg-reply-quote .reply-author {
        color: var(--accent);
        font-weight: 600;
        flex-shrink: 0;
    }
    .msg-reply-quote .reply-author::after {
        content: ":";
        color: var(--text-faint);
        margin-left: 2px;
    }
    .msg-reply-quote .reply-text {
        color: var(--text-muted);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-style: italic;
        flex: 1;
        min-width: 0;
    }


/* Подсветка сообщения при переходе из цитаты */
    .msg.highlight-flash {
        background: var(--accent-soft);
        border-radius: var(--radius-sm);
        transition: background 0.3s;
    }



/* =========================================================
   6. VOICE
   ========================================================= */

/* --- Слайдер громкости микрофона --- */
    .mic-volume-wrap {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 12px 20px;
        background: var(--bg-sidebar);
        border-radius: var(--radius-md);
        margin-top: 8px;
        border: 1px solid var(--border);
        min-width: 320px;
    }
    .mic-volume-wrap label {
        color: var(--text-muted);
        font-size: 13px;
        white-space: nowrap;
    }
    .mic-volume-wrap input[type="range"] {
        flex: 1;
        accent-color: var(--accent);
    }
    .mic-volume-wrap #mic-volume-label {
        color: var(--text-primary);
        font-size: 13px;
        min-width: 42px;
        text-align: right;
    }
    .voice-btn {
        padding: 12px 24px;
        border-radius: var(--radius-md);
        font-size: 15px;
        font-weight: 600;
        cursor: pointer;
        background: var(--btn-primary-bg);
        color: var(--btn-primary-fg);
        border: 1px solid var(--btn-primary-border);
        transition: background var(--transition-base),
                    border-color var(--transition-base);
    }
    .voice-btn:hover:not(:disabled) {
        background: var(--btn-primary-bg-hover);
        border-color: var(--btn-primary-bg-hover);
    }
    .voice-btn:disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        opacity: 1;
        cursor: not-allowed;
    }
    .voice-btn.danger {
        background: var(--btn-danger-bg);
        color: var(--btn-danger-fg);
        border-color: var(--btn-danger-border);
    }
    .voice-btn.danger:hover:not(:disabled) {
        background: var(--btn-danger-bg-hover);
        border-color: var(--btn-danger-bg-hover);
    }
    .voice-btn.neutral {
        background: var(--btn-bg);
        color: var(--btn-fg);
        border-color: var(--btn-border);
    }
    .voice-btn.neutral:hover:not(:disabled) {
        background: var(--btn-bg-hover);
        border-color: var(--btn-border-hover);
    }
    .voice-btn.success { background: var(--success); }
    .voice-btn.success:hover:not(:disabled) { background: var(--success-hover); }
    .voice-status {
        font-size: 14px;
        color: var(--text-dim);
    }
    .voice-status.on { color: var(--success); }
    .voice-status.err { color: var(--danger); }
    .voice-conn-title {
        font-size: 14px;
        font-weight: 600;
        color: var(--success);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* Качество связи: цвет — семантический токен на обёртке, сама иконка
       остаётся currentColor и не несёт своей заливки. */
    .voice-quality {
        display: inline-flex;
        align-items: center;
        gap: 4px;
    }
    .voice-quality--excellent { color: var(--success); }
    .voice-quality--good { color: var(--warning); }
    .voice-quality--poor { color: var(--danger); }
    .voice-controlbar .voice-status {
        font-size: 12px;
        color: var(--text-dim);
    }
    .voice-controlbar-buttons {
        display: flex;
        align-items: center;
        gap: 10px;
    }


/* Круглые кнопки как в Discord. Состояния (.active/.muted/.danger)
   наследуются от кнопок сайдбара — логика не дублируется. */
    .voice-controlbar .vcb-btn {
        flex: 0 0 auto;
        width: 46px;
        height: 46px;
        min-height: 46px;
        border-radius: var(--radius-round);
        background: var(--btn-bg);
        color: var(--btn-fg);
        /* Видимая граница: круглая кнопка на панели должна читаться как
           элемент, а не как «дырка» в фоне. Размер фиксирован, поэтому
           граница его не меняет. */
        border: 1px solid var(--btn-border);
        /* Блик по верхнему краю: «стеклянный» вид без backdrop-filter,
           поэтому не нагружает слабые машины в игре. */
        box-shadow: var(--inset-highlight);
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    transform var(--transition-base);
    }
    .voice-controlbar .vcb-btn:hover:not(:disabled) {
        background: var(--btn-bg-hover);
        transform: translateY(-1px);
        border-color: var(--btn-border-hover);
        box-shadow: var(--neon-glow), var(--inset-highlight);
    }
    .voice-controlbar-right {
        display: flex;
        align-items: center;
        gap: 12px;
    }
    .voice-sysaudio {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 12px;
        color: var(--text-dim);
        cursor: pointer;
        user-select: none;
        white-space: nowrap;
    }
    .voice-sysaudio input {
        accent-color: var(--accent);
        cursor: pointer;
    }


/* Пока нет подключения — прячем кнопки управления */
    .voice-controlbar.is-idle .voice-controlbar-buttons,
    .voice-controlbar.is-idle .voice-controlbar-right {
        display: none;
    }
    .voice-controlbar.is-idle .voice-conn-title {
        color: var(--text-muted);
    }


/* Всплывающая подсказка в голосовой области */
    .voice-toast {
        position: absolute;
        left: 50%;
        bottom: 88px;
        transform: translateX(-50%) translateY(8px);
        max-width: 560px;
        padding: 10px 14px;
        background: var(--bg-menu);
        color: var(--text-primary);
        font-size: 13px;
        line-height: 1.4;
        border-radius: var(--radius-lg);
        box-shadow: var(--shadow-menu);
        opacity: 0;
        pointer-events: none;
        transition: opacity var(--transition-base), transform var(--transition-base);
        z-index: 20;
    }
    .voice-toast.show {
        opacity: 1;
        transform: translateX(-50%) translateY(0);
    }


/* Индикация говорящего на плитках участников */
    .video-placeholder.speaking,
    .video-placeholder.speaking .avatar-circle {
        box-shadow: 0 0 0 3px var(--success);
    }


/* Точка статуса в строке участника голосового канала */
    .voice-member-in-sidebar .vm-avatar {
        position: relative;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 18px;
        height: 18px;
        border-radius: var(--radius-round);
        overflow: hidden;
        background: var(--accent);
        color: var(--on-accent);
        font-size: 10px;
        font-weight: 600;
        flex-shrink: 0;
    }
    .voice-member-in-sidebar .vm-avatar .status-dot {
        position: absolute;
        right: -2px;
        bottom: -2px;
        width: 9px;
        height: 9px;
        border-radius: 50%;
        border: 2px solid var(--bg-sidebar);
        background: var(--offline);
        box-sizing: content-box;
    }
    .voice-member-in-sidebar .vm-avatar .status-dot.online { background: var(--success); }
    .voice-member-in-sidebar .vm-avatar .status-dot.idle { background: var(--warning); }
    .voice-member-in-sidebar .vm-avatar .status-dot.dnd { background: var(--danger-solid); }
    .voice-member-in-sidebar .vm-avatar .status-dot.offline { background: var(--offline); }
/* =========================================================
   РАСКЛАДКА ГОЛОСОВОЙ ОБЛАСТИ — сетка 16:9 и «крупный + полоса»
   =========================================================
   Геометрию считает static/js/voice/grid.js (computeGridLayout и
   computePinnedLayout) и кладёт её CSS-переменными на #video-grid:
     --voice-cols / --voice-rows    сколько плиток в ряду и в столбце;
     --voice-grid-tile-w / -h       размер одной плитки (всегда 16:9);
     --voice-grid-w / -h            размер всей сетки — по нему центрируем.
   Ячейка сетки больше не решает, какого размера будет плитка (именно из-за
   этого видео с камеры обрезалось по краям): первичен размер плитки, сетка
   подстраивается под свободное место, а лишнее место остаётся пустым по
   краям. В режиме .screen-active эти правила не действуют — там раскладку
   задаёт блок «ГОЛОСОВАЯ СЕТКА» слоя layout (полоса плиток под сценой). */
    #video-grid-container:not(.screen-active) {
        align-items: center;        /* лишнее место — по краям, а не в плитке */
        justify-content: center;
    }
    #video-grid-container:not(.screen-active) #video-grid {
        grid-template-columns: repeat(var(--voice-cols, 1), var(--voice-grid-tile-w, 1fr));
        grid-template-rows: repeat(var(--voice-rows, 1), var(--voice-grid-tile-h, 1fr));
        grid-auto-rows: var(--voice-grid-tile-h, 1fr);
        width: var(--voice-grid-w, 100%);
        height: var(--voice-grid-h, auto);
        flex: 0 0 auto;             /* размер задаёт расчёт, а не свободное место */
    }


/* Полоса миниатюр режима «крупный + полоса». Элемента нет в разметке —
   его создаёт grid.js один раз и только перемещает в него существующие
   плитки (пересборка отвязала бы <video> от дорожки LiveKit).

   ★ Размер плиток полосы вычисляет grid.js:computePinnedLayout ОТ ИХ ЧИСЛА и
   кладёт сюда переменными. Полоса занимает всю высоту (справа) или ширину
   (снизу), n равных плиток с зазорами делят её без остатка — пустоты внизу
   правой колонки больше нет. При переполнении (плитка уже STRIP_TILE_MIN_H)
   включается прокрутка; место под неё зарезервировано в --voice-strip-w/h. */
    .voice-grid-strip { display: none; }
    #video-grid-container.is-pinned-layout:not(.screen-active) {
        flex-direction: row;        /* крупный слева, полоса справа */
    }
    #video-grid-container.is-pinned-layout.strip-bottom:not(.screen-active) {
        flex-direction: column;     /* крупный сверху, полоса снизу */
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip {
        display: flex;
        flex: 0 0 auto;
        align-self: center;
        gap: 8px;
        width: var(--voice-strip-w, 170px);
        height: var(--voice-strip-h, auto);
        /* Тонкая полоса прокрутки — видимый признак «ниже есть ещё»;
           её ширина/высота совпадает с STRIP_SCROLLBAR в grid.js (8).
           ★ scrollbar-color здесь НЕ задаём: Chromium от него переключается на
           нативную полосу (15 px) и игнорирует ::-webkit-scrollbar, а продукт
           резервирует 8 — плитка уезжала под полосу. Цвет задают правила ниже. */
        overflow: hidden;
    }
    #video-grid-container.is-pinned-layout.strip-right:not(.screen-active) .voice-grid-strip {
        flex-direction: column;
        overflow-x: hidden;
        overflow-y: auto;
    }
    #video-grid-container.is-pinned-layout.strip-bottom:not(.screen-active) .voice-grid-strip {
        flex-direction: row;
        overflow-x: auto;
        overflow-y: hidden;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip::-webkit-scrollbar {
        width: 8px;
        height: 8px;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip::-webkit-scrollbar-thumb {
        background: var(--text-muted);
        border-radius: 999px;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip::-webkit-scrollbar-track {
        background: transparent;
    }
    /* Плитка полосы: обе стороны задаёт расчёт (16:9 в нём уже учтён).
       Размеры фиксированы, поэтому полоса прокрутки их не сжимает: место под
       неё зарезервировано в размере самой полосы. */
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip > .voice-tile {
        flex: 0 0 auto;
        width: var(--voice-strip-tile-w, 170px);
        height: var(--voice-strip-tile-h, 96px);
        aspect-ratio: 16 / 9;
        border-radius: var(--radius-lg);
    }
    /* Аватар миниатюры центрируется и остаётся круглым; размер — от плитки
       (на 96 px плитке круг 320 px не поместился бы). */
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-avatar .avatar-circle {
        width: auto;
        height: var(--voice-strip-avatar, 48px);
        min-height: 0;
        max-height: none;
        aspect-ratio: 1 / 1;
        font-size: var(--voice-strip-avatar-font, 20px);
        border-radius: var(--radius-round);
        display: flex;
        align-items: center;
        justify-content: center;
        background: var(--accent);
        color: var(--on-accent);
        overflow: hidden;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-avatar .avatar-circle img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-name {
        left: 4px;
        bottom: 4px;
        max-width: calc(100% - 30px);
        padding: 2px 6px;
        font-size: var(--voice-strip-name-font, 12px);
        line-height: 1.25;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-indicators {
        top: 4px;
        right: 4px;
        gap: 2px;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-indicators .indicator {
        width: var(--voice-strip-badge, 24px);
        height: var(--voice-strip-badge, 24px);
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-indicators .indicator .ico {
        width: 60%;
        height: 60%;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-pin {
        right: 4px;
        bottom: 4px;
    }
    #video-grid-container.is-pinned-layout:not(.screen-active) .voice-grid-strip .voice-tile-pin .ico {
        width: var(--voice-strip-badge, 24px);
        height: var(--voice-strip-badge, 24px);
    }
    .voice-tile {
        position: relative;
        display: flex;
        align-items: center;
        justify-content: center;
        min-width: 0;
        min-height: 0;
        /* Плитка участника — «сцена», а не панель: на ней лежат подпись и
           значки ПОВЕРХ аватарки. Токен, а не --bg-sidebar-dark: в светлой
           теме сцена тёмная (светлая плитка на белой панели сливалась), а
           имя/значки берут цвета с --on-media. В «по умолчанию» значение
           токена равно прежнему --bg-sidebar-dark — вид не изменился. */
        background: var(--bg-voice-stage);
        border: 2px solid transparent;
        border-radius: var(--radius-xl);
        overflow: hidden;
        cursor: pointer;
        /* ★ Перехода по border-color и box-shadow здесь больше нет: «говорит»
           меняет и рамку, и кольцо, а переход по этим свойствам красит плитку
           целиком на каждом кадре. Замер: при трёх говорящих убирание этого
           перехода дало −0.22 ядра (0.61 → 0.39). Рамка и кольцо появляются
           мгновенно — так даже отзывчивее. */
    }
    .voice-tile:hover { border-color: var(--border-soft); }
    .voice-tile.local { background: var(--bg-voice-local); }


/* Говорит — зелёная рамка, как в Discord */
    .voice-tile.speaking {
        border-color: var(--success);
        box-shadow: 0 0 0 1px var(--success);
    }


/* «Фокус» на участнике — плитка на всю ширину */
    .voice-tile.pinned {
        grid-column: 1 / -1;
        border-color: var(--accent);
    }
    .voice-tile-video {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }
    .voice-tile.local .voice-tile-video {
        transform: scaleX(-1);   /* своё видео — как в зеркале */
    }


/* Обёртки растягиваем на всю плитку, иначе процент высоты у аватара
   разрешается в auto и круг остаётся маленьким */
    .voice-tile-avatar,
    .voice-tile-avatar .video-placeholder {
        width: 100%;
        height: 100%;
        min-height: 0;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .voice-tile-avatar .video-placeholder {
        background: transparent;
        padding: 0;
        border-radius: 0;
    }
    #video-grid .voice-tile-avatar .avatar-circle {
        /* аватар занимает большую часть плитки и остаётся круглым:
           размер считается от высоты плитки, ширина — по аспекту 1:1 */
        height: var(--voice-avatar, 140px);
        max-height: 320px;
        min-height: 48px;
        aspect-ratio: 1 / 1;
        width: auto;
        display: flex;
        align-items: center;
        justify-content: center;
        background: var(--accent);
        color: var(--on-accent);
        font-size: clamp(20px, 5vh, 84px);
        font-weight: 600;
        border-radius: var(--radius-round);
        overflow: hidden;
    }
    #video-grid .voice-tile-avatar .avatar-circle img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }
    .voice-tile-name {
        position: absolute;
        left: 8px;
        bottom: 8px;
        max-width: calc(100% - 44px);
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 3px 8px;
        background: var(--overlay-soft);
        border-radius: var(--radius-sm);
        font-size: 13px;
        color: var(--white);
    }
    .voice-tile-name > span {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .voice-tile-indicators {
        position: absolute;
        right: 8px;
        top: 8px;
        display: flex;
        gap: 4px;
    }
    .voice-tile-indicators .indicator {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 24px;
        height: 24px;
        border-radius: var(--radius-round);
        background: var(--overlay-soft);
        /* Значок лежит на тёмной плашке (--overlay-soft) поверх сцены плитки,
           поэтому цвет берётся с --on-media: в светлой теме это белый, иначе
           тёмный значок на тёмной плашке не виден. */
        color: var(--on-media);
    }
    .voice-tile-indicators .indicator.muted { color: var(--danger-on-media); }
    .voice-tile-pin {
        position: absolute;
        right: 8px;
        bottom: 8px;
        display: inline-flex;
        color: var(--on-media);
    }
    .voice-tile-pin .ico {
        width: 14px;
        height: 14px;
    }


/* Пустой голосовой канал */
/* Баннер «нет доступа к микрофону/камере» */
    .voice-permission-hint {
        display: none;
        align-items: flex-start;
        gap: 10px;
        margin: 8px 12px;
        padding: 10px 12px;
        border: 1px solid var(--warning);
        border-radius: 8px;
        background: var(--bg-warning-soft);
        color: var(--warning);
        font-size: 13px;
        line-height: 1.4;
    }
    .voice-permission-hint.show { display: flex; }
    .voice-permission-hint .vph-text { flex: 1; }
    .voice-permission-hint .vph-actions { display: flex; align-items: center; gap: 8px; }
    .voice-permission-hint button {
        background: none;
        border: 1px solid currentColor;
        color: inherit;
        border-radius: 6px;
        padding: 4px 8px;
        cursor: pointer;
        font-size: 12px;
    }
    .voice-permission-hint .vph-close { border: none; font-size: 14px; padding: 2px 4px; }
    .voice-empty {
        display: none;
        flex: 1 1 auto;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 10px;
        min-height: 200px;
        color: var(--text-dim);
        font-size: 14px;
        text-align: center;
    }
    .voice-empty button {
        padding: 8px 16px;
        border-radius: var(--radius-md);
        background: var(--accent);
        font-weight: 600;
        cursor: pointer;
    }
    .voice-empty button:hover { background: var(--accent-hover); }



/* --- Быстрые контролы голосового канала --- */
    #voice-quick-controls {
        display: flex;
        gap: 6px;
        padding: 8px;
        background: var(--bg-panel);
        border-top: 1px solid var(--border);
    }
    .vqc-btn {
        flex: 1;
        padding: 8px 4px;
        /* Иконочная кнопка: прозрачное тело в покое, видимая поверхность и
           граница при наведении. */
        background: transparent;
        color: var(--btn-icon);
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        font-size: 16px;
        line-height: 1;
        cursor: pointer;
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    color var(--transition-base);
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 32px;
    }
    .vqc-btn:hover:not(:disabled) {
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
        color: var(--btn-fg);
    }
    .vqc-btn:active:not(:disabled) { background: var(--btn-bg-active); }
    .vqc-btn:disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        cursor: not-allowed;
    }
    .vqc-btn.danger {
        background: var(--btn-danger-bg);
        color: var(--btn-danger-fg);
        border-color: var(--btn-danger-border);
    }
    .vqc-btn.danger:hover:not(:disabled) {
        background: var(--btn-danger-bg-hover);
        border-color: var(--btn-danger-bg-hover);
    }
    .vqc-btn.active {
        background: var(--success);
        color: var(--on-strong);
        border-color: var(--success);
    }
    .vqc-btn.active:hover:not(:disabled) { background: var(--success-hover); }
    .vqc-btn.muted {
        background: var(--btn-danger-bg);
        color: var(--btn-danger-fg);
        border-color: var(--btn-danger-border);
    }
    .vqc-btn.muted:hover:not(:disabled) {
        background: var(--btn-danger-bg-hover);
        border-color: var(--btn-danger-bg-hover);
    }


/* --- Аватар в правом сайдбаре (.m-avatar) --- */
    .member .m-avatar {
        width: 32px;
        height: 32px;
        border-radius: var(--radius-round);
        background: var(--accent);
        color: var(--on-accent);
        display: flex;
        align-items: center;
        justify-content: center;
        font-weight: 600;
        font-size: 13px;
        flex-shrink: 0;
        position: relative;
    }
    .member .m-avatar img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        border-radius: var(--radius-round);
    }
    .member .m-avatar .status-dot {
        position: absolute;
        bottom: -2px;
        right: -2px;
        width: 12px;
        height: 12px;
        border-radius: var(--radius-round);
        background: var(--offline);
        border: 3px solid var(--bg-sidebar);
    }
    .member .m-avatar .status-dot.online { background: var(--success); }
    .member .m-avatar .status-dot.idle { background: var(--warning); }
    .member .m-avatar .status-dot.dnd { background: var(--danger-solid); }
    .member .m-avatar .status-dot.offline { background: var(--offline); }


/* --- Мини-аватар в голосовом сайдбаре (.vm-avatar) --- */
    .voice-member-in-sidebar .vm-avatar img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }



/* =========================================================
   11. SCREEN STAGE (демонстрация экрана)
   Режимы: inline | fullscreen | pip
   ========================================================= */
    .screen-stage {
        background: var(--bg-media);
        border-radius: 8px;
        overflow: hidden;
        position: relative;
        width: 100%;
    }


/* --- Inline (по умолчанию — встроен в voice-area) --- */
    .screen-stage--inline {
        flex: 1 1 auto;       /* ★ растягиваем на всё доступное */
        width: 100%;
        min-height: 0;        /* ★ нужно для flex-детей, чтобы не переполнять */
        max-width: none;      /* ★ убираем 960px */
        aspect-ratio: auto;   /* ★ убираем жёсткое соотношение */
    }


/* --- Fullscreen (на весь viewport) --- */
    .screen-stage--fullscreen {
        position: fixed;
        inset: 0;
        width: 100vw;
        height: 100vh;
        z-index: 9999;
        border-radius: 0;
        max-width: none;
        aspect-ratio: auto;
    }
    .screen-stage--fullscreen .screen-stage-controls {
        opacity: 1;
    }


/* --- Настоящий полноэкранный режим (Fullscreen API) ---
   Сцена лежит в top layer, размеры задаёт браузер: убираем скругление,
   подложку и растягиваем видео на весь экран. Правило для
   -webkit-full-screen продублировано отдельно: неизвестный селектор в
   общем списке отбросил бы правило целиком (так делает не-Chromium). */
    .screen-stage:fullscreen {
        width: 100vw;
        height: 100vh;
        border-radius: 0;
        background: var(--bg-menu);
    }
    .screen-stage:-webkit-full-screen {
        width: 100vw;
        height: 100vh;
        border-radius: 0;
        background: var(--bg-menu);
    }
    .screen-stage:fullscreen .screen-stage-controls,
    .screen-stage:-webkit-full-screen .screen-stage-controls {
        opacity: 1;
    }
    .screen-stage:fullscreen::backdrop {
        background: var(--bg-menu);
    }



/* Кнопки управления — появляются при наведении */
    .screen-stage-controls {
        position: absolute;
        top: 8px;
        right: 8px;
        display: flex;
        gap: 6px;
        z-index: 10;
        opacity: 0;                         /* ← скрыты */
        transition: opacity 0.15s;
    }
    .screen-stage:hover .screen-stage-controls {
        opacity: 1;
    }
    .screen-stage-btn {
        width: 32px;
        height: 32px;
        border: none;
        border-radius: 4px;
        background: var(--overlay-modal);
        color: var(--on-strong);
        font-size: 16px;
        line-height: 1;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: background 0.15s;
    }
    .screen-stage-btn:hover {
        background: var(--bg-accent-strong);
    }
    .screen-stage-btn:active {
        background: var(--accent);
    }
/* Включённый режим (полный экран / отдельное окно): кнопка помечена нажатой */
    .screen-stage-btn.active {
        background: var(--accent);
        color: var(--text-primary);
    }
    .screen-stage-btn .ico {
        width: 18px;
        height: 18px;
    }


/* Видео внутри stage */
    .screen-stage video {
        width: 100%;
        height: 100%;
        object-fit: contain;
        display: block;
    }


/* Тёмная подложка за fullscreen-stage */
    .screen-stage-backdrop {
        position: fixed;
        inset: 0;
        background: var(--overlay-strong);
        z-index: 9998;
    }


/* ===== ИНДИКАТОР ГОЛОСОВОГО — КЛИКАБЕЛЬНОЕ ИМЯ ===== */
    .voice-indicator-clickable {
        transition: color 0.15s, text-decoration 0.15s;
    }
    /* ★ `!important` убран: цвет `--success` приходит по НАСЛЕДОВАНИЮ от
       родителя `#voice-indicator` (templates/servers/app.html:144 —
       `style="… color:var(--success);"`), а других источников цвета у
       `.voice-indicator-clickable` нет ни в CSS, ни в JS (`voice/session.js:721`
       меняет только `textContent` и `display`). Объявление ничего не меняло,
       важность при нём была карго-культом. Подчёркивание при наведении остаётся
       и продолжает работать — оно и есть видимая реакция на ховер. */
    .voice-indicator-clickable:hover {
        color: var(--success);                 /* светло-зелёный при наведении */
        text-decoration: underline;
    }



/* Слайдер громкости демонстрации */
    .screen-stage-volume {
        position: absolute;
        bottom: 8px;
        right: 8px;
        display: flex;
        align-items: center;
        gap: 8px;
        background: var(--overlay-modal);
        color: var(--on-strong);
        padding: 6px 10px;
        border-radius: 6px;
        font-size: 12px;
        z-index: 10;
        opacity: 0;
        transition: opacity 0.15s;
    }
    .screen-stage:hover .screen-stage-volume {
        opacity: 1;
    }
    .screen-stage-volume input[type="range"] {
        width: 100px;
        accent-color: var(--accent);
    }
    .screen-stage-volume-icon {
        font-size: 14px;
    }
    #screen-audio-volume-label {
        min-width: 38px;
        text-align: right;
    }
    #video-grid-container.is-hidden {
        display: none;
    }


/* --- Плейсхолдер (когда камеры нет) ---
   Единственное место с правилами .video-placeholder: раньше блок был объявлен
   дважды, и позднее правило перебивало раннее. Дубль `background` убран —
   значение у обоих объявлений было одно и то же (var(--bg-sidebar-dark)),
   поэтому вычисленный фон не меняется. */
    .video-placeholder {
        position: absolute;
        inset: 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        background: var(--bg-sidebar-dark);
        border-radius: var(--radius-xl);
        color: var(--text-normal);
        font-size: 14px;
        font-weight: 600;
        gap: 10px;
    }
    .video-placeholder .avatar-circle {
        width: 48px;
        height: 48px;
        border-radius: 50%;
        background: var(--accent);
        color: var(--on-accent);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 20px;
        font-weight: 700;
        user-select: none;
        overflow: hidden;
    }
    .video-placeholder .avatar-circle img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }


/* Раньше блок был объявлен дважды: color: var(--text-normal) перебивался
   поздним color: var(--text-muted). Мёртвое первое объявление убрано —
   вычисленный цвет остаётся var(--text-muted). */
    .video-placeholder .placeholder-name {
        font-size: 14px;
        color: var(--text-muted);
        max-width: 80%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }



/* =========================================================
   ЧАТ: кнопка «вниз» и относительное позиционирование области
   ========================================================= */
    #chat-view {
        position: relative;
    }
    .jump-to-bottom {
        position: absolute;
        right: 20px;
        bottom: 92px;
        width: 38px;
        height: 38px;
        display: none;
        align-items: center;
        justify-content: center;
        border: 1px solid var(--btn-primary-border);
        border-radius: var(--radius-round);
        background: var(--btn-primary-bg);
        color: var(--btn-primary-fg);
        font-size: 18px;
        line-height: 1;
        cursor: pointer;
        z-index: 6;
        box-shadow: var(--shadow-menu);
        transition: background var(--transition-base), transform var(--transition-base);
    }
    .jump-to-bottom.visible {
        display: flex;
    }
    .jump-to-bottom:hover {
        background: var(--btn-primary-bg-hover);
        border-color: var(--btn-primary-bg-hover);
        transform: translateY(-1px);
    }
    .jump-to-bottom:disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        opacity: 1;
        cursor: not-allowed;
    }
    /* Индикатор догрузки НОВЫХ сообщений у нижней границы ленты (после
       перехода к закреплённому/найденному сообщению). Живёт в контейнере окна
       (#chat-view/#dm-view, оба relative) поверх ленты, поэтому не сдвигает
       сообщения и не участвует в их порядке. Цвета — токенами. */
    .messages-newer-loading {
        position: absolute;
        left: 50%;
        bottom: 92px;
        transform: translateX(-50%);
        z-index: 5;
        padding: 6px 12px;
        border: 1px solid var(--border-soft);
        border-radius: var(--radius-pill);
        background: var(--bg-menu);
        color: var(--text-muted);
        font-size: 12px;
        line-height: 1.2;
        box-shadow: var(--shadow-menu);
        pointer-events: none;
    }
    .messages-newer-loading[hidden] {
        display: none;
    }


/* Инлайн-код и блоки */
    .msg-code {
        background: var(--bg-menu);
        padding: 1px 4px;
        border-radius: 3px;
        font-family: var(--font-mono);
        font-size: 13px;
    }
    .msg-pre {
        background: var(--bg-menu);
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        padding: 8px 10px;
        margin: 4px 0;
        overflow-x: auto;
        font-family: var(--font-mono);
        font-size: 13px;
        white-space: pre-wrap;
    }


/* Ссылки */
    .msg-text a {
        color: var(--link);
        text-decoration: none;
    }
    .msg-text a:hover {
        text-decoration: underline;
    }


/* Упоминания */
    .msg-mention {
        background: var(--mention-bg);
        color: var(--text-primary);
        border-radius: 3px;
        padding: 0 2px;
        font-weight: 500;
    }
    .msg-mention.me {
        background: var(--mention-self-bg);
        color: var(--mention-self-fg);
    }



/* =========================================================
   СПИСОК УЧАСТНИКОВ: поиск
   ========================================================= */
    .members-search {
        width: calc(100% - 16px);
        margin: 4px 8px 6px;
        padding: 6px 10px;
        background: var(--bg-sidebar-dark);
        color: var(--text-normal);
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        font-size: 13px;
        outline: none;
    }
    .members-search::placeholder {
        color: var(--text-faint);
    }
    .members-search:focus {
        border-color: var(--accent);
    }



/* =========================================================
   ЧАТ: непрочитанные сообщения
   ========================================================= */
    .new-divider {
        display: flex;
        align-items: center;
        gap: 8px;
        margin: 12px 0 6px;
        color: var(--danger);
        font-size: 12px;
        font-weight: 600;
    }
    .new-divider::before,
    .new-divider::after {
        content: '';
        flex: 1;
        height: 1px;
        background: var(--danger-solid);
        opacity: 0.5;
    }
    .msg.msg-new {
        border-left: 2px solid var(--danger);
        margin-left: -10px;
        padding-left: 8px;
    }



/* =========================================================
   АВТОДОПОЛНЕНИЕ УПОМИНАНИЙ
   ========================================================= */
    .mention-menu {
        display: none;
        position: absolute;
        left: 16px;
        right: 16px;
        bottom: 100%;
        max-height: 240px;
        overflow-y: auto;
        background: var(--bg-menu);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
        box-shadow: var(--shadow-menu);
        z-index: 8;
    }
    .mention-item {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 6px 10px;
        cursor: pointer;
        font-size: 13px;
        color: var(--text-normal);
    }
    .mention-item.active,
    .mention-item:hover {
        background: var(--bg-active);
    }
    .mention-avatar {
        display: inline-flex;
        width: 20px;
        height: 20px;
        flex-shrink: 0;
    }
    .mention-name {
        flex: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .mention-username {
        color: var(--text-faint);
        font-size: 12px;
    }
}

/* =========================================================
   СЛОЙ pages
   ========================================================= */
@layer pages {

}


/* =========================================================
   ДОБАВЛЕНО ПО СПИСКУ ДИЗАЙНЕРА (фон колонок, границы, ховер
   кнопок пользователя, пустое состояние канала, сетка фона)
   ========================================================= */
@layer components {

    /* Кнопки в панели пользователя (настройки, выход): при наведении
       подсвечиваются акцентом, как остальные активные элементы. */
    .user-panel .icon-btn:hover {
        border-color: var(--neon-edge);
        box-shadow: var(--neon-glow);
        color: var(--accent);
    }


    /* Пустое состояние канала переехало в отдельный блок .chat-empty в разметке
       (см. templates/servers/app.html): у него есть заголовок, подпись и кнопка
       приглашения. Прежний вариант через .messages-area:empty::before убран —
       он показывал второй, дублирующий текст. */


    /* Техно-сетка под сообщениями убрана по замечанию дизайнера: линии
       читались как «клетка» из квадратов. Фон области сообщений теперь не
       задаётся вовсе — сквозь него видно фон приложения с узором сот. */

}


/* =========================================================
   ТОКЕНЫ -> КОМПОНЕНТЫ (кнопки панели пользователя,
   пустое состояние канала, капсульная кнопка)
   ========================================================= */
@layer layout {

    /* Кнопки нижней панели (настройки, выход, скачать). Идут через :where(),
       чтобы специфичность осталась нулевой: иначе это правило перебило бы
       контекстные стили кнопок в окне настроек (#settings-modal button),
       которое специально идёт с высокой специфичностью. */
    :where(.user-panel button, .user-panel .icon-btn) {
        color: var(--text-icon);
        /* ★ Без перехода по `box-shadow`: ореол — две размытые тени, и переход
           перерисовывал их все 0.15 с каждого наведения. Появляется мгновенно,
           как у `.speaker-dot` / `.voice-tile` / `.vcb-btn`. */
        transition: color var(--transition-base);
    }
    :where(.user-panel button:hover, .user-panel .icon-btn:hover) {
        color: var(--accent);
        box-shadow: var(--glow-accent);
    }

}

@layer components {

    /* =========================================================
       ПУСТОЕ СОСТОЯНИЕ КАНАЛА и капсульная кнопка
       ========================================================= */
    /* Блок показывается только когда в канале нет сообщений.
       Сообщения и их количество знает chat.js, поэтому признак берётся из
       разметки: контейнер #messages получает класс has-messages от chat.js —
       но правки в JS не нужны, потому что блок кладётся РЯДОМ с #messages, а
       не внутрь: chat.js очищает свой контейнер, и вложенный блок он бы стёр. */
    .chat-empty {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100%;
        text-align: center;
        padding: 40px 20px;
        box-sizing: border-box;
    }
    #chat-view:has(.messages-area:not(:empty)) .chat-empty {
        display: none;
    }

    .chat-empty h2 {
        color: var(--text-primary);
        font-size: 24px;
        margin: 0 0 8px;
    }
    /* Знак канала рисуется CSS, а не текстом в разметке: имя канала знает только
       chat.js (он заполняет #chat-channel-name), и трогать его нельзя. Знак
       вынесен отдельным элементом, чтобы заголовок оставался текстом. */
    .chat-empty h2 .hash {
        color: var(--accent);
        margin-right: 6px;
    }
    .chat-empty p {
        max-width: 420px;
        color: var(--text-muted);
        font-size: 14px;
        line-height: 1.5;
        margin: 0 0 24px;
    }

    /* Капсульная кнопка. Первая кнопка в проекте такого вида: раньше были
       только .btn-primary / .btn-secondary и контекстные кнопки. */
    .btn-pill {
        border-radius: var(--radius-pill);
        padding: 10px 22px;
        /* Вторичная капсула: собственная поверхность, видимая граница и
           читаемая подпись. Раньше тело было почти цвета панели
           (в светлой теме — буквально белое на белом). */
        background: var(--btn-bg);
        color: var(--btn-fg);
        border: 1px solid var(--btn-border);
        box-shadow: none;
        font-family: var(--font-ui);
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: background var(--transition-base),
                    border-color var(--transition-base),
                    color var(--transition-base);
    }
    .btn-pill:hover:not(:disabled) {
        background: var(--btn-bg-hover);
        color: var(--btn-fg);
        border-color: var(--btn-border-hover);
    }
    .btn-pill:active:not(:disabled) {
        background: var(--btn-bg-active);
        transform: translateY(1px);
    }
    .btn-pill:disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        cursor: not-allowed;
        box-shadow: none;
    }

}


/* =========================================================
   СТЕКЛЯННЫЕ ЭЛЕМЕНТЫ УПРАВЛЕНИЯ

   Ползунки, флажки и подписи разделов по референсам дизайнера. Системные
   элементы управления браузер умеет красить только через accent-color, и
   форма остаётся браузерной — в референсах же это стеклянные капсулы со
   светящимся бегунком. Поэтому форма задана явно.

   Псевдоэлементы дорожки и бегунка у WebKit и Firefox разные: правила идут
   парными блоками, один набор для Chrome, второй для Firefox. Если оставить
   только WebKit, в Firefox ползунок вернётся к системному виду.
   ========================================================= */
@layer components {

    /* --- Подписи разделов ---
       Мелкие, в верхнем регистре, с разрядкой — как MENU и TEAM в референсах.
       Было 0.02em: буквы стояли почти вплотную и строка читалась как обычный
       текст, а не как заголовок раздела. */
    .channel-group-label,
    .members-group-label {
        letter-spacing: 0.08em;
        color: var(--text-faint);
    }


    /* --- Ползунок: дорожка --- */
    input[type="range"] {
        -webkit-appearance: none;
        appearance: none;
        background: transparent;
        cursor: pointer;
    }
    input[type="range"]::-webkit-slider-runnable-track {
        height: 6px;
        border-radius: var(--radius-pill);
        background: var(--bg-input);
        /* Внутренняя тень вместо рамки: рамка на дорожке в WebKit считается
           от другого прямоугольника, и бегунок вставал криво. */
        box-shadow: inset 0 0 0 1px var(--border), var(--inset-glow);
    }
    input[type="range"]::-webkit-slider-thumb {
        -webkit-appearance: none;
        appearance: none;
        width: 14px;
        height: 14px;
        /* Половина разницы высот дорожки (6) и бегунка (14), со знаком минус. */
        margin-top: -4px;
        border-radius: var(--radius-round);
        background: var(--accent);
        box-shadow: var(--glow-thumb), var(--inset-highlight);
    }
    input[type="range"]::-moz-range-track {
        height: 6px;
        border-radius: var(--radius-pill);
        background: var(--bg-input);
        box-shadow: inset 0 0 0 1px var(--border);
    }
    input[type="range"]::-moz-range-thumb {
        width: 14px;
        height: 14px;
        border: none;
        border-radius: var(--radius-round);
        background: var(--accent);
        box-shadow: var(--glow-thumb);
    }
    /* Кольцо фокуса — с клавиатуры видно, какой ползунок выбран. */
    input[type="range"]:focus {
        outline: none;
    }
    input[type="range"]:focus-visible::-webkit-slider-thumb {
        box-shadow: var(--glow-thumb), var(--focus-ring);
    }
    input[type="range"]:focus-visible::-moz-range-thumb {
        box-shadow: var(--glow-thumb), var(--focus-ring);
    }


    /* --- Флажок ---
       Системный флажок заменён на стеклянный квадрат со светящейся заливкой
       при выборе. Галочка рисуется рамками повёрнутого прямоугольника:
       отдельной картинки не нужно. */
    input[type="checkbox"] {
        -webkit-appearance: none;
        appearance: none;
        width: 16px;
        height: 16px;
        margin: 0;
        border-radius: var(--radius-sm);
        background: var(--bg-input);
        box-shadow: inset 0 0 0 1px var(--border);
        cursor: pointer;
        position: relative;
        flex-shrink: 0;
        vertical-align: middle;
        /* ★ Без перехода по `box-shadow`: галочка переключается мгновенно, а
           переход заставлял перерисовывать размытый ореол на каждом кадре. */
        transition: background var(--transition-base);
    }
    input[type="checkbox"]:hover {
        box-shadow: inset 0 0 0 1px var(--border-strong);
    }
    input[type="checkbox"]:checked {
        background: var(--accent);
        box-shadow: var(--glow-accent), inset 0 0 0 1px var(--accent-hover);
    }
    input[type="checkbox"]:checked::after {
        content: "";
        position: absolute;
        left: 5px;
        top: 1px;
        width: 4px;
        height: 9px;
        border: solid var(--on-accent);
        border-width: 0 2px 2px 0;
        transform: rotate(45deg);
    }
    input[type="checkbox"]:focus-visible {
        outline: none;
        box-shadow: inset 0 0 0 1px var(--border-strong), var(--focus-ring);
    }
}


/* =========================================================
   НЕОНОВАЯ ОБВОДКА ЭЛЕМЕНТОВ

   Градиентная грань: у элемента прозрачная граница, а фон собран из двух
   слоёв. Первый — цвет тела, обрезается по padding-box; второй — градиент,
   обрезается по border-box. Поэтому градиент виден ровно в полосе границы и
   повторяет скругление. Через border-image так не сделать: он не работает с
   border-radius.
   ========================================================= */
@layer components {

    /* --- Кнопки «+» у заголовков групп каналов ---
       Разметку создаёт channels.js (`.channel-group-label`), здесь — вид.
       ★ Раньше здесь стояли 13 пометок `!important`: стили были зашиты прямо в
       строку JS (`style="float:right;background:none;border:none;color:#949ba4;
       cursor:pointer;font-size:16px;line-height:1;"`), а инлайновый стиль бьёт
       любое обычное объявление независимо от слоёв. Инлайновый стиль из строки
       убран (features/channels.js:234, 264 — правка узкая, только эта строка),
       а всё, что он задавал, переехало сюда: `float: right` вместо него, а
       `background:none; border:none; color; cursor:pointer; font-size:16px;
       line-height:1` уже было (цвет — токеном `--text-muted`, а не литералом).
       `box-shadow` у `.add-channel:hover` совпадал с базовым и потому ничего
       не менял — это объявление убрано вместе с `!important`.
       Проверка: `tools/run_smoke.py` (маркеры кнопки «+» — часть renderChannels). */
    .add-channel {
        float: right;
        font-size: 16px;
        background-image: linear-gradient(var(--neon-body), var(--neon-body)),
                          var(--neon-grad);
        background-origin: border-box;
        background-clip: padding-box, border-box;
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        box-shadow: var(--inset-highlight);
        color: var(--text-muted);
        cursor: pointer;
        line-height: 1;
        padding: 3px 7px;
    }
    .add-channel:hover {
        color: var(--accent);
    }

    /* --- Иконки в панели пользователя ---
       Значок читался плохо, хотя контраст у прежнего серого был в норме
       (7.9:1 на теле кнопки). Причина не в контрасте, а в визуальном весе:
       штрих тонкий, значок мелкий. Поэтому цвет светлее и размер чуть больше,
       а не «ещё контрастнее». Размер задаётся атрибутами в JS (16 px), здесь
       он перебивается — иначе значок остался бы прежним. */
    .user-actions .icon-btn {
        color: var(--text-normal);
    }
    .user-actions .icon-btn svg {
        width: 18px;
        height: 18px;
    }
    .user-actions .icon-btn:hover {
        color: var(--btn-fg);
    }


    /* --- Кнопки-иконки ---
       Тело прозрачное: иконка читается на панели, а состояние наведения
       показывает заливка из общего набора кнопок. */
    .icon-btn {
        background: transparent;
        background-image: none;
    }
    .icon-btn:hover {
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
        box-shadow: none;
    }

    /* --- Кнопки голосовой панели --- */
    .voice-controlbar .vcb-btn {
        background: var(--btn-bg);
        background-image: none;
        border-color: var(--btn-border);
    }

    /* --- Иконки серверов в рельсе --- */
    .server-icon {
        background-image: linear-gradient(var(--neon-body), var(--neon-body)),
                          var(--neon-grad);
        background-origin: border-box;
        background-clip: padding-box, border-box;
    }
    .server-icon.active {
        background-image: linear-gradient(var(--accent), var(--accent)),
                          var(--neon-grad);
    }

    /* --- Капсульная кнопка ---
       Никаких градиентных тел: поверхность и граница — из токенов кнопок,
       иначе в светлой теме тело снова становится белым на белой панели. */
    .btn-pill {
        background: var(--btn-bg);
        background-image: none;
        border-color: var(--btn-border);
    }
    .btn-pill:hover:not(:disabled) {
        background: var(--btn-bg-hover);
        border-color: var(--btn-border-hover);
    }

    /* --- Строка ввода сообщения ---
       Тот же приём, что у капсульной кнопки: тёмное тело и градиентная грань.
       В покое свечение мягче, при вводе — сильнее. */
    .input-wrapper {
        border: 1px solid transparent;
        background-image: linear-gradient(var(--bg-input), var(--bg-input)),
                          var(--neon-grad);
        background-origin: border-box;
        background-clip: padding-box, border-box;
        /* Ореол убран по замечанию дизайнера: остаётся градиентная грань. */
        box-shadow: none;
    }
    /* В фокусе — тонкое акцентное кольцо вместо ореола: состояние ввода должно
       быть видно, но свечения здесь быть не должно. */
    .input-wrapper:focus-within {
        box-shadow: 0 0 0 1px var(--border-strong);
    }

    /* --- Обводка аватаров ---
       Кольцо рисуется той же двухслойной техникой: тело аватара закрывает
       середину, в полосе границы остаётся градиент. Кольцо тонкое (2 px),
       иначе на аватаре 32 px оно съедает лицо. */
    .member .m-avatar,
    .msg .msg-avatar {
        border: 2px solid transparent;
        background-image: linear-gradient(var(--accent), var(--accent)),
                          var(--neon-ring);
        background-origin: border-box;
        background-clip: padding-box, border-box;
        box-shadow: var(--neon-ring-glow);
    }
    /* У аватара с картинкой тело прозрачное, а кольцо остаётся. */
    .msg .msg-avatar:has(img) {
        background-image: linear-gradient(transparent, transparent),
                          var(--neon-ring);
    }
}


/* =========================================================
   ГОЛОСОВОЙ ЭКРАН (слой features)

   Правила лежат именно в features, а не в components, потому что исходное
   оформление .voice-btn и .voice-empty объявлено в features. Слой важнее
   порядка в файле: features объявлен в списке @layer позже components,
   поэтому он сильнее, и правило из components проигрывало бы независимо от
   того, что записано ниже по файлу. На этом уже спотыкались: кнопки
   оставались залитыми акцентом, хотя правила были на месте.
   ========================================================= */
@layer features {
        /* --- Голосовой экран ---
           «Подключиться» и «Пригласить участников» — ОСНОВНЫЕ действия:
           акцентная заливка и читаемая подпись. Градиентное тело убрано:
           в светлой теме оно давало белое на белом, а подпись на градиенте
           не проходила 4.5:1. */        .voice-btn,
        .voice-empty button {
            border: 1px solid var(--btn-primary-border);
            background: var(--btn-primary-bg);
            background-image: none;
            color: var(--btn-primary-fg);
            box-shadow: none;
        }
        .voice-btn:hover:not(:disabled),
        .voice-empty button:hover {
            background: var(--btn-primary-bg-hover);
            border-color: var(--btn-primary-bg-hover);
            color: var(--btn-primary-fg);
        }

        .voice-btn.danger {
            background: var(--btn-danger-bg);
            border-color: var(--btn-danger-border);
            color: var(--btn-danger-fg);
        }
        .voice-btn.danger:hover:not(:disabled) {
            background: var(--btn-danger-bg-hover);
            border-color: var(--btn-danger-bg-hover);
        }
        .voice-btn.neutral {
            background: var(--btn-bg);
            border-color: var(--btn-border);
            color: var(--btn-fg);
        }
        .voice-btn.neutral:hover:not(:disabled) {
            background: var(--btn-bg-hover);
            border-color: var(--btn-border-hover);
        }
        .voice-btn.success {
            background: var(--success);
            border-color: var(--success);
            color: var(--on-strong);
        }
        .voice-btn.success:hover:not(:disabled) {
            background: var(--success-hover);
        }

        /* Значок пустого канала: было серое полупрозрачное пятно (opacity 0.5).
           Стало кольцо с той же градиентной гранью, что у аватаров, и значок
           акцентом внутри — как круглые значки в референсе. */
        .voice-empty .voice-empty-icon {
            width: 76px;
            height: 76px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 34px;
            opacity: 1;
            border: 2px solid transparent;
            border-radius: var(--radius-round);
            background-image: linear-gradient(var(--neon-body), var(--neon-body)),
                              var(--neon-ring);
            background-origin: border-box;
            background-clip: padding-box, border-box;
            color: var(--accent);
        }

        /* Нижняя панель голосового экрана: светлая кромка сверху и плотный фон.
           ★ `backdrop-filter` убран: панель лежит ВНУТРИ `.main-area`, а та уже
           размыта (`--glass-blur`, правило для четырёх панелей каркаса выше).
           Вложенное стекло — это второй проход чтения подложки по результату
           первого плюс ещё один композитный слой, а сквозь нижнюю панель видна
           ровно та же подложка, что и сквозь `.main-area`. Фон берётся плотным
           токеном `--bg-panel` — ровно то, что уже делает базовое правило
           `.voice-controlbar` (слой layout) и `.main-header`; вид панели от
           этого не меняется (проверено снимками на 1440 и 390). Кромку
           оставляем: она и держит «стеклянный» вид без размытия. */
        .voice-controlbar {
            background: var(--bg-panel);
            border-top: 1px solid var(--rim);
            border-radius: var(--radius-lg);
        }

        /* =====================================================
           ОКНО ПОДТВЕРЖДЕНИЯ ПРИГЛАШЕНИЯ
           (templates/servers/app.html + static/js/features/invite-modal.js)
           ===================================================== */
        /* Оболочка — существующие .settings-modal-backdrop/.settings-modal,
           поэтому здесь только размер и внутренняя раскладка. Узкое окно: это
           вопрос «вступить или нет», а не управление сервером на три раздела. */
        .invite-modal {
            width: 420px;
            max-width: calc(100vw - 32px);
        }

        .invite-modal-body {
            position: relative;
            padding: 28px 24px 22px;
            text-align: center;
        }

        .invite-modal-close {
            position: absolute;
            top: 10px;
            right: 12px;
            background: none;
            border: none;
            color: var(--text-dim);
            font-size: 16px;
            line-height: 1;
            padding: 4px 8px;
            cursor: pointer;
        }

        .invite-modal-close:hover {
            color: var(--text-primary);
        }

        .invite-modal-close:focus-visible {
            outline: none;
            box-shadow: var(--focus-ring);
            border-radius: var(--radius-sm);
        }

        /* Аватар сервера: 72×72 — тот же размер, что у карточки на странице
           приглашения, чтобы окно и страница выглядели одинаково. */
        .invite-modal-avatar {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 72px;
            height: 72px;
            margin: 4px auto 16px;
        }

        .invite-modal-avatar img {
            width: 72px;
            height: 72px;
            border-radius: var(--radius-round);
            object-fit: cover;
        }

        /* Инициал — запасной вариант, если не отдалась и картинка кота
           (рисует TVLib.avatarFallback, единственное место с этим решением). */
        .invite-modal-avatar .avatar-initial {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 72px;
            height: 72px;
            border-radius: var(--radius-round);
            background: var(--bg-input);
            color: var(--text-primary);
            font-size: 28px;
            font-weight: 700;
        }

        .invite-modal-title {
            margin: 0 0 4px;
            font-size: 13px;
            font-weight: 600;
            letter-spacing: 0.08em;
            text-transform: uppercase;
            color: var(--text-muted);
        }

        /* Название сервера — главное в окне, поэтому крупно и первым планом. */
        .invite-modal-server {
            font-size: 22px;
            font-weight: 700;
            color: var(--text-primary);
            /* Длинное название не должно ломать окно. */
            overflow-wrap: anywhere;
        }

        .invite-modal-sub {
            margin: 10px 0 0;
            font-size: 14px;
            line-height: 1.5;
            color: var(--text-muted);
        }

        .invite-modal-state {
            margin: 14px 0 0;
            font-size: 14px;
            line-height: 1.45;
            min-height: 20px;
            color: var(--text-normal);
        }

        /* Цвет состояния — по data-state, который ставит модуль:
           busy/loading — спокойный, ok — успех, member — тоже успех
           («вы уже участник» это не ошибка), invalid/error — опасность.
           Цвета только токенами. */
        .invite-modal-state[data-state="ok"],
        .invite-modal-state[data-state="member"] {
            color: var(--success);
        }

        .invite-modal-state[data-state="invalid"],
        .invite-modal-state[data-state="error"] {
            color: var(--danger);
        }

        .invite-modal-state[data-state="loading"],
        .invite-modal-state[data-state="busy"] {
            color: var(--text-dim);
        }

        .invite-modal-actions {
            display: flex;
            gap: 10px;
            justify-content: center;
            margin-top: 22px;
        }

        .invite-modal-actions .btn-pill {
            flex: 1 1 0;
            min-width: 0;
        }

        /* Главная кнопка окна: залита акцентом (это основное действие), но
           остаётся той же капсулой .btn-pill, что и «Отмена» — иначе в одном
           окне оказались бы две разные кнопки. */
        .invite-modal-actions .invite-btn-primary {
            background: var(--btn-primary-bg);
            color: var(--btn-primary-fg);
            border-color: var(--btn-primary-border);
        }

        .invite-modal-actions .invite-btn-primary:hover:not(:disabled) {
            background: var(--btn-primary-bg-hover);
            border-color: var(--btn-primary-bg-hover);
            color: var(--btn-primary-fg);
        }

        .invite-modal-actions .btn-pill:disabled {
            background: var(--btn-disabled-bg);
            color: var(--btn-disabled-fg);
            border-color: var(--btn-disabled-border);
            opacity: 1;
            cursor: default;
        }

        /* На узком экране (390 px и телефоны) окно не должно вылезать за
           границы: кнопки в столбик и меньше отступы. Тот же брейкпоинт,
           что у остальной мобильной раскладки. */
        @media (max-width: 480px) {
            .invite-modal-body {
                padding: 22px 16px 18px;
            }

            .invite-modal-actions {
                flex-direction: column-reverse;
            }
        }
}

/* ★ Правило ВНЕ СЛОЁВ. Раньше оно лежало без обёртки, а неименованный слой
   сильнее ЛЮБОГО именованного: вместе с `!important` (для которого порядок
   слоёв вообще обратный) оно перебивало всё, что когда-либо будет написано про
   `.tv-avatar-file` в любом слое любого файла. Ровно эту болезнь в проекте уже
   ловили и лечили — в `profile.css` (шапка файла) и дважды описывали в
   документах. Теперь правило в слое `components` (это компонент формы профиля)
   и без `!important`: конкурентов на `display` у `.tv-avatar-file` нет ни в
   одном файле — проверено поиском по `static/css` (кроме этого места селектор
   не встречается), поэтому подавление осталось ровно таким же. Заодно
   `check_styleguide`-класс замечаний «правило вне слоёв» больше не находит в
   `app.css` ни одного. */
@layer components {
    /* «Родная» обвязка поля выбора файла: `ClearableFileInput` рисует вокруг
       `<input>` ссылку «Сейчас: <файл>» и флажок «Очистить», и они ОСТАЮТСЯ
       видны, хотя сам `input` спрятан (JS ставит ему `display: none`). В блоке
       аватара это выглядело как ссылка с именем файла и «Очистить» посреди
       формы — человек видел их там, где ожидает круг с фото. Прячем ВСЮ
       обвязку: загрузку открывает `<label class="avatar-upload-btn">`, а
       текущий аватар показывает круг. Правка касается только формы профиля:
       контейнер `.tv-avatar-file` ставится в `accounts/profile.html` и
       `accounts/_profile_form.html`. */
    .tv-avatar-file,
    .tv-avatar-file * {
        display: none;
    }
}

/* ==================================================================
   ВЫБОР ОБЛАСТИ АВАТАРА (.tv-avatar-crop-*)
   ------------------------------------------------------------------
   Разметку строит static/js/features/avatar-crop.js, здесь только вид.

   ★ Почему этот блок лежит в app.css (слой components), а не в profile.css
   (слой pages). Форма профиля показывается В ДВУХ местах: на странице
   /profile/ и в окне профиля на /app/. Стили из profile.css на /app/ НЕ
   ЗАГРУЖАЮТСЯ (в шаблоне его нет), и первый живой прогон это показал
   буквально: класс .is-cropping на кадре был, а правила не применялись —
   кадр остался 80x80, полотно 76x76, кнопки масштаба без оформления.
   Оформление компонента, который живёт на двух страницах, обязано лежать в
   общем файле компонентов. Правило «страничные правила — в файл страницы»
   относится к тому, что есть ровно на одной странице (шапка, контейнер
   профиля) — они и остались в profile.css.

   Цвета — только токены: --checker-a/--checker-b/--checker-size из
   tokens.css, --overlay-soft для затемнения вне круга, --focus-ring для
   кольца фокуса. Новых литералов нет (см. tools/check_colors.py).

   Как это выглядит: пока файл не выбран, всё как раньше — круглый превью
   80x80 и кнопка «Загрузить аватар». После выбора блок разворачивается в
   столбец, круг становится кадром выбора, под ним — подсказка, кнопки
   масштаба и «Сохранить»/«Отмена».
   ================================================================== */

@layer components {

/* Пока файл не выбран, ни кадр, ни панель не занимают места: форма выглядит
   ровно как до этой правки. Признак «открыт» — `hidden` на самих элементах
   (его ставит и снимает avatar-crop.js); правила с `[hidden]` стоят первыми и
   выигрывают при равной специфичности, поэтому ничего не мигает до выбора. */
.tv-avatar-crop-stage[hidden],
.tv-avatar-crop-editor[hidden] {
    display: none;
}

/* Кадр выбора. `!important` здесь НЕ для борьбы со слоями, а из-за второй
   страницы: в app.css уже есть `.profile-modal-form .avatar-preview` с
   размерами 80x80 (две классовые части, как и у этого правила), и порядок
   файлов решал бы исход. Правило про кадр обязано выигрывать всегда — оно
   про состояние, а не про оформление. */
.avatar-preview.is-cropping {
    width: 100% !important;
    max-width: 320px;
    height: auto !important;
    aspect-ratio: 1 / 1;
    margin: 0 auto;
    border-radius: var(--radius-lg);
    overflow: visible;
    background: none;
    border-color: var(--accent);
    display: block;
    /* ★ Запас под сообщение об ошибке даёт САМ ТЕКСТ, а не коробка кадра.
       Текст ошибки — элемент БЛОКА (`features/avatar-crop.js` кладёт его рядом
       с кадром, а не внутрь): внутри кадра он вылезал за коробку
       (`overflow: visible` обязателен), а коробка при `aspect-ratio: 1 / 1` не
       растёт от содержимого — следующий элемент, «Загрузить аватар», вставал
       под коробкой и перечёркивался текстом (измерено: `overlap: true`,
       `btnTop 485` против `errBottom 505`). */
    flex: 0 0 auto;
    touch-action: none;          /* палец двигает кадр, а не прокручивает окно */
    cursor: grab;
    position: relative;
}

.avatar-preview.is-cropping:active {
    cursor: grabbing;
}

/* Картинку прежнего аватара в кадре прячем: её место занимает полотно. */
.avatar-preview.is-cropping img,
.avatar-preview.is-cropping .avatar-initial,
.avatar-preview.is-cropping .avatar-default {
    display: none;
}

.tv-avatar-crop-stage {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--radius-lg);
    /* Шахматка: прозрачные места PNG видно как прозрачные, а не как чёрный
       квадрат (иначе человек не понимает, что попадёт в аватар). */
    background-color: var(--checker-a);
    background-image:
        linear-gradient(45deg, var(--checker-b) 25%, transparent 25%),
        linear-gradient(-45deg, var(--checker-b) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--checker-b) 75%),
        linear-gradient(-45deg, transparent 75%, var(--checker-b) 75%);
    background-size: var(--checker-size) var(--checker-size);
    background-position: 0 0, 0 calc(var(--checker-size) / 2),
        calc(var(--checker-size) / 2) calc(var(--checker-size) / -2),
        calc(var(--checker-size) / -2) 0;
}

.avatar-preview.is-cropping .tv-avatar-crop-stage:not([hidden]) {
    display: block;
}

.tv-avatar-crop-canvas,
.tv-avatar-crop-mask,
.tv-avatar-crop-ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

/* Затемнение вне круга — отдельным полотном (см. renderMask в avatar-crop.js):
   человек видит, что осталось за кадром, и двигает фото осознанно. */
.tv-avatar-crop-mask {
    pointer-events: none;
}

/* Ободок круга: ярче, когда фокус на кадре — «сюда смотрят клавиши».
   ★ Без перехода по `box-shadow`: ободок меняет цвет мгновенно, а переход
   перерисовывал круг на каждом кадре перехода фокуса. */
.tv-avatar-crop-ring {
    border-radius: var(--radius-round);
    pointer-events: none;
    box-shadow: 0 0 0 2px var(--border-strong);
}

.avatar-preview.is-cropping:focus {
    outline: none;
}

.avatar-preview.is-cropping:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.avatar-preview.is-cropping:focus-visible .tv-avatar-crop-ring {
    box-shadow: 0 0 0 2px var(--accent);
}

/* Панель управления: подсказка, масштаб, «Сохранить»/«Отмена». Признак
   «открыт» — `hidden`; правило с ним стоит первым и выигрывает у
   `display: flex` при равной специфичности. */
.tv-avatar-crop-editor {
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: 100%;
    margin-top: 12px;
}

.tv-avatar-crop-hint {
    margin: 0;
    color: var(--text-faint);
    font-size: 12px;
    line-height: 1.4;
}

.tv-avatar-crop-zoom-row {
    display: flex;
    gap: 8px;
}

.tv-avatar-crop-zoom {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* `text-align` и размер знака — для текстового «−»: у набора значков
       (`static/js/icons.js`) минуса нет, поэтому кнопка «уменьшить» рисует
       символ текстом. Без выравнивания он уезжал к левому краю, и кнопка
       выглядела пустой (живой прогон это и показал). */
    text-align: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--bg-neutral);
    color: var(--text-primary);
    cursor: pointer;
    transition: background var(--transition-base);
}

.tv-avatar-crop-zoom:hover:not(:disabled) {
    background: var(--bg-neutral-hover);
}

.tv-avatar-crop-zoom:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Текстовый знак «−»: размер как у значка набора (18 px), чтобы кнопки
   «уменьшить» и «увеличить» выглядели одинаково. */
.tv-avatar-crop-zoom-sign {
    font-size: 18px;
    line-height: 1;
}

.tv-avatar-crop-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.tv-avatar-crop-actions .btn-primary,
.tv-avatar-crop-actions .btn-secondary {
    padding: 8px 16px;
    font-size: 14px;
}

/* Ошибка — рядом с кадром, а не вместо него: прежний аватар при этом остаётся
   на месте, и человеку есть куда вернуться.
   ★ Текст ошибки — обычный элемент БЛОКА (ширина кадра, своё место в потоке).
   Внутри коробки кадра он не помещался: `overflow: visible` там обязателен,
   а `aspect-ratio: 1 / 1` не даёт коробке вырасти от содержимого — текст
   вылезал за неё и ложился на кнопку «Загрузить аватар» (измерено живым
   прогоном: `overlap: true`, `btnTop 485` против `errBottom 505`). Переносить
   его «в абсолют» тоже нельзя: тогда коробка кадра не знает о тексте, и
   наложение остаётся. Место под текст даёт сам текст. */
.tv-avatar-crop-error {
    flex: 0 0 auto;
    width: 100%;
    max-width: 320px;
    margin: 0 auto;
    color: var(--danger);
    font-size: 13px;
}

.avatar-preview.is-busy {
    opacity: 0.7;
    pointer-events: none;
}

/* ★ Редактор раскрывается на ВСЮ ширину блока: это не «две колонки», а
   рабочая область с кадром, подсказкой и кнопками. Строку разворачиваем в
   столбец на ЛЮБОЙ ширине, а не только на узкой: в окне профиля на 1440 две
   колонки оставляли кадру 34 px, и выбирать область было нечем. `:has()`
   поддержан во всех браузерах проекта (WebView2/Chromium, Safari 15.4+). */
.profile-avatar-block:has(.avatar-preview.is-cropping) {
    flex-direction: column;
    align-items: stretch;
}

.profile-avatar-block:has(.avatar-preview.is-cropping) .avatar-info {
    align-items: center;
}

/* Тот же разворот БЕЗ `:has()` — запасной путь: на узком экране строку
   разворачиваем в столбец и здесь, а превью с кнопкой встают друг под друга.
   768px — та же граница, что у --bp-md в tokens.css и у шторок панелей в
   mobile.css. */
@media (max-width: 768px) {
    .profile-avatar-block {
        flex-direction: column;
        align-items: stretch;
        text-align: center;
    }

    .profile-avatar-block .avatar-info {
        align-items: center;
    }

    .profile-avatar-block .avatar-upload-btn {
        align-self: stretch;
        text-align: center;
    }
}

    /* ★ Недоступная кнопка в слое components.
       Правило в base проигрывает ЛЮБОМУ правилу components: у .icon-btn,
       .attach-button, .settings-modal-close, .jump-to-bottom своя заливка,
       и disabled снова становился неотличим от активного. Здесь тот же
       контракт, но в том слое, который эти правила объявляет. */
    button:disabled {
        background: var(--btn-disabled-bg);
        color: var(--btn-disabled-fg);
        border-color: var(--btn-disabled-border);
        opacity: 1;
        cursor: not-allowed;
    }

}   /* @layer components */

/* =========================================================
   ПОИСК ПО СООБЩЕНИЯМ (канал и личная беседа)
   =========================================================
   Панель — сосед ленты, а не её часть: позиционируется поверх, поэтому
   открытие поиска не сдвигает сообщения и не меняет scrollTop. Ширина
   ограничена и на узком экране (см. mobile.css), длинные строки переносятся
   — горизонтальной прокрутки быть не должно. */
@layer components {
    #chat-view,
    #dm-view {
        position: relative;
    }

    .chat-search-btn {
        margin-left: auto;
        width: 32px;
        height: 32px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        background: transparent;
        color: var(--text-muted);
        cursor: pointer;
        font-size: 18px;
    }

    .chat-search-btn:hover {
        color: var(--btn-fg);
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
    }

    .chat-search-btn:focus-visible,
    .message-search-close:focus-visible,
    .message-search-result:focus-visible,
    .message-search-scope:focus-visible,
    .message-search-input:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 1px;
    }

    .chat-search-btn[aria-expanded="true"] {
        color: var(--accent);
        background: var(--accent-soft);
    }

    .message-search-panel {
        position: absolute;
        top: 52px;
        right: 12px;
        width: min(420px, calc(100% - 24px));
        max-height: min(70vh, 560px);
        display: flex;
        flex-direction: column;
        min-width: 0;
        overflow: hidden;
        z-index: 30;
        background: var(--bg-panel);
        border: 1px solid var(--rim);
        border-radius: var(--radius-md);
        box-shadow: var(--panel-shadow);
    }

    .message-search-panel[hidden] {
        display: none;
    }

    .message-search-head {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 8px;
        border-bottom: 1px solid var(--border);
    }

    .message-search-input {
        flex: 1 1 auto;
        min-width: 0;
        width: 100%;
        padding: 8px 10px;
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        background: var(--bg-sidebar);
        color: var(--text-primary);
        font: inherit;
    }

    .message-search-input::placeholder {
        color: var(--text-faint);
    }

    .message-search-close {
        flex: 0 0 auto;
        width: 32px;
        height: 32px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        background: transparent;
        color: var(--text-muted);
        cursor: pointer;
        font-size: 16px;
    }

    .message-search-close:hover {
        color: var(--btn-fg);
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
    }

    .message-search-filters {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 6px 10px;
        border-bottom: 1px solid var(--border);
    }

    .message-search-scope {
        min-width: 0;
        max-width: 100%;
        padding: 4px 8px;
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        background: var(--bg-sidebar);
        color: var(--text-primary);
        font: inherit;
        font-size: 13px;
    }

    .message-search-count {
        margin-left: auto;
        color: var(--text-faint);
        font-size: 12px;
        white-space: nowrap;
    }

    .message-search-list {
        min-height: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    .message-search-result {
        display: flex;
        flex-direction: column;
        gap: 2px;
        width: 100%;
        min-width: 0;
        padding: 10px 12px;
        border: 0;
        border-bottom: 1px solid var(--border);
        background: transparent;
        color: var(--text-primary);
        text-align: left;
        font: inherit;
        cursor: pointer;
    }

    .message-search-result:hover {
        background: var(--bg-sidebar);
    }

    .message-search-result-top {
        display: flex;
        align-items: baseline;
        gap: 8px;
        min-width: 0;
    }

    .message-search-result-where {
        color: var(--text-muted);
        font-size: 12px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .message-search-result-time {
        margin-left: auto;
        color: var(--text-faint);
        font-size: 12px;
        white-space: nowrap;
    }

    .message-search-result-meta {
        color: var(--accent);
        font-size: 12px;
        font-weight: 600;
        overflow-wrap: anywhere;
    }

    .message-search-result-text {
        color: var(--text-primary);
        font-size: 13px;
        line-height: 1.35;
        overflow-wrap: anywhere;
    }

    .message-search-result-text mark {
        background: var(--accent-soft);
        color: var(--text-primary);
        border-radius: 2px;
        padding: 0 1px;
    }

    .message-search-hint {
        margin: 0;
        padding: 8px 12px;
        color: var(--text-muted);
        font-size: 12px;
        line-height: 1.35;
        overflow-wrap: anywhere;
    }

    .message-search-hint:empty {
        display: none;
    }

    .message-search-hint.is-error {
        color: var(--danger);
    }
}

/* =========================================================
   ЗАКРЕПЛЁННЫЕ СООБЩЕНИЯ (канал и личная беседа)
   =========================================================
   Кнопка — в шапке окна рядом с поиском, счётчик — значком на ней.
   Панель — сосед ленты (как поиск), поэтому открытие не сдвигает сообщения.
   Строка списка — кнопка: доступна с клавиатуры и не требует второго
   «открыть окно» (переход идёт существующим путём). Оформление общее:
   у канала и беседы одинаковые классы, отличаются только id. */
@layer components {
    .chat-pin-btn {
        position: relative;
        margin-left: auto;
        width: 32px;
        height: 32px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        background: transparent;
        color: var(--text-muted);
        cursor: pointer;
        font-size: 18px;
    }

    /* Кнопка закрепления стоит ПЕРВОЙ и прижимает к правому краю пару
       «закрепить + поиск» — и в канале, и в личной беседе. */
    .chat-pin-btn + .chat-search-btn {
        margin-left: 0;
    }

    .chat-pin-btn:hover {
        color: var(--btn-fg);
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
    }

    .chat-pin-btn[aria-expanded="true"] {
        color: var(--accent);
        background: var(--accent-soft);
    }

    .chat-pin-btn:focus-visible,
    .pinned-panel-close:focus-visible,
    .pinned-item:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 1px;
    }

    .chat-pin-count {
        position: absolute;
        top: -3px;
        right: -3px;
        min-width: 16px;
        height: 16px;
        padding: 0 4px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: var(--radius-pill);
        background: var(--accent);
        color: var(--on-accent);
        font-size: 10px;
        font-weight: 700;
        line-height: 1;
    }

    .chat-pin-count[hidden] {
        display: none;
    }

    .pinned-panel {
        position: absolute;
        top: 52px;
        right: 12px;
        width: min(440px, calc(100% - 24px));
        max-height: min(70vh, 560px);
        display: flex;
        flex-direction: column;
        min-width: 0;
        overflow: hidden;
        z-index: 31;
        background: var(--bg-panel);
        border: 1px solid var(--rim);
        border-radius: var(--radius-md);
        box-shadow: var(--shadow-menu);
    }

    .pinned-panel[hidden] {
        display: none;
    }

    .pinned-panel-head {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 8px 10px;
        border-bottom: 1px solid var(--border);
        color: var(--text-primary);
    }

    .pinned-panel-title {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        min-width: 0;
        font-size: 14px;
        font-weight: 600;
        white-space: nowrap;
    }

    .pinned-panel-title .ico {
        color: var(--accent);
    }

    .pinned-panel-count {
        color: var(--text-faint);
        font-size: 12px;
        white-space: nowrap;
    }

    .pinned-panel-count:empty {
        display: none;
    }

    .pinned-panel-close {
        margin-left: auto;
        width: 28px;
        height: 28px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        background: transparent;
        color: var(--text-muted);
        cursor: pointer;
    }

    .pinned-panel-close:hover {
        color: var(--btn-fg);
        background: var(--btn-icon-hover-bg);
        border-color: var(--btn-icon-hover-border);
    }

    .pinned-list {
        min-height: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    .pinned-item {
        display: flex;
        flex-direction: column;
        gap: 3px;
        width: 100%;
        min-width: 0;
        padding: 10px 12px;
        border: 0;
        border-bottom: 1px solid var(--border);
        background: transparent;
        color: var(--text-primary);
        text-align: left;
        font: inherit;
        cursor: pointer;
    }

    .pinned-item:hover {
        background: var(--bg-sidebar);
    }

    .pinned-item-top {
        display: flex;
        align-items: baseline;
        gap: 8px;
        min-width: 0;
    }

    .pinned-item-author {
        color: var(--accent);
        font-size: 13px;
        font-weight: 600;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .pinned-item-time {
        margin-left: auto;
        color: var(--text-faint);
        font-size: 11px;
        white-space: nowrap;
    }

    .pinned-item-text {
        display: -webkit-box;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
        overflow: hidden;
        color: var(--text-primary);
        font-size: 13px;
        line-height: 1.35;
        overflow-wrap: anywhere;
    }

    .pinned-item-meta {
        display: flex;
        align-items: center;
        flex-wrap: wrap;
        gap: 4px 10px;
        color: var(--text-muted);
        font-size: 11px;
        line-height: 1.3;
    }

    .pinned-item-meta .ico {
        color: var(--text-icon);
    }

    .pinned-item-meta > span {
        display: inline-flex;
        align-items: center;
        gap: 3px;
        min-width: 0;
        overflow-wrap: anywhere;
    }

    .pinned-hint {
        margin: 0;
        padding: 8px 12px;
        color: var(--text-muted);
        font-size: 12px;
        line-height: 1.35;
        overflow-wrap: anywhere;
    }

    .pinned-hint:empty {
        display: none;
    }

    .pinned-hint.is-error {
        color: var(--danger);
    }

    /* Метка в ленте: «Закреплено» рядом с временем. Не занимает отдельную
       строку и не сдвигает сообщение — значит, не ломает раскладку ленты. */
    .msg-pin-mark {
        display: inline-flex;
        align-items: center;
        gap: 3px;
        color: var(--accent);
        font-size: 11px;
        font-weight: 600;
        white-space: nowrap;
    }

    .msg-pin-mark .ico {
        width: 12px;
        height: 12px;
    }

    /* Неактивный пункт меню (нет права закреплять). Не притворяется
       активным: цвет приглушён, курсор — not-allowed, наведение не
       подсвечивает. Подсказка под пунктом объясняет, какого права нет. */
    .context-menu-item.disabled,
    .context-menu-item.disabled:hover {
        background: transparent;
        color: var(--text-faint);
        cursor: not-allowed;
    }
}

/* =========================================================
   ЛЕНИВЫЕ ЧАНКИ: индикатор загрузки и карточка ошибки
   (static/js/lib/lazy.js). Скромно и поверх всего: это не окно,
   а «мы уже грузим то, что вы попросили».
   ========================================================= */
.tv-lazy-indicator {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 9500;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--bg-menu);
    color: var(--text-primary);
    font-size: 13px;
    line-height: 1.2;
    box-shadow: var(--shadow-toast);
    pointer-events: none;
}

.tv-lazy-indicator-ring {
    width: 14px;
    height: 14px;
    flex: 0 0 14px;
    border-radius: 50%;
    border: 2px solid var(--accent);
    border-right-color: transparent;
    animation: tv-lazy-spin .8s linear infinite;
}

@keyframes tv-lazy-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .tv-lazy-indicator-ring { animation: none; }
}

.tv-lazy-error {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 9600;
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: min(360px, calc(100vw - 32px));
    padding: 14px 16px;
    border-radius: 12px;
    border: 1px solid var(--danger);
    background: var(--bg-menu);
    color: var(--text-primary);
    font-size: 13px;
    line-height: 1.35;
    box-shadow: var(--shadow-modal);
}

.tv-lazy-error-title { font-size: 14px; }
.tv-lazy-error-text { color: var(--text-dim); }
.tv-lazy-error-actions { display: flex; gap: 8px; margin-top: 2px; }

.tv-lazy-retry,
.tv-lazy-dismiss {
    padding: 6px 12px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.tv-lazy-retry {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--on-accent);
    font-weight: 600;
}

.tv-lazy-retry:focus-visible,
.tv-lazy-dismiss:focus-visible {
    outline: 2px solid var(--focus-outline);
    outline-offset: 2px;
}

@media (max-width: 520px) {
    .tv-lazy-indicator, .tv-lazy-error { right: 10px; bottom: 10px; }
}


/* Кнопка «Отправить» в композере: на телефоне Enter неочевиден. */
.send-button {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    margin: 0 4px 4px 0;
    border: 1px solid var(--btn-border);
    border-radius: var(--radius-md);
    background: var(--btn-bg);
    color: var(--btn-fg);
    cursor: pointer;
    transition: background var(--transition-base), color var(--transition-base);
}
.send-button:hover { background: var(--btn-bg-hover); border-color: var(--btn-border-hover); }
.send-button:focus-visible { outline: 2px solid var(--focus-outline); outline-offset: 2px; }
.send-button .ico { display: block; }
@media (max-width: 768px) {
    .send-button { width: 44px; height: 44px; margin: 0 6px 6px 0; }
}
