/*
 * Оболочка приложения — направление «Тёмный кабинет»
 * (docs/design/2026-07-27-shell-visual-direction.md).
 *
 * Суть направления — контраст материалов: тёмная панель навигации остаётся
 * тёмной и в светлой теме, справа от неё — светлый рабочий лист. Поэтому
 * цвета сайдбара живут в собственных токенах `--app-side-*` и не читаются
 * из `--bs-*`: общие токены переворачиваются вместе с темой, а сайдбар нет.
 *
 * Всё остальное по-прежнему идёт через токены Bootstrap 5.3 — мы их
 * переопределяем, но не обходим, иначе тёмная тема будет «дырявой» (11Г.2).
 * Готовый `bootstrap.min.css` не пересобирается: node и SCSS в проекте нет.
 *
 * Акцент — ровно одна переменная `--app-accent`. Когда заказчик назовёт
 * фирменный цвет (§11Д), меняться будет она одна, а не полфайла.
 */

/* ── Токены ─────────────────────────────────────────────────────── */

:root,
[data-bs-theme='light'] {
    /* Приглушённая латунь. Работает в двух местах: рельс активного пункта
       меню и главная кнопка — больше акцента интерфейсу не нужно */
    --app-accent: #b99b62;
    /* Латунь на белом даёт всего 2:1 — для текста ссылок она затемнена
       до 5.2:1 (WCAG AA для основного текста) */
    --app-accent-text: #8a6d2f;
    --app-accent-text-hover: #6f571f;

    --app-font-sans: 'Source Sans 3', system-ui, -apple-system, 'Segoe UI', sans-serif;
    --app-font-display: 'Montserrat', system-ui, -apple-system, 'Segoe UI', sans-serif;

    --app-sidebar-width: 262px;
    --app-topbar-height: 58px;
    --app-content-px: 30px;
    --app-content-py: 26px;

    /* Стрелка «откроется соседней вкладкой». Токен общий на обе темы: цвет
       берётся от текста ссылки, в самой картинке он не задан */
    --app-icon-external: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M10 14 21 3'/%3E%3C/svg%3E");

    /* Один радиус на контейнеры, другой на контролы — третьего нет */
    --app-radius: 10px;
    --app-radius-sm: 7px;
    /* Тень многослойная: ближний слой отделяет карточку от фона, дальний
       даёт ей вес. Одним слоем получается либо грязь, либо ничего */
    --app-shadow: 0 1px 2px rgba(24, 28, 34, 0.05), 0 8px 24px -12px rgba(24, 28, 34, 0.18);

    /* Сайдбар: тёмный при любой теме */
    --app-side-bg: #15181d;
    --app-side-fg: #a8aeb8;
    --app-side-muted: #7b828d;
    --app-side-icon: #767d88;
    --app-side-brand: #f2f1ee;
    --app-side-hover: rgba(255, 255, 255, 0.045);
    --app-side-active-bg: rgba(185, 155, 98, 0.13);
    --app-side-active-fg: #f0eadd;

    /* Главная кнопка: на светлом — чернильная, на тёмном — латунная */
    --app-btn-bg: #15181d;
    --app-btn-fg: #f2f1ee;
    --app-btn-hover-bg: #23272e;
    --app-btn-shadow: 0 1px 2px rgba(21, 24, 29, 0.16), 0 4px 12px rgba(21, 24, 29, 0.1);

    /* Три серых ниже темнее мокапных: там они давали 3.2–4.5:1 и не проходили
       WCAG AA для основного текста. Тон и насыщенность сохранены, изменена
       только светлота — на глаз разницы почти нет */
    --app-th-fg: #6f757e; /* было #8B9199 — 3.18:1 */
    --app-filter-bg: #fbfbfc;
    --app-ok-bg: #eaf2ed;
    --app-ok-fg: #35664d;
    --app-off-bg: #eff0f2;
    --app-off-fg: #666c75; /* было #6C737C — 4.20:1 */

    /* Переопределения Bootstrap. `body-bg` — это поверхность карточки, а не
       фон окна: так белыми остаются и карточки, и дропдауны, и поля ввода */
    --bs-body-bg: #ffffff;
    --bs-body-bg-rgb: 255, 255, 255;
    --bs-secondary-bg: #f1f2f4;
    --bs-secondary-bg-rgb: 241, 242, 244;
    --bs-tertiary-bg: #fbfbfc;
    --bs-tertiary-bg-rgb: 251, 251, 252;

    --bs-body-color: #1f2328;
    --bs-body-color-rgb: 31, 35, 40;
    --bs-emphasis-color: #0f1216;
    --bs-secondary-color: #686d76; /* было #6A7079 — на фоне окна 4.46:1 */
    --bs-secondary-color-rgb: 104, 109, 118;
    --bs-tertiary-color: #949aa2;
    --bs-heading-color: inherit;

    --bs-border-color: #e4e6e9;
    --bs-border-color-translucent: rgba(24, 28, 34, 0.1);

    --bs-link-color: var(--app-accent-text);
    --bs-link-color-rgb: 138, 109, 47;
    --bs-link-hover-color: var(--app-accent-text-hover);
    --bs-link-hover-color-rgb: 111, 87, 31;

    --bs-primary: var(--app-accent);
    --bs-primary-rgb: 185, 155, 98;
    --bs-focus-ring-color: rgba(185, 155, 98, 0.25);

    --bs-body-font-family: var(--app-font-sans);
    --bs-body-font-size: 0.9375rem;
    --bs-body-line-height: 1.5;

    --bs-border-radius: var(--app-radius-sm);
    --bs-border-radius-sm: 5px;
    --bs-border-radius-lg: var(--app-radius);
    --bs-border-radius-xl: var(--app-radius);
}

[data-bs-theme='dark'] {
    --app-accent-text: #cdaf77;
    --app-accent-text-hover: #ddc79b;

    --app-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);

    --app-side-bg: #0c0e12;

    --app-btn-bg: var(--app-accent);
    --app-btn-fg: #14171b;
    --app-btn-hover-bg: #c8ac76;
    --app-btn-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);

    --app-th-fg: #838b95;
    --app-filter-bg: #15191e;
    --app-ok-bg: #17301f;
    --app-ok-fg: #8cc49e;
    --app-off-bg: #22272d;
    --app-off-fg: #8d959f;

    --bs-body-bg: #181c21;
    --bs-body-bg-rgb: 24, 28, 33;
    --bs-secondary-bg: #101317;
    --bs-secondary-bg-rgb: 16, 19, 23;
    --bs-tertiary-bg: #15191e;
    --bs-tertiary-bg-rgb: 21, 25, 30;

    --bs-body-color: #e6e8eb;
    --bs-body-color-rgb: 230, 232, 235;
    --bs-emphasis-color: #f4f5f7;
    --bs-secondary-color: #929aa4;
    --bs-secondary-color-rgb: 146, 154, 164;
    --bs-tertiary-color: #6e767f;

    --bs-border-color: #252a30;
    --bs-border-color-translucent: rgba(255, 255, 255, 0.1);

    --bs-link-color-rgb: 205, 175, 119;
    --bs-link-hover-color-rgb: 221, 199, 155;
}

/* ── Контрастный режим ──────────────────────────────────────────────
 *
 * Действует поверх обеих тем, а не вместо них: тумблер светлая/тёмная
 * остаётся, меняется только глубина контраста. Обычные цвета проходят
 * WCAG AA (4.5:1) — это порог сломанности, а не комфорт; здесь всё
 * текстовое поднято до AAA (7:1), границы — до 3:1.
 *
 * Значения посчитаны, не подобраны: цифра в комментарии — фактический
 * коэффициент против фона, на котором цвет реально лежит.
 */

[data-contrast='high'] {
    /* Ссылочная латунь темнее: 5.2:1 → 7.57:1 на белом */
    --app-accent-text: #6a5017;
    --app-accent-text-hover: #4d3a0f;

    /* Тень заменяется рамкой карточки: при слабом зрении тень не читается */
    --app-shadow: none;

    /* Сайдбар тёмный при любой теме и из --bs-* не читается — иначе
       контрастный режим обошёл бы навигацию стороной */
    --app-side-fg: #eef0f3; /* 15.58:1 */
    --app-side-muted: #c3c9d1; /* 10.67:1 — было 4.9:1 */
    --app-side-icon: #ced4db; /* 11.92:1 */
    --app-side-brand: #ffffff;
    --app-side-hover: rgba(255, 255, 255, 0.12);
    --app-side-active-bg: rgba(185, 155, 98, 0.3);
    --app-side-active-fg: #ffffff;

    --app-th-fg: #2b3036; /* 13.30:1 — было 4.6:1 */
    /* Панель фильтров на белом была неотличима от карточки */
    --app-filter-bg: #f1f2f4;
    --app-ok-bg: #dcecdf;
    --app-ok-fg: #17452c; /* 8.89:1 на своей плашке */
    --app-off-bg: #e4e6e9;
    --app-off-fg: #3f444b; /* 7.85:1 на своей плашке */

    --bs-body-color: #000000; /* 21:1 */
    --bs-body-color-rgb: 0, 0, 0;
    --bs-emphasis-color: #000000;
    --bs-secondary-color: #3f444b; /* 9.81:1 — было 4.46:1 */
    --bs-secondary-color-rgb: 63, 68, 75;
    --bs-tertiary-color: #4d525a; /* 7.86:1 */

    --bs-border-color: #767c85; /* 4.21:1 — было 1.2:1, границы не было видно */
    --bs-border-color-translucent: rgba(0, 0, 0, 0.45);

    --bs-link-color-rgb: 106, 80, 23;
    --bs-link-hover-color-rgb: 77, 58, 15;

    /* Клавиатурная навигация не должна угадываться */
    --bs-focus-ring-color: rgba(185, 155, 98, 0.65);
}

[data-contrast='high'][data-bs-theme='dark'] {
    --app-accent-text: #e0c58d; /* 11.39:1 на карточке */
    --app-accent-text-hover: #efdcb6; /* 14.15:1 */

    --app-side-bg: #000000;

    /* Латунь под чёрным текстом даёт 7.93:1, под прежним #14171b было 6.79 */
    --app-btn-fg: #000000;

    --app-th-fg: #c2c8d0; /* 11.32:1 */
    --app-filter-bg: #1a1f25;
    --app-ok-bg: #0f2a19;
    --app-ok-fg: #a8d9b8; /* 9.73:1 на своей плашке */
    --app-off-bg: #2a2f36;
    --app-off-fg: #c2c8d0; /* 8.00:1 на своей плашке */

    /* Поверхность карточки и фон окна расходятся сильнее: окно уходит
       в чистый чёрный, карточка остаётся почти чёрной */
    --bs-body-bg: #0d1014;
    --bs-body-bg-rgb: 13, 16, 20;
    --bs-secondary-bg: #000000;
    --bs-secondary-bg-rgb: 0, 0, 0;
    --bs-tertiary-bg: #1a1f25;
    --bs-tertiary-bg-rgb: 26, 31, 37;

    --bs-body-color: #ffffff; /* 19.07:1 на карточке */
    --bs-body-color-rgb: 255, 255, 255;
    --bs-emphasis-color: #ffffff;
    --bs-secondary-color: #c2c8d0; /* 11.32:1 — было 5.9:1 */
    --bs-secondary-color-rgb: 194, 200, 208;
    --bs-tertiary-color: #a3aab3; /* 8.13:1 */

    --bs-border-color: #6d757e; /* 4.08:1 */
    --bs-border-color-translucent: rgba(255, 255, 255, 0.5);

    --bs-link-color-rgb: 224, 197, 141;
    --bs-link-hover-color-rgb: 239, 220, 182;
}

/* Кольцо фокуса шире: 0.2rem по краю контрола заметно не всем. Ширина
   задаётся здесь же, где и в обычном режиме, — правила фокуса написаны
   руками через box-shadow и переменной `--bs-focus-ring-width` не читают */
[data-contrast='high'] .form-control:focus,
[data-contrast='high'] .form-select:focus,
[data-contrast='high'] .form-check-input:focus {
    box-shadow: 0 0 0 0.3rem var(--bs-focus-ring-color);
}

/* Содержательные ссылки подчёркиваются всегда: цвет не должен быть
   единственным признаком ссылки. Блочные плитки (.app-metric,
   .app-task-link) — исключение: это карточки, а не текст в строке */
[data-contrast='high'] .app-content a:not(.btn):not(.nav-link):not(.app-metric):not(.app-task-link) {
    text-decoration: underline;
}

/* ── Типографика ────────────────────────────────────────────────── */

body {
    min-height: 100vh;
    font-family: var(--app-font-sans);
    background-color: var(--bs-secondary-bg);
    -webkit-font-smoothing: antialiased;
}

/* Montserrat берёт на себя только заголовки страниц и название системы:
   в тексте и таблицах она читается хуже гуманистической гарнитуры */
.app-page-header h1,
.app-brand,
.app-display {
    font-family: var(--app-font-display);
}

/* ── Иконки ─────────────────────────────────────────────────────── */

.app-icon {
    width: 1em;
    height: 1em;
    flex-shrink: 0;
    /* Штриховая иконка рядом с текстом всегда чуть «всплывает» — сдвиг
       возвращает её на оптическую середину строки */
    vertical-align: -0.135em;
}

/* Ссылка, открывающая соседнюю вкладку, помечается стрелкой — иначе она
   ничем не отличается от кнопки, которая достраивает текущую форму.
   Правило висит на самом `target="_blank"`, а не на классе: пометку нельзя
   забыть проставить, она появляется вместе с открытием новой вкладки.
   Исключения — ссылки-картинки и ссылки, свёрстанные кнопками: там стрелка
   встала бы поверх содержимого.
   Стрелка нарисована маской, а не спрайтом: ::after не умеет <use>, а цвет
   маски наследуется от текста ссылки в обеих темах */
a[target='_blank']:not(.btn):not(.app-gallery-item)::after {
    content: '';
    display: inline-block;
    width: 0.8em;
    height: 0.8em;
    margin-left: 0.25em;
    vertical-align: -0.02em;
    background-color: currentColor;
    -webkit-mask: var(--app-icon-external) center / contain no-repeat;
    mask: var(--app-icon-external) center / contain no-repeat;
}

/* ── Оболочка ───────────────────────────────────────────────────── */

.app-main {
    min-width: 0;
}

/* ── Сайдбар ────────────────────────────────────────────────────── */

.app-sidebar {
    width: var(--app-sidebar-width);
    padding: 22px 16px;
    /* `!important` тут вынужденный: выше breakpoint'а Bootstrap гасит фон
       `.offcanvas-lg` своим `background-color: transparent !important`, и без
       этого панель на десктопе оставалась бы светлой */
    background-color: var(--app-side-bg) !important;
    border-right: 0;
    /* На узком экране это off-canvas Bootstrap — ему нужен свой фон,
       иначе панель выедет светлой */
    --bs-offcanvas-bg: var(--app-side-bg);
    --bs-offcanvas-color: var(--app-side-fg);
    --bs-offcanvas-width: var(--app-sidebar-width);
    --bs-offcanvas-border-width: 0;
}

/* На десктопе сайдбар — колонка во всю высоту окна, off-canvas
   отключается (11Д.3) */
@media (min-width: 992px) {
    .app-sidebar.offcanvas-lg {
        position: sticky;
        top: 0;
        height: 100vh;
        overflow-y: auto;
        visibility: visible !important;
        transform: none !important;
    }
}

.app-sidebar-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 24px;
}

.app-brand {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 4px 8px 0;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.055em;
    color: var(--app-side-brand);
    text-decoration: none;
}

.app-brand:hover {
    color: var(--app-side-brand);
}

.app-sidebar .nav {
    gap: 2px;
}

.app-sidebar .nav-link {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 12px;
    font-size: 14.5px;
    font-weight: 400;
    color: var(--app-side-fg);
    border-radius: var(--app-radius-sm);
}

.app-sidebar .nav-link .app-icon {
    width: 17px;
    height: 17px;
    color: var(--app-side-icon);
    vertical-align: 0;
}

.app-sidebar .nav-link:hover,
.app-sidebar .nav-link:focus-visible {
    color: var(--app-side-brand);
    background-color: var(--app-side-hover);
}

.app-sidebar .nav-link.active {
    background-color: var(--app-side-active-bg);
    color: var(--app-side-active-fg);
    font-weight: 600;
}

.app-sidebar .nav-link.active .app-icon {
    color: var(--app-side-active-fg);
}

/* Латунный рельс слева — единственное место, где акцент обозначает
   состояние. Подложки одной было бы мало: она почти не видна */
.app-sidebar .nav-link.active::before {
    content: '';
    position: absolute;
    left: 0;
    top: 6px;
    bottom: 6px;
    width: 2px;
    border-radius: 2px;
    background-color: var(--app-accent);
}

/* Подпункты аккордеона (второй уровень навигации, 11Д.1) */
.app-sidebar .nav-sub .nav-link {
    padding-left: 42px;
    font-size: 14px;
    color: var(--app-side-muted);
}

.app-sidebar .nav-sub .nav-link.active {
    color: var(--app-side-active-fg);
}

.app-sidebar hr {
    border-color: rgba(255, 255, 255, 0.08);
    opacity: 1;
}

.app-sidebar .accordion-caret {
    width: 14px;
    height: 14px;
    margin-left: auto;
    color: var(--app-side-muted);
    transition: transform 0.15s ease-in-out;
}

.app-sidebar [aria-expanded='true'] .accordion-caret {
    transform: rotate(90deg);
}

.app-sidebar .btn-close {
    filter: var(--bs-btn-close-white-filter, invert(1) grayscale(100%) brightness(200%));
}

/* ── Top-плашка ─────────────────────────────────────────────────── */

.app-topbar {
    height: var(--app-topbar-height);
    padding: 0 var(--app-content-px);
    background-color: var(--bs-body-bg);
    border-bottom: 1px solid var(--bs-border-color);
    position: sticky;
    top: 0;
    z-index: 1020;
}

/* Кнопки плашки — одна высота и одна геометрия, включая те, что открывают
   дропдаун: разнобой в 2px в ряду из четырёх контролов очень заметен */
.app-topbar .btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 32px;
    padding: 0 11px;
    font-size: 14.5px;
}

.app-topbar .btn-icon {
    width: 32px;
    padding: 0;
    justify-content: center;
}

.app-topbar .btn .app-icon {
    width: 16px;
    height: 16px;
    vertical-align: 0;
}

/* Иконка в обёртке (переключатель темы прячет неактивную через `d-none`)
   иначе остаётся инлайновой и садится на базовую линию — а она выше середины
   строки, и иконка уезжает вверх на треть высоты. Обёртка тоже должна быть
   flex-контейнером, тогда центрируется она сама, а не её базовая линия */
.app-topbar .btn > span {
    display: inline-flex;
    align-items: center;
}

.app-topbar .btn .app-icon.caret {
    width: 12px;
    height: 12px;
    opacity: 0.65;
}

/* Стрелка у дропдауна нарисована иконкой — треугольник Bootstrap лишний */
.app-topbar .dropdown-toggle::after {
    display: none;
}

/* ── Контент ────────────────────────────────────────────────────── */

.app-content {
    min-width: 0; /* иначе широкие таблицы растянут grid-колонку */
    max-width: 1600px;
    padding: var(--app-content-py) var(--app-content-px);
}

/* Индикатор загрузки Turbo Frame */
turbo-frame[busy] {
    opacity: 0.6;
    transition: opacity 0.15s ease-in-out;
}

/* Ссылки в таблицах и карточках без подчёркивания: строк много, и рябь
   подчёркиваний утомляет */
.app-content a:not(.btn) {
    text-decoration: none;
}

.app-content a:not(.btn):hover {
    text-decoration: underline;
}

/* ── Заголовок страницы ─────────────────────────────────────────── */

.app-page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 22px;
}

.app-page-header h1 {
    margin: 0;
    font-size: 21px;
    font-weight: 600;
    letter-spacing: 0.005em;
    line-height: 1.25;
}

.app-page-subtitle {
    margin: 4px 0 0;
    font-size: 14.5px;
    color: var(--bs-secondary-color);
}

/* ── Карточка — основной контейнер контента ─────────────────────── */

.app-card {
    background-color: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--app-radius);
    box-shadow: var(--app-shadow);
    /* Радиус карточки обрезает таблицу и панель фильтров: у них своих
       скруглений нет, и без этого углы вылезали бы поверх рамки */
    overflow: hidden;
}

.app-card + .app-card {
    margin-top: 1rem;
}

.app-card-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 14px 20px;
    border-bottom: 1px solid var(--bs-border-color);
}

.app-card-header h2 {
    margin: 0;
    font-size: 15px;
    font-weight: 600;
}

.app-card-body {
    padding: 20px;
}

.app-card-body > :last-child {
    margin-bottom: 0;
}

/* ── Таблицы ────────────────────────────────────────────────────── */

/* Таблица занимает карточку целиком — рамка карточки и есть её рамка */
.app-card > .table-responsive > .table,
.app-card > .table {
    margin-bottom: 0;
}

.app-card .table {
    --bs-table-bg: transparent;
    --bs-table-color: var(--bs-body-color);
    --bs-table-border-color: var(--bs-border-color);
    --bs-table-hover-bg: var(--bs-tertiary-bg);
    --bs-table-hover-color: var(--bs-body-color);
}

.app-card .table > thead > tr > th {
    padding: 11px 20px;
    background-color: transparent;
    border-bottom: 1px solid var(--bs-border-color);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.085em;
    color: var(--app-th-fg);
}

.app-card .table > tbody > tr > td {
    padding: 13px 20px;
    font-size: 14.5px;
    vertical-align: middle;
}

/* Последняя строка не рисует линию: её роль играет край карточки */
.app-card .table > tbody > tr:last-child > td {
    border-bottom-width: 0;
}

/* ── Панель фильтров ────────────────────────────────────────────── */

/* Визуально спокойнее таблицы, но заметнее карточки */
.app-filters {
    padding: 12px 20px;
    background-color: var(--app-filter-bg);
    border-bottom: 1px solid var(--bs-border-color);
}

.app-filters .form-control,
.app-filters .form-select,
.app-filters .btn {
    height: 32px;
    font-size: 14.5px;
}

/* Поле поиска с иконкой внутри: отдельного input-group не заводим — он
   ломает высоту ряда и добавляет вторую рамку */
.app-search {
    position: relative;
}

.app-search .app-icon {
    position: absolute;
    left: 10px;
    top: 50%;
    width: 14px;
    height: 14px;
    margin-top: -7px;
    color: var(--bs-tertiary-color);
    pointer-events: none;
}

.app-search .form-control {
    padding-left: 31px;
}

/* ── Формы ──────────────────────────────────────────────────────── */

.app-form-panel {
    border-top: 1px solid var(--bs-border-color);
    padding: 20px;
    background-color: var(--app-filter-bg);
}

/* Звёздочка у обязательных полей. Класс `required` на подписи ставит сам
   form_label — метка появляется у каждого нового обязательного поля без
   правки шаблонов и не разъезжается с реальными констрейнтами.
   Символ рисуется в ::after, а не в подписи: в каталог переводов он бы попал
   частью текста и терялся при переводе */
.form-label.required::after,
.form-check-label.required::after,
.app-form-legend.required::after {
    content: ' *';
    color: var(--bs-danger);
}

/* Фокус в Bootstrap скомпилирован синим — цвет фокуса переопределяем
   вручную, к переменным он не сведён */
.form-control:focus,
.form-select:focus {
    border-color: var(--app-accent);
    box-shadow: 0 0 0 0.2rem var(--bs-focus-ring-color);
}

.form-check-input:checked {
    background-color: var(--app-accent);
    border-color: var(--app-accent);
}

.form-check-input:focus {
    border-color: var(--app-accent);
    box-shadow: 0 0 0 0.2rem var(--bs-focus-ring-color);
}

/* Ветка полей внутри формы: партнёрская трата, компенсация, реквизиты.
   Рамка нужна, чтобы длинная форма читалась блоками, а не сплошным полем */
.app-form-branch {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    padding: 12px 16px 16px;
}

.app-form-legend {
    float: none;
    width: auto;
    padding: 0 6px;
    margin-bottom: 0;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

/* Приветственное окно заявки: выбор типа плитками (макет image2) */
.app-choice {
    color: inherit;
    transition: border-color 0.15s ease, transform 0.15s ease;
}

.app-choice:hover {
    border-color: var(--app-accent);
    transform: translateY(-1px);
}

.app-choice-title {
    font-weight: 600;
}

/* Шаги согласования в карточке заявки */
.app-steps {
    padding-left: 18px;
    margin-bottom: 0;
}

.app-steps li + li {
    margin-top: 6px;
}

/* Зона перетаскивания файлов в форме заявки */
.app-dropzone {
    border: 1px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    padding: 18px;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

.app-dropzone.is-dragover {
    border-color: var(--app-accent);
    background-color: var(--app-filter-bg);
}

.app-files,
.app-thread {
    list-style: none;
    padding-left: 0;
    margin-bottom: 0;
}

/* Галерея вложений: чек глазами узнают быстрее, чем по имени файла.
   Миниатюра — та же картинка, ужатая рамкой: сервер их не режет, потому что
   обработчика изображений в образе нет, а чеки весят килобайты. Появятся
   мегабайтные фото — здесь добавится настоящий превью-файл, разметка не
   изменится */
.app-gallery {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.app-gallery-item {
    display: block;
    width: 120px;
    height: 120px;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    overflow: hidden;
    background-color: var(--bs-tertiary-bg);
}

.app-gallery-item:hover {
    border-color: var(--app-accent);
}

.app-gallery-item img {
    width: 100%;
    height: 100%;
    /* Обрезка, а не растяжение: пропорции чека важнее заполненной плитки */
    object-fit: cover;
    display: block;
}

.app-files li + li,
.app-thread-item + .app-thread-item {
    margin-top: 10px;
}

/* Внутренняя заметка видна только финконтролю — и должна отличаться взглядом,
   а не только подписью: перепутать её с сообщением автору дорого */
.app-thread-item {
    border-left: 2px solid var(--bs-border-color);
    padding-left: 10px;
}

.app-thread-internal {
    border-left-color: var(--bs-warning);
}

/* Непрочитанное уведомление — свой знак, а не «внутренняя заметка»: жёлтая
   полоса у ленты уведомлений означала бы то же, что у комментариев, то есть
   «видно только финконтролю», а это неправда */
.app-thread-unread {
    border-left-color: var(--bs-primary);
    font-weight: 500;
}

.app-thread-system {
    border-left-style: dashed;
}

.app-thread-meta {
    margin-bottom: 2px;
}

/* Панель быстрого ввода внутри формы */
.app-inline-panel {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background-color: var(--app-filter-bg);
    padding: 12px;
}

/* Строка табличного ввода: «группа чеков» вводится подряд, поэтому строки
   должны читаться как строки, а не как отдельные формы */
.app-collection-row {
    border-bottom: 1px solid var(--bs-border-color);
    padding-bottom: 8px;
    margin-bottom: 8px;
}

.app-collection-row:last-child {
    border-bottom: 0;
}

/* Карточка расхода: поля идут в ряд — так строка читается как строка
   таблицы, а не как отдельная форма (`.app-lines` в `expense/show.html.twig` —
   чеки расхода и списания без заказа на этой же карточке).

   Разнесение заявки по статьям этим приёмом раньше тоже пользовалось, но
   теперь заказы и их списания идут одной настоящей `<table>` со своей
   раскладкой (`.app-orders`) — класс `.app-lines` там больше не стоит.

   ⚠️ Сетка вешается на **вложенный** div, а не на саму `.app-collection-row`:
   `form_widget()` составной формы заворачивает поля строки в один контейнер, и
   на строке грид получил бы ровно один элемент — отчего поля и вставали
   столбиком. Сама строка при этом flex: рядом с полями живёт кнопка удаления.

   Чеки компенсации этим приёмом больше не пользуются: там настоящая `<table>`
   с `<thead>`, и подпись столбца стоит один раз, а не в каждой строке. */
.app-lines .app-collection-row {
    display: flex;
    align-items: flex-end;
    gap: 12px;
}

.app-lines .app-collection-row > div:first-child {
    flex: 1 1 auto;
    min-width: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 0 12px;
    align-items: end;
}

/* Файлы строки чека: во всю ширину строки, а не одной колонкой сетки.
   `minmax(150px, 1fr)` дал бы дропзоне колонку в полтора сантиметра, в которую
   не попасть ни перетаскиванием, ни кликом. В формах без сетки (заведение и
   правка расхода автором) правило ничего не меняет: `grid-column` действует
   только внутри грида. */
.app-lines .app-line-files,
.app-line-files {
    grid-column: 1 / -1;
}

/* Дропзона внутри строки — тише и ниже общей: она повторяется на каждом чеке,
   и восемнадцать пикселей отступа превращали таблицу из десяти строк в
   бесконечную ленту */
.app-dropzone-inline {
    padding: 8px;
}

.app-collection-remove {
    padding-left: 0;
    padding-right: 0;
    white-space: nowrap;
}

/* Таблица чеков компенсации. Поля внутри ячеек не носят собственных отступов:
   расстояние между строками задаёт сама таблица, а `mb-3` из темы формы
   раздувал бы строку вдвое */
.app-receipts td .mb-3,
.app-receipts td .form-check {
    margin-bottom: 0;
}

/* Ширины столбцов: валюта показывает код целиком («RUB», а не «RU…»), сумма
   узкая, назначение забирает остаток. Без этого браузер делит ширину по
   содержимому шапки, и «Назначение» получает столько же, сколько код валюты */
.app-receipts th:nth-child(2),
.app-receipts td:nth-child(2) {
    /* `min-width`, а не `width`: ширину ячейки браузер считает подсказкой и
       ужимает её под соседей — код валюты от этого превращался в «RU…» */
    /* Полторы ширины кода: у `.form-select` справа ещё стрелка на 2.25rem,
       и без запаса «RUB» обрезается до «RU» */
    min-width: 7.5rem;
}

.app-receipts th:nth-child(3),
.app-receipts td:nth-child(3) {
    min-width: 7.5rem;
}

/* Поле файла показывает две подписи — кнопку и «файл не выбран» */
.app-receipts th:nth-child(4),
.app-receipts td:nth-child(4) {
    min-width: 13rem;
}

.app-receipts th:nth-child(5),
.app-receipts td:nth-child(5) {
    min-width: 12rem;
}

.app-receipts th:nth-child(6),
.app-receipts td:nth-child(6) {
    width: 6rem;
}

/* Подпись флажка прячется от глаз, но остаётся для экранного диктора: её роль
   в таблице играет заголовок столбца, а голый чекбокс без подписи диктор
   называет никак. Приём тот же, что у `.visually-hidden` в bootstrap */
.app-receipts td .form-check-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.app-receipts td .form-check {
    display: flex;
    justify-content: center;
    padding-left: 0;
}

.app-receipts td .form-check-input {
    margin-left: 0;
    float: none;
}

/* Чеки расхода в форме автора — та же таблица, что у компенсации, но со своим
   составом столбцов: дата, сумма, валюта, назначение, период, файлы. Ширины —
   `min-width` по той же причине, что выше: `width` браузер ужимает под
   соседей, и дата превращалась в «дд.мм.г…». Назначение забирает остаток */
/* Таблица ввода, а не списка: отступы списочных таблиц карточки (двадцать
   пикселей с боков) здесь съедали ширину полей — `min-width` ячейки считается
   вместе с ними, и код валюты обрезался до «RU» */
.app-card .table.app-expense-lines > thead > tr > th,
.app-card .table.app-expense-lines > tbody > tr > td {
    padding: 8px 6px;
}

.app-card .table.app-expense-lines > thead > tr > th:first-child,
.app-card .table.app-expense-lines > tbody > tr > td:first-child {
    padding-left: 0;
}

.app-expense-lines th:nth-child(1),
.app-expense-lines td:nth-child(1) {
    min-width: 9.5rem;
}

.app-expense-lines th:nth-child(2),
.app-expense-lines td:nth-child(2) {
    min-width: 7rem;
}

.app-expense-lines th:nth-child(3),
.app-expense-lines td:nth-child(3) {
    min-width: 6.5rem;
}

.app-expense-lines th:nth-child(4),
.app-expense-lines td:nth-child(4) {
    min-width: 9rem;
}

/* Период — две даты в одной ячейке, одна под другой. Рядом они забирали
   девятнадцать рем, и на экране ноутбука столбцы файлов и удаления уезжали за
   край таблицы — а файл чека и есть главный сценарий формы */
.app-expense-lines th:nth-child(5),
.app-expense-lines td:nth-child(5) {
    min-width: 9.5rem;
}

/* `.app-line-pair` — та же столбиковая пара, что и период (задача 2 волны
   «Один блок „Заказы“»): статья/подстатья строки разнесения делят одну ячейку
   тем же приёмом */
.app-line-period,
.app-line-pair {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

/* Подпись подписанного поля (`_shell/_labeled_field.html.twig`, замечание
   финансиста 15.09.2026): мелкая и приглушённая — она объясняет поле, а не
   спорит с подписью столбца над ним. Над полем, а не слева: слева она
   забирала бы ширину самого узкого столбца таблицы. Заводился приём под
   период (дата «с» / дата «по»), обобщён на кассу и хеш заказа (круг правок 1
   задачи 11 волны «Финконтроль на заказе») — та же ошибка чтения нашлась в
   соседней ячейке */
.app-field-label {
    display: block;
    margin-bottom: 0.125rem;
    font-size: 0.75rem;
    line-height: 1.2;
    color: var(--bs-secondary-color);
}

.app-expense-lines th:nth-child(6),
.app-expense-lines td:nth-child(6) {
    min-width: 10rem;
}

.app-expense-lines .app-dropzone-inline {
    padding: 4px 8px;
}

/* Уже приложенные файлы строки — миниатюрами по высоте поля: полноразмерные
   плитки галереи раздували бы строку таблицы на сто двадцать пикселей */
.app-expense-lines .app-gallery {
    gap: 4px;
}

.app-expense-lines .app-gallery-item {
    width: 36px;
    height: 36px;
}

/* Блок «Заказы» — одна таблица на оба уровня (задача 6 волны «Заказы и
   списания одной таблицей», 14.09.2026): строка заказа родительская, под ней
   подстроки его списаний. Отступы таблицы ввода, а не списочной.

   Фиксированной раскладки здесь нет намеренно (доработка 1 финального ревью
   волны 11.09.2026): `table-layout: fixed` с долями в `%` выравнивал столбцы,
   но ширина считалась от таблицы, а не от содержимого, и то, что не
   помещалось, **обрезалось**, а не переносилось — на 1366 px «Удалить
   списание» становилось «Удалить сп», номер «WO-TASK2-DEM» — «WO-TASK2-DEI»,
   дата «05.09.2026» — «05.09.202». Авто-раскладка по `min-width` ниже этого не
   режет: браузер уступает контенту, а не наоборот. */
.app-card .table.app-orders > thead > tr > th,
.app-card .table.app-orders > tbody > tr > td {
    padding: 8px 6px;
}

.app-card .table.app-orders > thead > tr > th:first-child,
.app-card .table.app-orders > tbody > tr > td:first-child {
    padding-left: 0;
}

/* Столбец «ID заявки / списания» — общий на оба уровня: у заказа в нём номер
   заказа и под ним подписанная пара «касса / хеш» (`.app-line-pair` +
   `.app-field-label`, задача 11 волны «Финконтроль на заказе»), у подстроки —
   номер списания и иконка журнала сверки. Ширина шире соседних текстовых
   полей: под номером ещё и селект кассы — имя кассы не должно обрезаться */
.app-orders th:nth-child(1),
.app-orders td:nth-child(1) {
    min-width: 12rem;
}

.app-orders th:nth-child(2),
.app-orders td:nth-child(2) {
    min-width: 7rem;
}

/* Статья и подстатья — одной ячейкой столбиком (`.app-line-pair`): семь
   столбцов с двумя длинными селектами рядом шире карточки на экране 1366 */
.app-orders th:nth-child(3),
.app-orders td:nth-child(3) {
    min-width: 14rem;
}

/* Столбец периода: у заказа три подписанных поля столбиком (период с,
   период по, дата проведения — задача 11 волны «Финконтроль на заказе»), у
   списания одна дата проведения без подписи */
.app-orders th:nth-child(4),
.app-orders td:nth-child(4) {
    min-width: 9.5rem;
}

/* Соседние заказы — одной таблицей, и граница между группами нужна, чтобы они
   не читались одной кашей (раньше эту роль играл `.app-order + .app-order`) */
.app-orders > tbody.app-order-group + tbody.app-order-group > tr:first-child > td {
    border-top: 1px solid var(--bs-border-color);
}

/* Уровень читается фоном: строка заказа темнее, подстроки светлее — как в
   утверждённом макете (§3.1 спеки) */
.app-card .table.app-orders > tbody.app-order-group > tr.app-order-row > td {
    background-color: var(--app-filter-bg);
}

/* Отступ и линия-связка у подстрок: «+ списание» стоит в том же отступе
   последней строкой группы, поэтому правило общее для обеих */
.app-card .table.app-orders > tbody.app-order-group > tr.app-writeoff-row > td:first-child,
.app-card .table.app-orders > tbody.app-order-group > tr.app-order-actions > td:first-child {
    position: relative;
    padding-left: 2rem;
}

.app-card .table.app-orders > tbody.app-order-group > tr.app-writeoff-row > td:first-child::before,
.app-card .table.app-orders > tbody.app-order-group > tr.app-order-actions > td:first-child::before {
    content: '';
    position: absolute;
    inset-block: 0;
    left: .75rem;
    border-left: 1px solid var(--bs-border-color);
}

/* Номер расхода и подписи кнопок — одной строкой: «ID-000203» переносился
   по дефису на две, а «Удалить списание» — по пробелу, и строка таблицы
   вырастала вдвое */
.app-orders td:nth-child(6) a,
.app-orders .app-collection-actions .btn {
    white-space: nowrap;
}

/* Метки расхождения и «по кассе не проведено» — переносятся: у bootstrap
   `.badge` в одну строку, и «Статья списания отличается от статьи заказа»
   раздвигала столбец состояния так, что столбец расхода уезжал за край */
.app-orders .badge {
    white-space: normal;
    text-align: start;
}

/* Поле поиска расхода на привязку (§5 спеки 2026-09-14, задача 7 волны
   «Заказы и списания одной таблицей»): разворачивается под ссылкой
   «Привязать расход», не шире одной колонки ввода — список результатов может
   вырасти на 20 кнопок, и без ограничения ширины они растянули бы строку */
.app-expense-search {
    max-width: 28rem;
    margin-top: .5rem;
}

.app-expense-search-results {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    margin-top: .5rem;
}

/* Ссылка в журнал прогонов сверки — иконкой рядом с полем номера: текст
   «Прогоны сверки» в каждой строке съел бы ячейку. Подпись — `aria-label` и
   `title` ключом. Стрелка та же, что у ссылок в соседнюю вкладку, и
   нарисована тем же приёмом — маской, цвет от текста ссылки */
.app-writeoff-history {
    flex: none;
    display: inline-block;
    width: 1rem;
    height: 1rem;
    background-color: currentColor;
    -webkit-mask: var(--app-icon-external) center / contain no-repeat;
    mask: var(--app-icon-external) center / contain no-repeat;
}

/* Счётчик непрочитанных на колокольчике. Сидит на углу кнопки-иконки:
   отдельной строкой рядом он растянул бы плашку, а плашка одна на все экраны.

   ⚠️ Селектор с `.btn` не для красоты: у bootstrap есть `.btn .badge
   { position: relative }` — специфичность (0,2,0) против (0,1,0) у одного
   класса, и значок вставал бы в строку рядом с иконкой, растягивая кнопку. */
.btn .app-bell-badge {
    position: absolute;
    top: -6px;
    right: -6px;
    /* Кнопка делает содержимое flex-контейнером, и `text-align` значка не
       касается: раскладку flex-элементов решает `justify-content`, а он по
       умолчанию прижимает их к началу — однозначная цифра вставала левее
       центра. Центрируем обеими осями, а не текстом */
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 1.15rem;
    padding: .15rem .3rem;
    font-size: .6875rem;
    line-height: 1;
    color: #fff;
    /* Красный, а не акцент: акцент в оболочке работает ровно в двух местах
       (рельс меню и главная кнопка), и третье размывало бы его смысл */
    background: var(--bs-danger);
}

/* ── Кнопки ─────────────────────────────────────────────────────── */

/* Главная кнопка — второе и последнее место, где работает акцент */
.btn-primary {
    --bs-btn-bg: var(--app-btn-bg);
    --bs-btn-border-color: var(--app-btn-bg);
    --bs-btn-color: var(--app-btn-fg);
    --bs-btn-hover-bg: var(--app-btn-hover-bg);
    --bs-btn-hover-border-color: var(--app-btn-hover-bg);
    --bs-btn-hover-color: var(--app-btn-fg);
    --bs-btn-active-bg: var(--app-btn-hover-bg);
    --bs-btn-active-border-color: var(--app-btn-hover-bg);
    --bs-btn-active-color: var(--app-btn-fg);
    --bs-btn-disabled-bg: var(--app-btn-bg);
    --bs-btn-disabled-border-color: var(--app-btn-bg);
    --bs-btn-disabled-color: var(--app-btn-fg);
    box-shadow: var(--app-btn-shadow);
    font-weight: 600;
}

.btn-outline-secondary {
    --bs-btn-color: var(--bs-body-color);
    --bs-btn-border-color: var(--bs-border-color);
    --bs-btn-bg: var(--bs-body-bg);
    --bs-btn-hover-color: var(--bs-body-color);
    --bs-btn-hover-bg: var(--bs-tertiary-bg);
    --bs-btn-hover-border-color: var(--bs-border-color);
    --bs-btn-active-color: var(--bs-body-color);
    --bs-btn-active-bg: var(--bs-secondary-bg);
    --bs-btn-active-border-color: var(--bs-border-color);
    --bs-btn-disabled-color: var(--bs-secondary-color);
    --bs-btn-disabled-border-color: var(--bs-border-color);
    font-weight: 500;
}

.btn {
    --bs-btn-focus-box-shadow: 0 0 0 0.2rem var(--bs-focus-ring-color);
}

/* Кнопка «с иконкой и подписью» — иконка не должна разъезжаться с текстом */
.btn .app-icon {
    width: 15px;
    height: 15px;
    margin-right: 7px;
    vertical-align: -0.15em;
}

.app-topbar .btn .app-icon,
.btn .app-icon:only-child {
    margin-right: 0;
}

/* ── Дропдауны ──────────────────────────────────────────────────── */

.dropdown-menu {
    --bs-dropdown-border-color: var(--bs-border-color);
    --bs-dropdown-border-radius: var(--app-radius-sm);
    --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);
    --bs-dropdown-link-active-bg: var(--bs-secondary-bg);
    --bs-dropdown-link-active-color: var(--bs-emphasis-color);
    --bs-dropdown-font-size: 14.5px;
    box-shadow: var(--app-shadow);
}

/* Касса, чья сверка не сошлась: строка метится целиком, а не одним бейджем.
   Список касс просматривают сверху вниз столбцом остатков — бейдж в первой
   колонке при этом остаётся сбоку от взгляда, и расхождение замечают, только
   если специально его ищут.

   Заливка берётся у Bootstrap (`--bs-danger-bg-subtle`), а не заводится своим
   токеном: у него уже есть значения под светлую и тёмную тему, а тут нужен
   ровно фон предупреждения, а не собственный оттенок.

   ⚠️ Красится переменной, а не `background-color`: ячейку таблицы Bootstrap
   заливает внутренней тенью поверх фона, и обычный фон под ней не виден.
   Приём тот же, что у собственного `.table-active`, и наведение поверх
   расхождения продолжает работать — оно ставит ту же переменную на ячейке. */
.app-cash-mismatch {
    --bs-table-bg-state: var(--bs-danger-bg-subtle);
}

/* ── Бейджи состояния ───────────────────────────────────────────── */

/* Мягкая заливка вместо плотной: в таблице десяток плотных бейджей
   перетягивает внимание на себя. `!important` — потому что классы
   `text-bg-*` в готовом bootstrap.min.css объявлены с ним же */
.badge {
    padding: 3px 9px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.03em;
}

.badge.text-bg-success {
    background-color: var(--app-ok-bg) !important;
    color: var(--app-ok-fg) !important;
}

.badge.text-bg-secondary {
    background-color: var(--app-off-bg) !important;
    color: var(--app-off-fg) !important;
}

/* Точка перед подписью: состояние читается раньше, чем прочитан текст */
.badge-status::before {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-right: 5px;
    vertical-align: 0.08em;
    border-radius: 50%;
    background-color: currentColor;
}

/* ── Светофор состояния (11Г.7) ─────────────────────────────────── */

.status-dot {
    display: inline-block;
    width: 0.625rem;
    height: 0.625rem;
    border-radius: 50%;
}

.status-dot-ok {
    background-color: var(--bs-success);
}

.status-dot-warn {
    background-color: var(--bs-warning);
}

.status-dot-fail {
    background-color: var(--bs-danger);
}

/* ── Дерево оргструктуры ────────────────────────────────────────── */

.org-tree,
.org-tree ul {
    list-style: none;
    margin: 0;
    padding-left: 0;
}

.org-tree ul {
    margin-left: 0.75rem;
    padding-left: 1rem;
    border-left: 1px solid var(--bs-border-color);
}

.org-tree li {
    padding: 0.125rem 0;
}

.org-node {
    display: inline-flex;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.3rem 0.6rem;
    border-radius: var(--app-radius-sm);
}

.org-node:hover {
    background-color: var(--bs-tertiary-bg);
}

.org-node-title {
    font-weight: 500;
}

.org-node-meta {
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
}

/* ── Карта оргструктуры ─────────────────────────────────────────── */

.org-map {
    position: relative;
}

/* Высота фиксированная: карту двигают внутри окна, а не растягивают
   страницу вниз на всё дерево */
.org-map-viewport {
    position: relative;
    overflow: hidden;
    height: min(70vh, 720px);
    background-color: var(--bs-body-bg);
    background-image: radial-gradient(var(--bs-border-color) 1px, transparent 1px);
    background-size: 24px 24px;
    cursor: grab;
    touch-action: none;
}

.org-map-viewport.is-dragging {
    cursor: grabbing;
}

.org-map-layer {
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
}

.org-map-edges {
    position: absolute;
    top: 0;
    left: 0;
    overflow: visible;
}

.org-map-edge {
    fill: none;
    stroke: var(--bs-border-color);
    stroke-width: 1.5;
}

.org-map-node {
    position: absolute;
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    width: 220px;
    min-height: 78px;
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--app-radius-sm);
    background-color: var(--bs-body-bg);
}

.org-map-node-name {
    font-weight: 500;
    line-height: 1.2;
}

.org-map-node-meta {
    font-size: 0.8125rem;
    line-height: 1.2;
    color: var(--bs-secondary-color);
}

.org-map-node-badge {
    align-self: flex-start;
    margin-top: 0.25rem;
    padding: 0.05rem 0.4rem;
    border-radius: var(--app-radius-sm);
    background-color: var(--bs-success-bg-subtle);
    color: var(--bs-success-text-emphasis);
    font-size: 0.75rem;
}

.org-map-toolbar {
    position: absolute;
    z-index: 2;
    top: 0.75rem;
    right: 0.75rem;
    display: flex;
    gap: 0.25rem;
}

.org-map-hint {
    margin: 0;
    padding: 0.5rem 0.75rem;
    border-top: 1px solid var(--bs-border-color);
    font-size: 0.8125rem;
}

/* Рабочее место финконтроля: Tabulator приходит со своим светлым оформлением,
   поэтому цвета переопределяются переменными Bootstrap — иначе в тёмной теме
   таблица светится белым прямоугольником */
.app-workbench .tabulator {
    background-color: transparent;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--app-radius-sm);
    font-size: 0.875rem;
}

.app-workbench .tabulator .tabulator-header,
.app-workbench .tabulator .tabulator-header .tabulator-col,
.app-workbench .tabulator .tabulator-footer {
    border-color: var(--bs-border-color);
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}

.app-workbench .tabulator-row,
.app-workbench .tabulator-row.tabulator-row-even {
    border-color: var(--bs-border-color);
    background-color: var(--bs-body-bg);
    color: var(--bs-body-color);
}

.app-workbench .tabulator-row:hover,
.app-workbench .tabulator-row.tabulator-selected {
    background-color: var(--bs-secondary-bg);
}

.app-workbench .tabulator-row .tabulator-cell {
    border-color: var(--bs-border-color);
}

/* Редактируемая ячейка обязана отличаться от нередактируемой до нажатия:
   иначе финансист выясняет это перебором */
.app-workbench .tabulator-row .tabulator-cell.tabulator-editable:not(.tabulator-editing):hover {
    outline: 1px solid var(--bs-primary-border-subtle);
}

.app-workbench .tabulator-row .tabulator-cell.tabulator-editing input,
.app-workbench .tabulator-row .tabulator-cell.tabulator-editing select {
    color: var(--bs-body-color);
    background-color: var(--bs-body-bg);
}

.app-workbench .tabulator .tabulator-header .tabulator-col .tabulator-header-filter input {
    color: var(--bs-body-color);
    background-color: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--app-radius-sm);
}

/* ── Главная: дела и блоки ──────────────────────────────────────── */

/* Зона дел стоит над сеткой блоков и всегда во всю ширину: то, что ждёт
   лично человека, не соревнуется за место с витринами */
.app-tasks {
    margin-bottom: 1rem;
}

/* Ряд действий — верхняя полоса карточки дел: место кнопок постоянное,
   и это первое, что видит человек, открывший главную */
.app-quick-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 14px 20px;
    border-bottom: 1px solid var(--bs-border-color);
}

/* На узком экране кнопка занимает строку целиком: в ряд они там всё равно
   не встают, а половинчатая ширина — промах пальцем */
@media (max-width: 575.98px) {
    .app-quick-actions .btn {
        flex: 1 1 100%;
    }
}

.app-task-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.app-task + .app-task {
    border-top: 1px solid var(--bs-border-color);
}

.app-task > a {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 12px 20px;
    font-size: 14.5px;
    text-decoration: none;
    color: var(--bs-body-color);
}

.app-task > a:hover {
    background-color: var(--bs-tertiary-bg);
}

.app-task .app-task-label {
    flex-grow: 1;
}

/* Счётчик — плашка, а не текст: взгляд ищет число, а не строку */
.app-task .app-task-count {
    min-width: 1.85rem;
    padding: 1px 8px;
    border-radius: 999px;
    background-color: var(--bs-secondary-bg);
    font-size: 12.5px;
    font-weight: 600;
    text-align: center;
}

/* Тон задаёт полосу слева и цвет иконки — не заливку строки: список из
   пяти цветных прямоугольников перестаёт читаться */
.app-task-urgent > a {
    box-shadow: inset 3px 0 0 var(--bs-danger);
}

.app-task-urgent .app-icon {
    color: var(--bs-danger);
}

.app-task-urgent .app-task-count {
    background-color: var(--bs-danger-bg-subtle);
    color: var(--bs-danger-text-emphasis);
}

.app-task-warn > a {
    box-shadow: inset 3px 0 0 var(--bs-warning);
}

.app-task-warn .app-icon {
    color: var(--bs-warning-text-emphasis);
}

/* Сетка блоков: широкие занимают строку целиком, остальные встают парами
   и сами схлопываются в одну колонку на узком экране */
.app-dashboard {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
    gap: 1rem;
    align-items: start;
}

.app-widget-wide {
    grid-column: 1 / -1;
}

/* Отступ между карточками задаёт сетка — иначе он удваивается */
.app-dashboard .app-card + .app-card {
    margin-top: 0;
}

.app-widget-subtitle {
    margin: 0 0 0.5rem;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.085em;
    color: var(--app-th-fg);
}

/* Ряд счётчиков внутри блока */
.app-metrics {
    display: flex;
    flex-wrap: wrap;
}

.app-metric {
    flex: 1 1 120px;
    padding: 16px 20px;
    text-decoration: none;
    color: var(--bs-body-color);
}

.app-metric + .app-metric {
    border-left: 1px solid var(--bs-border-color);
}

a.app-metric:hover {
    background-color: var(--bs-tertiary-bg);
}

.app-metric-value {
    display: block;
    font-family: var(--app-font-display);
    font-size: 26px;
    line-height: 1.1;
    font-weight: 600;
}

.app-metric-label {
    display: block;
    margin-top: 2px;
    font-size: 12.5px;
    color: var(--bs-secondary-color);
}

/* Ненулевая очередь — единственное, что выделяется цветом: остальные
   счётчики остаются нейтральными, иначе выделение перестаёт работать */
.app-metric-alert .app-metric-value {
    color: var(--bs-danger-text-emphasis);
}

/* Список «строка — значение» для блоков без таблицы */
.app-list {
    margin: 0;
    padding: 0;
    list-style: none;
}

.app-card > .app-list > li,
.app-card-body > .app-list > li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    font-size: 14.5px;
}

.app-card > .app-list > li {
    padding: 11px 20px;
}

.app-card > .app-list > li + li,
.app-card-body > .app-list > li + li {
    border-top: 1px solid var(--bs-border-color);
}

.app-card-body > .app-list > li {
    padding: 7px 0;
}

/* Светофор состояния: цветом красится только рамка карточки */
.app-health-warn {
    border-color: var(--bs-warning-border-subtle);
}

.app-health-fail {
    border-color: var(--bs-danger-border-subtle);
}

/* ── Переводы между кассами ─────────────────────────────────────── */

/* Строка перевода — это пара «касса и сумма» дважды. Касса отвечает на
   вопрос колонки, сумма читается ниже: цифры выровнены по разряду, иначе
   столбец сумм рассыпается и его приходится читать, а не просматривать */
.app-transfers .app-transfer-cashbox {
    font-weight: 600;
    line-height: 1.3;
}

.app-transfers .app-transfer-amount {
    font-size: 13px;
    color: var(--bs-secondary-color);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.app-transfers .app-transfer-rate {
    color: var(--bs-secondary-color);
    font-variant-numeric: tabular-nums;
}

/* Направление показывает стрелка, а не порядок колонок: пара «откуда —
   куда» иначе различима только по заголовку таблицы. Селектор длинный,
   потому что отступы ячеек в карточке заданы через `.app-card .table` */
.app-card .table > thead > tr > .app-transfer-arrow-cell,
.app-card .table > tbody > tr > .app-transfer-arrow-cell {
    width: 1%;
    padding-left: 0;
    padding-right: 0;
    text-align: center;
}

.app-transfers .app-transfer-arrow::before {
    content: '→';
    color: var(--bs-secondary-color);
}

.app-transfers .app-transfer-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
}

.app-transfers .app-transfer-conduct {
    display: flex;
    align-items: center;
    gap: 8px;
}

.app-transfers .app-transfer-conduct .form-control {
    width: auto;
}

.btn .app-transfer-caret {
    width: 13px;
    height: 13px;
    margin-right: 0;
    margin-left: 6px;
    transition: transform 0.15s ease-in-out;
}

.btn[aria-expanded='true'] .app-transfer-caret {
    transform: rotate(180deg);
}

/* Подробности свёрнуты: развёрнутыми они удваивали каждую строку списка.
   Ячейка-обёртка своих отступов и линии не рисует — в свёрнутом виде от
   неё не должно остаться и полосы */
.app-card .table > tbody > tr.app-transfer-details > td {
    padding: 0;
    border-bottom-width: 0;
}

.app-transfer-panel {
    padding: 16px 20px;
    background-color: var(--app-filter-bg);
    border-bottom: 1px solid var(--bs-border-color);
}

/* У последнего перевода линию под панелью рисует край карточки */
.app-card .table > tbody > tr.app-transfer-details:last-child .app-transfer-panel {
    border-bottom-width: 0;
}

.app-transfer-panel .app-list > li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 5px 0;
    font-size: 14px;
}

.app-transfer-panel .app-list > li + li {
    border-top: 1px solid var(--bs-border-color);
}

/* Шкала этапов документа — макет `info/status-tracker.html` (п. 13 правок
   03.08.2026). Из макета взяты приёмы: соединительные линии, градиент на
   переходе «пройдено → текущее», кольцо у текущей точки и приписка снизу
   цветом состояния.

   Цвета — переменными темы, а не числами из макета: оболочка живёт в светлой
   теме, тёмной и контрастном режиме (11Г.2). Зелёный макета `#17a06f` — это
   `--bs-success`, охра `#c9a15e` — акцент темы `--bs-primary`. Шрифт и
   фиксированная ширина карточки из макета не переносятся: это оформление
   демонстрационной страницы, а не трекера.

   Узлов переменное число — пять у заявки, четыре у расхода, а при отказе или
   отмене хвост шкалы заменяется одним узлом исхода. Поэтому и колонки, и линии
   выводятся из разметки: числа узлов в стилях нет нигде.

   Состояния узла и тон приписки называются одними и теми же словами
   («done», «halted», «broken»), но живут на разных элементах — поэтому правила
   узлов уточнены классом `.app-progress-node`. Без него тон на обёртке
   раскрашивал бы все точки разом и вдобавок дорисовывал бы ей линию. */
.app-progress {
    /* Диаметр точки нужен ещё и линии — она держится за его половину */
    --app-progress-dot-size: .8125rem;
    /* Промежуток между узлами. Без него длинные подписи соседних узлов
       («Согласование» и «На проверке») смыкаются в одно слово: колонки делят
       ширину поровну, и текст занимает её всю. Тот же промежуток вычитается
       из линии — иначе она обрывалась бы, не дойдя до следующей точки */
    --app-progress-gap: 1.25rem;

    margin: .75rem 0 0;
}

.app-progress-track {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 0 var(--app-progress-gap);
    margin: 0 0 .625rem;
    padding: 0;
    list-style: none;
}

.app-progress-node {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    min-width: 0;
    color: var(--bs-secondary-color);
    font-size: .8125rem;
    line-height: 1.25;
    text-align: center;
}

/* Линия к следующему узлу висит на самом узле, а не отдельным элементом:
   последнему она тогда не достаётся сама собой, без правила «а сколько их» */
.app-progress-node:not(:last-child)::before {
    content: '';
    position: absolute;
    top: calc(var(--app-progress-dot-size) / 2 - 1px);
    left: 50%;
    /* До центра следующей точки: половина своей колонки плюс промежуток */
    right: calc(-50% - var(--app-progress-gap));
    height: 2px;
    background: var(--bs-border-color);
}

.app-progress-dot {
    position: relative;
    z-index: 1;
    width: var(--app-progress-dot-size);
    height: var(--app-progress-dot-size);
    border-radius: 50%;
    background: var(--bs-tertiary-color);
}

.app-progress-node.app-progress-done { color: var(--bs-success); }
.app-progress-node.app-progress-done .app-progress-dot { background: var(--bs-success); }
.app-progress-node.app-progress-done:not(:last-child)::before { background: var(--bs-success); }

/* Переход «пройдено → текущее». Какой узел последний пройденный, знает
   `:has()`, а не шаблон: расчёт этапа не должен знать про вёрстку */
.app-progress-node.app-progress-done:has(+ .app-progress-current)::before {
    background: linear-gradient(to right, var(--bs-success), var(--bs-primary));
}

.app-progress-node.app-progress-done:has(+ .app-progress-halted)::before {
    background: linear-gradient(to right, var(--bs-success), var(--bs-warning));
}

.app-progress-node.app-progress-done:has(+ .app-progress-broken)::before {
    background: linear-gradient(to right, var(--bs-success), var(--bs-danger));
}

.app-progress-node.app-progress-done:has(+ .app-progress-upcoming)::before {
    background: linear-gradient(to right, var(--bs-success), var(--bs-border-color));
}

/* Кольцо-подсветка: текущий узел должен находиться взглядом сразу, до чтения
   подписей. Подпись при этом берёт затемнённый акцент — латунь на белом даёт
   2:1 и на текст не годится */
.app-progress-node.app-progress-current { color: var(--app-accent-text); font-weight: 600; }

.app-progress-node.app-progress-current .app-progress-dot {
    background: var(--bs-primary);
    box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), .22);
}

.app-progress-node.app-progress-halted { color: var(--bs-warning-text-emphasis); font-weight: 600; }

.app-progress-node.app-progress-halted .app-progress-dot {
    background: var(--bs-warning);
    box-shadow: 0 0 0 3px rgba(var(--bs-warning-rgb), .22);
}

.app-progress-node.app-progress-broken { color: var(--bs-danger-text-emphasis); font-weight: 600; }

.app-progress-node.app-progress-broken .app-progress-dot {
    background: var(--bs-danger);
    box-shadow: 0 0 0 3px rgba(var(--bs-danger-rgb), .22);
}

/* Пометка «из шаблона регулярных платежей» — иконка рядом с типом заявки.
   Приглушена: это свойство заявки, а не предупреждение, и спорить за внимание
   со статусом она не должна */
.app-recurring-mark {
    display: inline-flex;
    align-items: center;
    margin-left: .3rem;
    color: var(--bs-secondary-color);
    vertical-align: -.15em;
}

.app-recurring-mark .app-icon {
    width: .9375rem;
    height: .9375rem;
}

/* Тот же словарь состояний, но одним словом без шкалы: в списке заявок
   показывается только узел, на котором заявка стоит сейчас. Класс свой —
   правила узла держатся за соседей и за колонку, а здесь ни того, ни другого */
.app-progress-mark.app-progress-done { color: var(--bs-success); }
.app-progress-mark.app-progress-current { color: var(--app-accent-text); font-weight: 600; }
.app-progress-mark.app-progress-halted { color: var(--bs-warning-text-emphasis); font-weight: 600; }
.app-progress-mark.app-progress-broken { color: var(--bs-danger-text-emphasis); font-weight: 600; }

/* Приписка снизу — цветом состояния: она и есть ответ «чего ждём и от кого» */
.app-progress-note { font-size: .9375rem; }
.app-progress-waiting .app-progress-note { color: var(--bs-body-color); }
.app-progress-done .app-progress-note { color: var(--bs-success); }
.app-progress-halted .app-progress-note { color: var(--bs-warning-text-emphasis); }
.app-progress-broken .app-progress-note { color: var(--bs-danger-text-emphasis); }

/* На узкой ширине шкала разворачивается вертикально. Пять колонок на телефоне
   дают по 60 px на подпись, а «Согласование» требует вдвое больше — подписи
   наезжают друг на друга. Ужать шрифт не помогает: слово одно и не переносится.

   Приёмы при этом те же самые — линия, градиент, кольцо, — просто линия идёт
   сверху вниз. Отдельного трекера для телефона нет. */
@media (max-width: 575.98px) {
    .app-progress-track {
        grid-auto-flow: row;
        grid-auto-columns: auto;
        gap: .75rem;
    }

    /* В вертикальном режиме промежуток между узлами задаёт `gap` строк, и
       поправка на колонки линии только помешала бы */
    .app-progress {
        --app-progress-gap: 0rem;
    }

    .app-progress-node {
        flex-direction: row;
        align-items: center;
        gap: .625rem;
        text-align: left;
    }

    /* Линия тянется от центра своей точки до центра следующей: при равной
       высоте строк это ровно «высота строки плюс промежуток». Верхний её
       кусок закрывает сама точка — она непрозрачна и лежит слоем выше */
    .app-progress-node:not(:last-child)::before {
        top: 50%;
        right: auto;
        bottom: calc(-.75rem - 50%);
        left: calc(var(--app-progress-dot-size) / 2 - 1px);
        width: 2px;
        height: auto;
    }

    .app-progress-node.app-progress-done:has(+ .app-progress-current)::before {
        background: linear-gradient(to bottom, var(--bs-success), var(--bs-primary));
    }

    .app-progress-node.app-progress-done:has(+ .app-progress-halted)::before {
        background: linear-gradient(to bottom, var(--bs-success), var(--bs-warning));
    }

    .app-progress-node.app-progress-done:has(+ .app-progress-broken)::before {
        background: linear-gradient(to bottom, var(--bs-success), var(--bs-danger));
    }

    .app-progress-node.app-progress-done:has(+ .app-progress-upcoming)::before {
        background: linear-gradient(to bottom, var(--bs-success), var(--bs-border-color));
    }
}

/* Числовое поле без стрелок-спиннера.

   В поле, где значение вводят один раз и целиком (ID на входе), спиннер только
   мешает попадать курсором: кнопки съедают правый край поля, а нажатие на них
   меняет значение на единицу — ровно то, чего от этого поля никогда не хотят. */
.app-numeric {
    -moz-appearance: textfield;
    appearance: textfield;
}

.app-numeric::-webkit-outer-spin-button,
.app-numeric::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
