:root {
    --color-bg: #f4f6f8;
    --color-surface: #ffffff;
    --color-border: #dde3ea;
    --color-primary: #2563eb;
    --color-primary-dark: #1d4ed8;
    --color-text: #1f2937;
    --color-muted: #6b7280;
    --color-danger: #dc2626;
    --color-danger-bg: #fee2e2;
    --color-success: #16a34a;
    --color-occupied: #cbd5e1;
    --color-occupied-server: #93c5fd;
    --color-occupied-network: #fcd34d;
    --color-occupied-aux: #a7f3d0;
    --radius: 6px;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
    background: var(--color-bg);
    color: var(--color-text);
    font-size: 14px;
}

a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }

h1 { font-size: 22px; margin: 0 0 16px; }
h2 { font-size: 18px; margin: 0 0 12px; }
h3 { font-size: 15px; margin: 16px 0 8px; }

.muted { color: var(--color-muted); }

/* ----- Login ----- */
.login-wrap {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #1e293b, #0f172a);
}
.login-box {
    background: var(--color-surface);
    padding: 32px;
    border-radius: var(--radius);
    width: 340px;
    box-shadow: 0 10px 30px rgba(0,0,0,.3);
}
.login-box h1 { font-size: 18px; margin-bottom: 20px; text-align: center; }
.login-box label { display: block; margin-bottom: 14px; font-size: 13px; color: var(--color-muted); }
.login-box input {
    display: block; width: 100%; margin-top: 4px; padding: 8px 10px;
    border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 14px;
}
.login-box button {
    width: 100%; padding: 10px; background: var(--color-primary); color: #fff;
    border: none; border-radius: var(--radius); font-size: 14px; cursor: pointer;
}
.login-box button:hover { background: var(--color-primary-dark); }

/* ----- Layout ----- */
.app-header {
    display: flex; align-items: center; gap: 24px;
    background: var(--color-surface); border-bottom: 1px solid var(--color-border);
    padding: 0 20px; height: 56px;
}
.app-title { font-weight: 600; white-space: nowrap; flex-shrink: 0; }
.app-nav { display: flex; align-items: center; gap: 4px; flex: 1 0 auto; flex-wrap: nowrap; min-width: 0; justify-content: center; }
.app-nav a {
    padding: 8px 12px; border-radius: var(--radius); color: var(--color-text);
    font-size: 13px; white-space: nowrap; flex-shrink: 0;
}
.app-nav a.active, .app-nav a:hover { background: var(--color-bg); text-decoration: none; }

/* Выпадающее подменю «Раздельный учёт» (Код-ресурса / Элемент услуги) —
   объединяет два пункта в один, чтобы не занимать в строке меню место под
   оба отдельных длинных названия. */
.app-nav-dropdown { position: relative; flex-shrink: 0; }
.app-nav-dropdown-toggle {
    background: none; border: none; color: inherit; font: inherit; cursor: pointer;
    padding: 8px 12px; border-radius: var(--radius); white-space: nowrap; font-size: 13px;
    transition: background-color .15s ease;
}
.app-nav-dropdown-toggle:hover, .app-nav-dropdown-toggle.active { background: var(--color-bg); }
.app-nav-dropdown-menu {
    display: block; position: absolute; top: 100%; left: 0; margin-top: 4px; min-width: 180px;
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    box-shadow: 0 8px 20px rgba(0,0,0,.18); z-index: 1000; padding: 4px;
    opacity: 0; visibility: hidden; transform: translateY(-6px);
    transition: opacity .15s ease, transform .15s ease, visibility .15s;
    pointer-events: none;
}
.app-nav-dropdown.open .app-nav-dropdown-menu { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; }
.app-nav-dropdown-menu a {
    display: block; padding: 8px 12px; color: var(--color-text) !important; border-radius: 4px;
    white-space: nowrap; font-size: 13px; transition: background-color .15s ease;
}
.app-nav-dropdown-menu a:hover,
.app-nav-dropdown-menu a.active { background: var(--color-primary-light, #dbeafe) !important; text-decoration: none !important; }
.app-user { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--color-muted); margin-left: auto; flex-shrink: 0; }
.btn-link { color: var(--color-primary); }

/* Кнопка-«гамбургер» — на десктопе не видна, меню и так помещается в строку;
   на мобильном заменяет собой полное горизонтальное меню (см. медиа-запрос
   ниже), чтобы не приходилось прокручивать шапку вбок. */
.app-nav-toggle {
    display: none; background: none; border: none; color: inherit; font-size: 22px;
    cursor: pointer; padding: 4px 10px; line-height: 1;
}
/* На десктопе это просто «прозрачная» обёртка — её дети (меню, поиск,
   пользователь) ведут себя так, будто обёртки нет вовсе. */
.app-header-collapsible { display: contents; }

.app-main { max-width: 1400px; margin: 0 auto; padding: 24px 20px 60px; }

/* ----- Cards ----- */
.cards-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 28px; perspective: 900px; }
/* transform выставляется в JS по позиции курсора (3D tilt, см. main.js,
   PAGE==='dashboard') — transition тут только чтобы возврат в исходное
   положение на mouseleave был плавным, сам tilt во время движения мыши
   должен успевать за курсором без задержки транзишена (JS не трогает
   transition-duration, полагаемся на то, что частые новые значения
   "перебивают" анимацию раньше, чем она завершится — стандартный приём). */
.card { position: relative; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 16px; cursor: grab; transition: transform .15s ease, box-shadow .15s ease; will-change: transform; }
.card:hover { box-shadow: 0 10px 24px -10px rgba(15, 23, 42, .25); }
.card-value { font-size: 28px; font-weight: 700; }
.card-label { font-size: 13px; color: var(--color-muted); margin-top: 4px; }

/* ----- Уведомления о незаполненных данных (дашборд) ----- */
.notice-collapsible {
    background: #fffbeb; border: 1px solid #fde68a; border-radius: var(--radius);
    margin-bottom: 20px; overflow: hidden;
}
.notice-summary {
    padding: 12px 16px; cursor: pointer; font-size: 14px; font-weight: 600;
    color: #92400e; list-style: none; user-select: none;
}
.notice-summary::-webkit-details-marker { display: none; }
.notice-summary::before { content: '▸ '; display: inline-block; transition: transform .15s; }
.notice-collapsible[open] .notice-summary::before { transform: rotate(90deg); }
.notice-body { padding: 0 16px 14px; }
.notice-body .data-table { margin-bottom: 0; }

/* ----- Tables ----- */
.data-table { width: 100%; border-collapse: collapse; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); overflow: hidden; margin-bottom: 16px; }
.data-table th, .data-table td { padding: 8px 12px; border-bottom: 1px solid var(--color-border); text-align: left; font-size: 13px; }
.data-table th { background: var(--color-bg); font-weight: 600; }
.data-table tbody tr:hover { background: #fafbfc; }

/* Строки таблицы ПО кликабельны (двойной клик — карточка с подробностями) —
   курсор и более заметная подсветка дают понять, что это не просто текст. */
#sw-table tbody tr:hover { background: #eff6ff; cursor: pointer; }
#eq-table tbody tr:hover { background: #eff6ff; cursor: pointer; }
/* ВАЖНО: не display:flex — flex на <td> может сбивать выравнивание строки
   относительно сетки таблицы в некоторых браузерах (ячейка перестаёт вести
   себя как table-cell). Тот же визуальный эффект (кнопки в ряд с отступом)
   получаем через margin на дочерних элементах, не трогая display ячейки. */
.data-table .actions { white-space: nowrap; }
.data-table .actions > * { margin-right: 6px; }
.data-table .actions > *:last-child { margin-right: 0; }
.data-table .actions button, .data-table .actions a { font-size: 12px; padding: 4px 8px; }

/* ----- Toolbar / forms ----- */
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; }
.toolbar input[type=text], .toolbar select { padding: 7px 10px; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 13px; }

button {
    padding: 7px 14px; border-radius: var(--radius); border: 1px solid var(--color-border);
    background: var(--color-surface); cursor: pointer; font-size: 13px;
}
button:hover { background: var(--color-bg); }
.btn-primary { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.btn-primary:hover { background: var(--color-primary-dark); }
.btn-secondary { background: var(--color-surface); }
.btn-danger { color: var(--color-danger); border-color: var(--color-danger); background: #fff; }
.btn-danger:hover { background: var(--color-danger-bg); }

/* ----- Modal ----- */
.modal-overlay {
    position: fixed; inset: 0; background: rgba(15,23,42,.5);
    display: flex; align-items: flex-start; justify-content: center;
    padding: 40px 16px; overflow-y: auto; z-index: 100;
}
.modal { background: #fff; border-radius: var(--radius); padding: 24px; width: 100%; max-width: 720px; }
/* История изменений — таблица с 6 колонками (включая «Было»/«Стало») тесновата
   в стандартной ширине модалки; расширяем и подстраховываемся горизонтальной
   прокруткой на случай длинных значений, которые не переносятся. */
#eq-history-modal .modal { max-width: 960px; }
#eq-history-modal .table-scroll { overflow-x: auto; }
/* word-break — только в этой таблице (длинные значения без пробелов
   переносятся внутри ячейки вместо растягивания), НЕ на все .data-table
   сайта: раньше это ломало перенос обычного короткого текста в остальных
   таблицах приложения. */
#eq-history-modal .data-table td { word-break: break-word; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px 16px; }
/* Стилизация распространяется на любую форму с этими классами, а не только
   внутри модалок — иначе обычные страничные формы (например, настройки
   профиля) остаются без выравнивания label/input по сетке. */
.modal form label, .form-grid label, .settings-group-body form > label {
    display: block; font-size: 12px; color: var(--color-muted); margin-bottom: 6px;
}
.modal form input, .modal form select, .modal form textarea,
.form-grid input, .form-grid select, .form-grid textarea,
.settings-group-body form > input, .settings-group-body form > select {
    display: block; width: 100%; margin-top: 4px; padding: 7px 9px;
    border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 13px; font-family: inherit;
}
.checkbox-label { display: flex !important; align-items: center; gap: 6px; }
.checkbox-label input { width: auto !important; }
.form-error { color: var(--color-danger); font-size: 13px; margin-top: 10px; min-height: 16px; }
.inline-form { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 10px; }
.inline-form label { display: block; font-size: 12px; color: var(--color-muted); }
.inline-form input, .inline-form select { display: block; margin-top: 4px; padding: 6px 8px; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 13px; font-family: inherit; height: 32px; }
.inline-form input[type=file] { height: auto; padding: 4px; }

/* Форма «Нарезать вложенную подсеть» на странице подсети (public/views/
   ip_subnet_view.php) — не модалка (в отличие от прочих форм добавления/
   редактирования в приложении) и не .form-grid целиком, поэтому не
   попадала под правило ".modal form label/input" — из-за этого поля CIDR
   и «Описание» были без стилизации: разной ширины, не на всю ширину
   формы, вперемешку с остальными полями (жалоба на нарушенную вёрстку).
   Стилизуем явно, тем же правилом, что и формы в модалках/.form-grid. */
#ipv-child-form > label {
    display: block; font-size: 12px; color: var(--color-muted); margin-bottom: 6px; margin-top: 10px;
}
#ipv-child-form > label:first-child { margin-top: 0; }
#ipv-child-form input, #ipv-child-form select, #ipv-child-form textarea {
    display: block; width: 100%; margin-top: 4px; padding: 7px 9px;
    border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 13px; font-family: inherit;
}

/* Нативная кнопка «Обзор...» у input[type=file] выбивается из общего вида —
   оформляем её как обычную кнопку приложения (поддерживается всеми
   современными браузерами; в старых просто останется системный вид). */
input[type=file] {
    border: 1px solid var(--color-border); border-radius: var(--radius);
    font-size: 13px; font-family: inherit; color: var(--color-text); background: var(--color-surface);
}
input[type=file]::file-selector-button,
input[type=file]::-webkit-file-upload-button {
    padding: 5px 12px; margin-right: 10px;
    border: 1px solid var(--color-border); border-radius: calc(var(--radius) - 2px);
    background: var(--color-surface); color: var(--color-text); font-size: 12px; font-family: inherit; cursor: pointer;
}
input[type=file]:hover::file-selector-button,
input[type=file]:hover::-webkit-file-upload-button { background: var(--color-bg); }

/* ----- Alerts ----- */
.alert { padding: 10px 14px; border-radius: var(--radius); margin-bottom: 14px; font-size: 13px; }
.alert-error { background: var(--color-danger-bg); color: var(--color-danger); }
.alert-success { background: #dcfce7; color: var(--color-success); }
.alert-info { background: var(--color-bg); color: var(--color-text); border: 1px solid var(--color-border); }
.alert-warn { background: #fffbeb; color: #92400e; border: 1px solid #f59e0b; }

/* ----- Pagination ----- */
.pagination { display: flex; gap: 6px; }
.pagination button.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }

/* ----- Rack planner (раздел 2.3 ТЗ) ----- */
.rack-view-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.side-toggle { display: inline-flex; border: 1px solid var(--color-border); border-radius: var(--radius); overflow: hidden; }
.side-btn { border: none; border-radius: 0; }
.side-btn.active { background: var(--color-primary); color: #fff; }
.occupancy-info-wrap { display: flex; gap: 16px; align-items: center; }
.occupancy-info { font-size: 13px; color: var(--color-muted); }
.occupancy-info.overflow { color: var(--color-danger); font-weight: 700; }

.rack-planner { display: grid; grid-template-columns: 220px 1fr 240px; gap: 20px; align-items: start; }
.rack-unplaced, .rack-side-panel {
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 12px;
}
.unplaced-list { display: flex; flex-direction: column; gap: 6px; max-height: 640px; overflow-y: auto; }
.unplaced-item {
    border: 1px solid var(--color-border); border-radius: var(--radius); padding: 6px 8px;
    background: #fff; font-size: 12px;
}
.unplaced-item-draghandle { cursor: grab; padding: 2px 0; }
.unplaced-item-draghandle:active { cursor: grabbing; }
.unplaced-item-title { font-weight: 600; }
.unplaced-item-sub { color: var(--color-muted); }
.unplaced-item-controls {
    display: flex; align-items: center; justify-content: space-between; gap: 6px;
    margin-top: 6px; padding-top: 6px; border-top: 1px dashed var(--color-border);
}
.unplaced-height-label {
    display: flex !important; align-items: center; gap: 6px;
    font-size: 11px !important; color: var(--color-muted); margin: 0 !important;
}
.unplaced-height-input {
    width: 48px; padding: 3px 5px; border: 1px solid var(--color-border);
    border-radius: 4px; font-size: 12px; cursor: text;
}
.unplaced-place-btn {
    font-size: 11px; padding: 4px 8px; background: var(--color-primary); color: #fff; border-color: var(--color-primary);
}
.unplaced-place-btn:hover { background: var(--color-primary-dark); }

.rack-schema-wrap { display: flex; justify-content: center; }
.rack-schema {
    background: #334155; border-radius: var(--radius); padding: 8px;
    display: flex; flex-direction: column; width: 420px;
    position: relative; /* обязательно: без этого .rack-equipment-block (position:absolute)
                            позиционируется относительно body, а не схемы стойки */
}
.rack-unit-row {
    display: flex; align-items: center; height: 34px; border-bottom: 1px solid #475569;
    position: relative;
}
.rack-unit-row:last-child { border-bottom: none; }
.rack-unit-label { width: 34px; font-size: 10px; color: #cbd5e1; text-align: right; padding-right: 6px; flex-shrink: 0; }
.rack-unit-slot { flex: 1; height: 100%; }
/* Свободные юниты — светлый фон (по ТЗ: "занятые — серый фон, свободные — белый/светло-серый") */
.rack-unit-slot.free { background: #f1f5f9; cursor: pointer; }
/* Занятые юниты закрашены самим .rack-equipment-block поверх — здесь фон не нужен,
   иначе получается "плавающая карточка" поверх отдельно закрашенного юнита */
.rack-unit-slot.occupied { background: transparent; }
.rack-unit-slot.free.drag-over { background: #bfdbfe; outline: 2px solid #2563eb; outline-offset: -2px; }
.rack-unit-slot.drag-over-invalid { background: #fecaca !important; outline: 2px solid #dc2626; outline-offset: -2px; }

.rack-equipment-block {
    position: absolute; left: 34px; right: 0; box-sizing: border-box;
    background: var(--color-occupied); border-top: 1px solid rgba(255,255,255,.35);
    border-bottom: 1px solid rgba(0,0,0,.25);
    display: flex; align-items: center; justify-content: space-between; padding: 0 8px;
    font-size: 11px; color: #1f2937; cursor: grab; overflow: hidden;
}
.rack-equipment-block.type-server { background: var(--color-occupied-server); }
.rack-equipment-block.type-network { background: var(--color-occupied-network); }
.rack-equipment-block.type-auxiliary { background: var(--color-occupied-aux); }
.rack-equipment-block:hover { filter: brightness(0.96); }
.rack-equipment-block:active { cursor: grabbing; }
.rack-equipment-block .eq-title {
    font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    background: rgba(255,255,255,.72); padding: 2px 6px; border-radius: 3px;
    box-shadow: 0 0 0 1px rgba(0,0,0,.06); min-width: 0; flex: 1 1 auto;
}
.rack-equipment-block .eq-remove {
    background: rgba(0,0,0,.15); border: none; border-radius: 3px; width: 16px; height: 16px;
    font-size: 10px; line-height: 1; cursor: pointer; flex-shrink: 0;
    position: relative; z-index: 3;
}

/* Выделение по принадлежности (own/client/technological/...) применяется
   динамически инлайн-стилем в JS/PHP на основе цвета из справочника
   «Настройки» — см. racks-view.js (ownershipColors) и hall_view.php. */

.hint-list { padding-left: 18px; font-size: 12px; color: var(--color-muted); line-height: 1.6; }


/* ----- Reports ----- */
.report-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
.report-tab.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.report-controls { display: flex; gap: 8px; margin-bottom: 16px; }

@media (max-width: 900px) {
    .rack-planner { grid-template-columns: 1fr; }
    .rack-schema { width: 100%; }

    /* Шапка: полное меню + поиск + пользователь сворачиваются под
       «гамбургер», чтобы шапка не растягивалась вбок и не требовала
       горизонтальной прокрутки. */
    .app-header { flex-wrap: wrap; height: auto; min-height: 56px; padding: 10px 14px; gap: 8px; }
    .app-nav-toggle { display: inline-block; margin-left: auto; }
    .app-header-collapsible {
        display: none; flex-direction: column; align-items: stretch; width: 100%; gap: 10px;
        order: 10; padding-top: 6px; border-top: 1px solid rgba(255,255,255,.2);
    }
    .app-header-collapsible.open { display: flex; }
    .app-nav { flex-direction: column; gap: 2px; flex: none; justify-content: flex-start; }
    .app-nav a { padding: 10px 12px; }
    .app-nav-dropdown-toggle { display: block; width: 100%; text-align: left; padding: 10px 12px; }
    .app-nav-dropdown-menu { position: static; box-shadow: none; border: none; margin: 0 0 0 12px; padding: 0; }
    .app-global-search { margin: 0; flex: none; max-width: none; }
    .app-user { flex-wrap: wrap; gap: 8px 14px; margin-left: 0; }

    /* Таблицы: не растягивают всю страницу вбок — прокручивается только
       сама таблица в своих границах, остальной интерфейс остаётся на месте. */
    .data-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }

    /* Схема стойки — тоже контролируемая прокрутка внутри своего блока,
       а не всей страницы целиком. */
    .rack-schema-wrap, .rack-planner { overflow-x: auto; -webkit-overflow-scrolling: touch; }

    /* Модалки — используют почти всю ширину экрана вместо фиксированных
       отступов, рассчитанных на десктоп. */
    .modal-overlay { padding: 16px 8px; }
    .modal { padding: 16px; max-width: 100%; }

    /* Карточки сводки и парные half-виджеты дашборда — в один столбец,
       иначе на узком экране они сжимаются до нечитаемой ширины. */
    .dash-widget.dash-widget-half { flex: 1 1 100%; width: 100%; min-width: 0; }
    .cards-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }

    /* Тулбары с большим числом элементов — переносятся по строкам без
       обрезания и без горизонтальной прокрутки. */
    .toolbar { gap: 6px; }
    .toolbar input[type=text], .toolbar select { flex: 1 1 140px; }
}

/* ----- Обзор зала: миниатюры стоек ----- */
.hall-mini-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 20px;
    align-items: start; /* рамки стоек выравниваются по верху сетки — их положение
                            не должно зависеть от длины текста в подписи под ними */
}
.hall-mini-rack { text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; }
.hall-mini-rack:hover .hall-mini-rack-frame { outline: 2px solid var(--color-primary); }
.hall-mini-rack-frame {
    position: relative; width: 70px; background: #334155; border-radius: 3px;
    outline: 1px solid var(--color-border); outline-offset: 0; flex-shrink: 0;
}
.hall-mini-rack-frame.power-overflow { outline: 3px solid var(--color-danger); box-shadow: 0 0 0 1px var(--color-danger); }
.hall-mini-eq {
    position: absolute; left: 4px; right: 4px; background: var(--color-occupied); border-radius: 1px;
}
.hall-mini-eq.type-server { background: var(--color-occupied-server); }
.hall-mini-eq.type-network { background: var(--color-occupied-network); }
.hall-mini-eq.type-auxiliary { background: var(--color-occupied-aux); }
.hall-mini-rack-label { margin-top: 8px; text-align: center; font-size: 12px; line-height: 1.4; min-height: 56px; }

.rack-schema.place-mode-active { cursor: crosshair; }
.rack-schema.place-mode-active .rack-unit-slot.free { cursor: crosshair; }
#place-mode-hint { background: #dbeafe; border: 1px solid #93c5fd; border-radius: 4px; padding: 6px 8px; margin: 0 0 10px; }
#place-mode-hint a { font-weight: 600; }

/* ----- Разворачиваемые разделы формы (карточка оборудования) ----- */
.form-section {
    border: 1px solid var(--color-border); border-radius: 6px; padding: 0 14px; margin-bottom: 10px;
}
.form-section summary {
    padding: 10px 0; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--color-text);
    list-style: none; user-select: none;
}
.form-section summary::-webkit-details-marker { display: none; }
.form-section summary::before { content: '▸ '; display: inline-block; transition: transform .15s; color: var(--color-muted); }
.form-section[open] summary::before { transform: rotate(90deg); }
.form-section[open] { padding-bottom: 14px; }
.form-section .form-grid { margin-top: 4px; }
.form-section textarea { margin-top: 4px; }

/* ----- Блоки питания (динамический список) ----- */
.psu-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.psu-row input { flex: 1; padding: 7px 9px; border: 1px solid var(--color-border); border-radius: 4px; font-size: 13px; }
.psu-row .psu-remove-btn {
    background: var(--color-danger-bg); color: var(--color-danger); border: none; border-radius: 4px;
    width: 26px; height: 26px; cursor: pointer; font-size: 14px; flex-shrink: 0;
}

/* ----- Дашборд: диаграмма и сегменты вместимости стоек ----- */
.capacity-chart-wrap { display: flex; gap: 20px; align-items: flex-start; margin-bottom: 20px; flex-wrap: wrap; }
.capacity-chart { width: 320px; height: 320px; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius); flex-shrink: 0; }
.capacity-chart circle { cursor: pointer; }
.capacity-chart-legend { font-size: 13px; display: flex; flex-direction: column; gap: 6px; padding-top: 8px; }
.legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; }

.hall-capacity-section {
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    margin-bottom: 10px; padding: 0 14px;
}
.hall-capacity-section summary {
    padding: 10px 0; cursor: pointer; font-weight: 600; font-size: 14px; list-style: none; user-select: none;
}
.hall-capacity-section summary::-webkit-details-marker { display: none; }
.hall-capacity-section summary::before { content: '▸ '; color: var(--color-muted); }
.hall-capacity-section[open] summary::before { content: '▾ '; }
.hall-capacity-section[open] { padding-bottom: 12px; }

.fit-good { color: #16a34a; font-weight: 600; }
.fit-warn { color: #d97706; font-weight: 600; }
.fit-bad { color: #dc2626; font-weight: 600; }

/* ----- Сегментация страницы «Настройки» ----- */
.settings-group {
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    margin-bottom: 14px; padding: 0 16px;
}
.settings-group summary {
    padding: 12px 0; cursor: pointer; font-weight: 700; font-size: 16px; list-style: none; user-select: none;
}
.settings-group summary::-webkit-details-marker { display: none; }
.settings-group summary::before { content: '▸ '; color: var(--color-muted); font-weight: 400; }
.settings-group[open] summary::before { content: '▾ '; }
.settings-group-body { padding-bottom: 16px; }
.settings-group-body h3 { margin-top: 20px; }
.settings-group-body h3:first-child { margin-top: 4px; }

/* ----- Автодополнение (подбор оборудования) ----- */
.autocomplete-suggestions {
    position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
    background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius);
    max-height: 220px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,.1); margin-top: 2px;
}
.autocomplete-item { padding: 7px 10px; cursor: pointer; font-size: 13px; }
.autocomplete-item:hover, .autocomplete-item.active { background: var(--color-bg); }
.autocomplete-item .ac-title { font-weight: 600; }
.autocomplete-item .ac-sub { color: var(--color-muted); font-size: 12px; }
.autocomplete-empty { padding: 8px 10px; color: var(--color-muted); font-size: 12px; }

/* ----- Полосы использования ресурсов (БП) ----- */
.resource-usage-row { margin-bottom: 12px; }
.resource-usage-label { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 4px; }
.resource-usage-bar-bg { background: var(--color-bg); border-radius: 4px; height: 10px; overflow: hidden; }
.resource-usage-bar-fill { height: 100%; background: var(--color-primary); border-radius: 4px; }
.resource-usage-bar-fill.over { background: var(--color-danger); }

/* ----- Сортируемые заголовки таблиц ----- */
.sortable-th { cursor: pointer; user-select: none; position: relative; padding-right: 18px !important; }
.sortable-th:hover { background: #e2e8f0; }
.sortable-th::after { content: '↕'; position: absolute; right: 6px; opacity: 0.35; font-size: 11px; }
.sortable-th.sort-asc::after { content: '↑'; opacity: 1; color: var(--color-primary); }
.sortable-th.sort-desc::after { content: '↓'; opacity: 1; color: var(--color-primary); }

/* ----- Перетаскиваемые виджеты дашборда ----- */
#dashboard-widgets { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; }
.dash-widget { position: relative; padding-left: 22px; flex: 1 1 100%; width: 100%; min-width: 0; }
/* Пара виджетов с этим классом встаёт рядом, по 50% экрана каждый — благодаря
   flex-wrap на контейнере это работает "из коробки" для любых двух подряд
   идущих half-виджетов, без отдельной логики в drag-and-drop.
   !important и явный width — защита от того, чтобы соседний .dash-widget-half
   случайно "утащил" за собой на 50% следующий обычный (полноширинный) виджет
   при определённых порядках после перетаскивания. */
.dash-widget:not(.dash-widget-half) { flex: 1 1 100% !important; width: 100% !important; }
.dash-widget.dash-widget-half { flex: 1 1 calc(50% - 14px); width: calc(50% - 14px); min-width: 360px; }
.dash-widget-handle {
    position: absolute; left: -4px; top: 2px; width: 20px; height: 26px;
    display: flex; align-items: center; justify-content: center;
    cursor: grab; color: var(--color-muted); font-size: 14px; border-radius: 4px;
}
.dash-widget-handle:hover { background: var(--color-bg); color: var(--color-text); }
.dash-widget-handle:active { cursor: grabbing; }
.dash-widget-hide-btn {
    position: absolute; right: 4px; top: 2px; width: 22px; height: 22px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    border: none; background: none; color: var(--color-muted); font-size: 13px;
    cursor: pointer; border-radius: 4px; opacity: 0; transition: opacity .15s;
}
.dash-widget:hover .dash-widget-hide-btn { opacity: 1; }
.dash-widget-hide-btn:hover { background: var(--color-danger-bg); color: var(--color-danger); }
.dash-widget.drag-over-top { box-shadow: inset 0 3px 0 var(--color-primary); }
.dash-widget.drag-over-bottom { box-shadow: inset 0 -3px 0 var(--color-primary); }
.dash-widget.dragging { opacity: 0.4; }
.card-drag-hint { position: absolute; top: 4px; right: 6px; color: var(--color-muted); font-size: 12px; opacity: .5; }
.card:hover .card-drag-hint { opacity: 1; }
.card:active { cursor: grabbing; }
.card.drag-over-left { box-shadow: inset 3px 0 0 var(--color-primary); }
.card.drag-over-right { box-shadow: inset -3px 0 0 var(--color-primary); }
.card.dragging { opacity: 0.4; }

/* ----- Логотип в шапке и подвал с копирайтом ----- */
.app-logo { height: 28px; max-width: 160px; object-fit: contain; vertical-align: middle; margin-right: 8px; }
.app-footer {
    max-width: 1400px; margin: 0 auto; padding: 16px 20px 32px; text-align: center;
    font-size: 12px; color: var(--color-muted); border-top: 1px solid var(--color-border); margin-top: 24px;
}

/* ----- Пресеты тем и превью градиента (настройки профиля) ----- */
.theme-preset-grid { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }
.theme-preset-btn {
    display: flex; align-items: center; gap: 8px; padding: 8px 12px;
    border: 1px solid var(--color-border); border-radius: var(--radius); background: #fff; font-size: 13px; cursor: pointer;
}
.theme-preset-btn:hover { border-color: var(--color-primary); }
.theme-preset-swatch {
    display: inline-block; width: 18px; height: 18px; border-radius: 50%;
    background: var(--preview-color); flex-shrink: 0; border: 1px solid rgba(0,0,0,.1);
}
.theme-preset-swatch.is-gradient { background: linear-gradient(135deg, var(--preview-color), var(--preview-color2)); }
.gradient-preview {
    height: 40px; border-radius: var(--radius); margin-top: 10px; border: 1px solid var(--color-border);
    background: var(--color-primary-bg, var(--color-primary));
}

/* ----- Логотип на странице входа/восстановления пароля ----- */
.app-logo-login { display: block; margin: 0 auto 14px; max-width: 220px; object-fit: contain; }

/* ----- QR-коды и печать этикеток (стойки/оборудование) ----- */
.qr-box { display: inline-block; line-height: 0; }
.qr-box canvas { border: 1px solid var(--color-border); border-radius: 4px; }
.print-label-wrap { max-width: 380px; }
.print-card {
    border: 1px solid var(--color-border); border-radius: var(--radius); padding: 16px;
    background: var(--color-surface);
}
.print-card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.print-only { display: none; }

@media print {
    .no-print, .app-header, .app-nav, .app-footer { display: none !important; }
    .print-only { display: block !important; }
    body.page-rack_view .app-main > *:not(.print-only) { display: none !important; }
    body { background: #fff !important; }
    .print-card { border: 1px solid #000; box-shadow: none; }
}

/* ----- Массовая печать этикеток ----- */
.bulk-print-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin-top: 16px;
}
.bulk-print-card { break-inside: avoid; page-break-inside: avoid; }
@media print {
    .bulk-print-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
}

/* ----- PDU (распределители питания): вертикальные полосы по краям схемы ----- */
.rack-pdu-strip {
    position: absolute; top: 8px; bottom: 8px; width: 16px;
    background: #f59e0b; border-radius: 3px; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 0 0 1px rgba(0,0,0,.25);
}
.rack-pdu-strip:hover { background: #fbbf24; }
.rack-pdu-strip-left { left: -24px; }
.rack-pdu-strip-right { right: -24px; }
.rack-pdu-strip-text {
    writing-mode: vertical-rl; transform: rotate(180deg); font-size: 9px; font-weight: 700;
    color: #1e293b; white-space: nowrap; overflow: hidden; max-height: 100%;
}
.rack-pdu-label {
    position: absolute; top: -30px; width: 70px; text-align: center; font-size: 11px;
    background: #fffbeb; border: 1px solid #f59e0b; border-radius: 4px; padding: 3px 4px; color: #92400e;
}
.rack-pdu-label-left { left: -59px; }
.rack-pdu-label-right { right: -59px; }

/* ----- Подсветка оборудования при переходе из списка "Оборудование" ----- */
.rack-block-highlighted {
    outline: 3px solid #ef4444 !important; outline-offset: -3px;
    animation: rack-block-pulse 1s ease-in-out 3;
}
@keyframes rack-block-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,.6); }
    50% { box-shadow: 0 0 0 6px rgba(239,68,68,0); }
}

/* ----- Карточка ПО: бейдж статуса лицензии ----- */
.sw-status-badge {
    display: inline-block; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
    white-space: nowrap;
}
.sw-status-badge.ok { background: #dcfce7; color: #166534; }
.sw-status-badge.warn { background: #fef9c3; color: #854d0e; }
.sw-status-badge.bad { background: #fee2e2; color: #991b1b; }
.sw-status-badge.neutral { background: #e2e8f0; color: #475569; }

/* ----- Контекстное меню (правый клик по строке списка) ----- */
.context-menu {
    position: absolute; z-index: 1000; background: var(--color-surface); border: 1px solid var(--color-border);
    border-radius: var(--radius); box-shadow: 0 6px 20px rgba(0,0,0,.15); padding: 4px; min-width: 180px;
}
.context-menu-item {
    display: block; width: 100%; text-align: left; padding: 8px 12px; border: none; background: none;
    font-size: 13px; border-radius: 4px; cursor: pointer; color: var(--color-text);
}
.context-menu-item:hover { background: var(--color-primary-light, #eff6ff); }
.context-menu-item.danger { color: #dc2626; }
.context-menu-item.danger:hover { background: #fee2e2; }
.context-menu-sep { height: 1px; background: var(--color-border); margin: 4px 2px; }

/* ----- Иконка «Помощь» (?) в шапке ----- */
/* Полупрозрачная белая заливка + более плотная обводка — вместо простого
   контура currentColor. Акцентный цвет шапки настраивается пользователем
   (темы оформления), и на части оттенков одноцветный контур сливался с
   фоном; белая полупрозрачная подложка даёт контраст независимо от того,
   какой конкретно цвет/градиент выбран. */
.help-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; border-radius: 50%;
    background: rgba(255,255,255,.25); border: 1.5px solid rgba(255,255,255,.85);
    color: #fff; font-size: 13px; font-weight: 700; text-decoration: none !important; margin-left: 4px;
    flex-shrink: 0; box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
.help-icon:hover { background: rgba(255,255,255,.4); border-color: #fff; }

/* ----- Новые виджеты дашборда: круговая и столбчатые диаграммы, тренд ----- */
.donut-chart-wrap { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.donut-chart-wrap svg { flex-shrink: 0; }
.donut-chart-wrap .capacity-chart-legend { font-size: 12px; gap: 3px; padding-top: 0; }
/* Сегменты доната ("Оборудование по типам") дорисовываются при загрузке —
   JS (main.js, PAGE==='dashboard') переключает stroke-dasharray с "0 C" на
   финальное значение вскоре после первой отрисовки, эта transition и
   отыгрывает анимацию. Hover увеличивает конкретный сегмент относительно
   его собственного центра (transform-origin выставлен инлайново в PHP). */
.donut-seg {
    transition: stroke-dasharray .9s cubic-bezier(.22, 1, .36, 1), transform .2s ease;
    cursor: pointer;
}
.donut-seg:hover { transform: scale(1.08); }
.bar-chart-row { display: grid; grid-template-columns: 190px 1fr 38px; align-items: center; gap: 8px; margin-bottom: 4px; }
.bar-chart-label { font-size: 11.5px; line-height: 1.25; overflow: visible; white-space: normal; word-break: break-word; display: flex; align-items: center; gap: 6px; }
.bar-chart-label-text { min-width: 0; }
/* Подпись зала на дашборде ("Занятость и мощность по залам") — ссылка на
   план зала (см. svgBarChart() в dashboard.php); подчёркиваем только по
   ховеру, чтобы не выглядело так, будто вся строка — ссылка. */
.bar-chart-label a:hover { text-decoration: underline; }
/* Мини-силуэт стойки-термометра рядом с названием зала (только в графике
   занятости юнитов) — заливка "поднимается" от 0 до текущего % при
   загрузке, теми же final-y/final-height, которые проставляет JS. */
.mini-rack-icon { flex-shrink: 0; }
/* Заливка рендерится сразу в финальном размере (см. miniRackIcon() в
   dashboard.php) и "въезжает" через transform:scaleY() — а не через
   анимацию y/height, у которых недостаточно широкая поддержка как
   CSS-свойств для SVG (в отличие от transform). transform-origin
   выставлен инлайново в PHP на нижнюю границу иконки, так что scaleY
   растёт снизу вверх, как термометр. */
.mini-rack-fill {
    transform: scaleY(0);
    transition: transform .9s cubic-bezier(.22, 1, .36, 1);
}
.mini-rack-fill.mini-rack-fill-visible { transform: scaleY(1); }
.bar-chart-track { background: var(--color-bg); border-radius: 3px; height: 9px; overflow: hidden; }
.bar-chart-fill { height: 100%; border-radius: 3px; transition: width .3s; }
/* Критичное значение (≥90%, порог совпадает с красным цветом заливки) —
   мягкая "дышащая" пульсация свечения, чтобы взгляд сам зацепился за
   проблемный зал без необходимости вчитываться в цифры. */
@keyframes bar-critical-breathe {
    0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, .45); }
    50% { box-shadow: 0 0 6px 2px rgba(220, 38, 38, .55); }
}
.bar-chart-fill.bar-critical { animation: bar-critical-breathe 1.8s ease-in-out infinite; }
.bar-chart-value { font-size: 11px; text-align: right; color: var(--color-muted); }
.dash-chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 900px) { .dash-chart-grid { grid-template-columns: 1fr; } }
.chart-col-caption { font-size: 11px; margin: 0 0 6px; }

/* "Живой" пульс-индикатор рядом с заголовком графика реального времени
   (тренд мощности PDU) — чисто CSS, без JS: точка + расходящееся кольцо
   (sonar-эффект), сигнализирует, что данные периодически обновляются. */
.live-pulse { position: relative; display: inline-flex; width: 10px; height: 10px; flex-shrink: 0; }
.live-pulse-dot, .live-pulse-ring {
    position: absolute; inset: 0; margin: auto; width: 8px; height: 8px;
    border-radius: 50%; background: #16a34a;
}
.live-pulse-ring { animation: live-pulse-ripple 2s ease-out infinite; }
@keyframes live-pulse-ripple {
    0% { transform: scale(1); opacity: .6; }
    100% { transform: scale(2.8); opacity: 0; }
}

/* Критичный прогноз заполнения стоек (осталось ≤2 мес.) — тот же приём
   "дыхания", что и у bar-critical выше, но фоном блока, а не свечением. */
@keyframes forecast-critical-breathe {
    0%, 100% { background-color: transparent; }
    50% { background-color: rgba(220, 38, 38, .08); }
}
.forecast-critical { animation: forecast-critical-breathe 2.2s ease-in-out infinite; border-radius: 4px; padding: 4px 6px; margin: -4px -6px 8px; }

/* Бейдж "уже истекло: N" в виджете лицензий ПО — та же логика "критичной
   пульсации", но через opacity (проще для инлайнового текста). */
@keyframes expiry-badge-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.expiry-critical-badge { animation: expiry-badge-pulse 1.6s ease-in-out infinite; display: inline-block; }

/* "Карта ЦОД" — плавающая кнопка (правый край экрана) + модалка со
   стилизованными зданиями по ЦОД, залы внутри анимированно "проявляются"
   при открытии (см. renderDcMapModal() в dashboard.php). */
.dash-dcmap-fab {
    position: fixed; right: 18px; top: 50%; transform: translateY(-50%);
    z-index: 40; display: flex; align-items: center; gap: 8px;
    background: var(--color-primary); color: #fff; border: none; border-radius: 999px;
    padding: 12px 16px; box-shadow: 0 6px 18px -6px rgba(0, 0, 0, .4); cursor: pointer;
    font-size: 13px; font-weight: 600;
}
.dash-dcmap-fab:hover {
    /* Явно повторяем background: без этого общее правило "button:hover {
       background: var(--color-bg); }" (см. выше по файлу) перебивает его
       по специфичности для свойства background конкретно на hover — кнопка
       на любой кастомной теме пользователя "белеет" (сливается с фоном
       страницы), а белые иконка/подпись пропадают. Именно это и было
       багом в репорте. Плюс НЕ используем filter:brightness(>1) — от
       осветления даже правильный фон уезжает к белому на светлых темах;
       затемнение безопасно для любого цвета. */
    background: var(--color-primary);
    filter: brightness(.9);
    box-shadow: 0 8px 22px -6px rgba(0, 0, 0, .5);
    transform: translateY(-50%) scale(1.05);
}
.dash-dcmap-fab-icon { font-size: 18px; line-height: 1; }
/* "Скан стойки" на дашборде — та же плавающая кнопка (позиция/цвета/
   мобильное сворачивание в кружок наследуются через общий класс
   .dash-dcmap-fab, см. renderDcMapModal()/dashboard.php), только сдвинута
   вниз, чтобы не перекрываться с "Картой ЦОД" над ней. 56px — высота самой
   кнопки (12px паддинг сверху/снизу + строка текста 13px + запас) плюс
   небольшой зазор, тот же порядок величины, что и остальные fixed-offset
   в этом файле. */
.dash-rackscan-fab { top: calc(50% + 56px); }
/* Только мобильные экраны — на десктопе наводить телефон/веб-камерой на QR
   стойки не имеет смысла (это фича именно "телефон в руке, идёшь по
   залу"), лишняя кнопка на десктопе только шумит. Тот же брейкпоинт
   (900px), что и у сворачивания "Карты ЦОД" в кружок чуть ниже — тут он
   означает не "сжать", а "не показывать вовсе". !important — кнопка
   изначально скрыта инлайн-стилем в dashboard.php и снимается им же через
   JS (rackScanBtn.style.display = '') только после успешного
   getUserMedia-фиче-детекта; на десктопе тот фиче-детект тоже пройдёт
   (у ноутбука почти всегда есть веб-камера), так что одного фиче-детекта
   недостаточно — нужно явно перебить его именно на широких экранах. */
@media (min-width: 901px) {
    .dash-rackscan-fab { display: none !important; }
}
@media (max-width: 900px) {
    /* На узких экранах кнопка рискует перекрывать контент/мобильное меню —
       сворачиваем в компактный кружок без подписи. */
    .dash-dcmap-fab-label { display: none; }
    .dash-dcmap-fab { padding: 12px; border-radius: 50%; right: 10px; }
}
.dcmap-modal-box { max-width: 820px; }
.dcmap-intro { font-size: 12px; margin: -4px 0 14px; }
/* Здания выстроены в ряд и выровнены по нижнему краю (align-items:
   flex-end) — при разной этажности получается силуэт мини-города, а не
   плоский список карточек. Переносятся на новую строку, если не влезают. */
.dcmap-buildings {
    display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: center;
    gap: 22px 18px; max-height: 62vh; overflow-y: auto; padding: 4px 4px 10px;
}
/* Здание "в профиль" (фасад, вид сбоку/спереди, не изометрия): крыша
   сверху, табличка с названием ЦОД, дальше этажи снизу вверх — первый
   зал списка становится первым (нижним) этажом за счёт
   flex-direction: column-reverse на .dcmap-floors при сохранении
   исходного порядка DOM-узлов, — и дверь у земли. */
.dcmap-skyline-building {
    width: 210px; display: flex; flex-direction: column;
    border: 1px solid var(--color-border); border-radius: 8px 8px 3px 3px;
    overflow: hidden; background: linear-gradient(180deg, #eef2f7, #dde5ef);
    box-shadow: 0 3px 0 0 rgba(15, 23, 42, .12);
}
.dcmap-building-roof {
    height: 12px; margin: 0 10px;
    background: repeating-linear-gradient(135deg, #94a3b8 0, #94a3b8 8px, #64748b 8px, #64748b 16px);
    clip-path: polygon(0 100%, 8% 0, 92% 0, 100% 100%);
}
.dcmap-building-sign {
    margin: 8px auto 6px; padding: 3px 10px; font-weight: 600; font-size: 12px;
    color: #1e293b; background: rgba(255, 255, 255, .55); border-radius: 999px;
    text-align: center; max-width: calc(100% - 20px);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dcmap-floors { display: flex; flex-direction: column-reverse; gap: 6px; padding: 4px 10px 10px; flex: 1; }
.dcmap-floor { display: flex; align-items: stretch; gap: 5px; }
/* Декоративные "окна" по бокам этажа — чисто оформление фасада, к залу
   отношения не имеют. */
.dcmap-window {
    width: 12px; flex-shrink: 0; border-radius: 2px;
    background: linear-gradient(180deg, #bcd4f0, #8fb4e0);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5);
}
.dcmap-hall {
    flex: 1; display: flex; align-items: center; justify-content: space-between; gap: 6px;
    padding: 10px; border-radius: 6px; text-decoration: none; color: #fff; font-size: 12px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .25);
    position: relative;
    /* Итоговое видимое состояние — базовое (не opacity:0!). Вход
       "с нуля" даёт сама entrance-анимация (dcmap-hall-in, from {opacity:0}
       + fill-mode both, см. .dcmap-modal-opening .dcmap-hall ниже) в момент
       открытия модалки. Если сделать opacity:0/transform базовыми
       свойствами, а не результатом анимации, то смена анимации на hover
       (dcmap-hall-pulse не трогает opacity/transform) сбрасывает элемент
       обратно к этим базовым — невидимым — значениям, и карточка "гаснет"
       при наведении. Так уже наступали на грабли с mini-rack-fill — тот же
       класс ошибки. */
    opacity: 1; transform: none;
}
.dcmap-hall-name { font-weight: 600; word-break: break-word; }
.dcmap-hall-percent { font-size: 11px; opacity: .9; flex-shrink: 0; }
.dcmap-hall-good { background: #16a34a; }
.dcmap-hall-warn { background: #d97706; }
.dcmap-hall-bad { background: #dc2626; }
/* Наведение — пульсация свечения вместо простого brightness(): одинаково
   заметна и читаема на всех 3 статусных цветах (в т.ч. на светлом
   warn-оранжевом, где brightness(>1) почти сливался бы с фоном). */
@keyframes dcmap-hall-pulse {
    0%, 100% { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5), 0 0 0 0 rgba(255, 255, 255, .55); }
    50% { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .7), 0 0 14px 4px rgba(255, 255, 255, .55); }
}
/* Специфичность дублирована с ".dcmap-modal-opening .dcmap-hall" не
   случайно: класс dcmap-modal-opening висит на оверлее весь срок, пока
   модалка открыта (не только в момент входной анимации), а specificity
   у него та же, что у простого ":hover" — без явного дублирования
   контекста hover проигрывал бы entrance-правилу по порядку в файле и
   пульсация никогда бы не была видна. */
.dcmap-hall:hover,
.dcmap-modal-opening .dcmap-hall:hover {
    animation: dcmap-hall-pulse 1s ease-in-out infinite;
    z-index: 1;
}
.dcmap-building-door {
    width: 36px; height: 20px; margin: 0 auto; border-radius: 4px 4px 0 0;
    background: linear-gradient(180deg, #475569, #334155);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .15);
}
@keyframes dcmap-hall-in {
    from { opacity: 0; transform: translateX(-16px) scale(.92); }
    to { opacity: 1; transform: translateX(0) scale(1); }
}
.dcmap-modal-opening .dcmap-hall { animation: dcmap-hall-in .4s cubic-bezier(.22, 1, .36, 1) both; }
/* Открытие/закрытие самой модалки — тот же приём и те же @keyframes
   (hlp-modal-backdrop-in/hlp-modal-box-in), что и у модалки предпросмотра
   стойки на плане зала (см. #hlp-rack-info-modal ниже), просто под своим
   ID — общие keyframes, не задействуем классы того модуля напрямую. */
#dash-dcmap-modal.dcmap-modal-opening { animation: hlp-modal-backdrop-in .18s ease both; }
#dash-dcmap-modal.dcmap-modal-opening .modal { animation: hlp-modal-box-in .22s ease both; }
#dash-dcmap-modal.dcmap-modal-closing { animation: hlp-modal-backdrop-in .15s ease reverse both; }
#dash-dcmap-modal.dcmap-modal-closing .modal { animation: hlp-modal-box-in .15s ease reverse both; }

@media (prefers-reduced-motion: reduce) {
    .donut-seg, .mini-rack-fill, .live-pulse-ring, .bar-chart-fill.bar-critical,
    .forecast-critical, .expiry-critical-badge, .card {
        animation: none !important;
        transition: none !important;
    }
    .dcmap-hall, .dcmap-hall:hover { animation: none !important; opacity: 1 !important; transform: none !important; }
    #dash-dcmap-modal.dcmap-modal-opening,
    #dash-dcmap-modal.dcmap-modal-opening .modal,
    #dash-dcmap-modal.dcmap-modal-closing,
    #dash-dcmap-modal.dcmap-modal-closing .modal { animation: none; }
}

/* ----- Настройки: правила полноты данных по принадлежности/типу ----- */
.completeness-rules-table th, .completeness-rules-table td { padding: 6px 8px; font-size: 12px; white-space: nowrap; }
.completeness-rules-table th:first-child, .completeness-rules-table td:first-child { text-align: left; white-space: normal; min-width: 120px; }

/* ----- Бейдж blade-шасси на схеме стойки + всплывающий список серверов ----- */
.rack-blade-slots {
    position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: row;
    border-radius: inherit; overflow: hidden;
}
.rack-blade-slot {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
    text-decoration: none; color: #0f172a; font-size: 9px; font-weight: 800; line-height: 1.15;
    background: rgba(255,255,255,.32); border-right: 1px solid rgba(0,0,0,.15);
    overflow: hidden; padding: 1px 2px; text-shadow: 0 0 3px rgba(255,255,255,.85); text-align: center;
}
.rack-blade-slot:last-child { border-right: none; }
.rack-blade-slot .bs-inv { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.rack-blade-slot .bs-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; font-weight: 600; font-size: 8px; opacity: .85; }
.rack-blade-slot:hover { background: rgba(255,255,255,.55); }
.rack-blade-slot.rack-blade-slot-inactive { background: rgba(148,163,184,.4); color: #1e293b; }
.rack-blade-slot.rack-blade-slot-highlighted { background: #fef08a !important; animation: dc-blade-pulse 1s ease-in-out infinite; }
@keyframes dc-blade-pulse { 0%, 100% { box-shadow: inset 0 0 0 2px #f59e0b; } 50% { box-shadow: inset 0 0 0 2px transparent; } }
.rack-blade-badge {
    position: absolute; top: 2px; right: 22px; z-index: 2;
    background: #7c3aed; color: #fff; border: none; border-radius: 10px;
    font-size: 10px; font-weight: 700; padding: 1px 7px; cursor: pointer; line-height: 1.4;
}
.rack-blade-badge:hover { background: #6d28d9; }
.rack-blade-popover {
    position: absolute; z-index: 1000; background: var(--color-surface); border: 1px solid var(--color-border);
    border-radius: var(--radius); box-shadow: 0 6px 20px rgba(0,0,0,.18); padding: 6px; min-width: 220px; max-width: 320px;
    max-height: 320px; overflow-y: auto;
}
.rack-blade-popover-title { font-size: 11px; font-weight: 700; color: var(--color-muted); padding: 4px 8px; }
.rack-blade-popover-item {
    display: block; padding: 6px 8px; border-radius: 4px; text-decoration: none; color: var(--color-text); font-size: 12px;
}
.rack-blade-popover-item:hover { background: var(--color-bg); }

/* ----- Печать развёрнутых схем стоек по ЦОД/залам ----- */
.print-letterhead { display: flex; align-items: center; gap: 14px; border-bottom: 2px solid #1e293b; padding-bottom: 10px; margin-bottom: 10px; }
.print-letterhead-logo { height: 44px; width: auto; }
.print-letterhead-title { font-size: 16px; }
.print-racks-grid { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 20px; }
.print-rack-card {
    border: 1px solid var(--color-border); border-radius: 6px; padding: 10px; width: 220px;
    break-inside: avoid; page-break-inside: avoid; overflow: hidden;
}
.print-rack-card h3 { margin: 0 0 2px; font-size: 13px; }
.print-rack-table { width: 100%; max-width: 100%; border-collapse: collapse; font-size: 9.5px; table-layout: fixed; }
.print-rack-table td {
    border: 1px solid #cbd5e1; padding: 2px 3px; overflow: hidden; word-break: break-word;
    vertical-align: top;
}
.print-rack-u { width: 20px; text-align: center; color: #64748b; background: #f1f5f9; vertical-align: middle; }
.print-rack-empty { background: #fff; }
.print-rack-eq { background: #dbeafe; font-weight: 600; white-space: normal; line-height: 1.25; }
@media print {
    .print-rack-card { border-color: #000; }
    .print-rack-table td { border-color: #000; }
}

/* ----- Единый поиск в шапке ----- */
.app-global-search { flex: 0 1 220px; min-width: 120px; margin: 0 12px; }
.app-global-search input {
    width: 100%; padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,.4);
    background: rgba(255,255,255,.15); color: #fff; font-size: 13px;
}
.app-global-search input::placeholder { color: rgba(255,255,255,.75); }
.app-global-search input:focus { outline: none; background: #fff; color: #1f2937; }
#global-search-results {
    position: absolute; top: 100%; left: 0; right: 0; margin-top: 4px; background: var(--color-surface);
    border: 1px solid var(--color-border); border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0,0,0,.18);
    max-height: 420px; overflow-y: auto; z-index: 1000; color: var(--color-text); text-align: left;
}
.gs-group-title { font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--color-muted); padding: 8px 10px 2px; }
.gs-item { display: block; padding: 6px 10px; text-decoration: none; color: var(--color-text); font-size: 13px; }
.gs-item:hover { background: var(--color-bg); }
.gs-item .gs-sub { color: var(--color-muted); font-size: 11px; }
.gs-empty { padding: 10px; color: var(--color-muted); font-size: 13px; }

/* ----- Массовое редактирование оборудования ----- */
.bulk-edit-row { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.bulk-edit-row .checkbox-label { flex: 0 0 220px; }
.bulk-edit-row select, .bulk-edit-row input[type="text"] { flex: 1; }

/* ----- Массовый выбор галочками (БП: оборудование/ПО, Элемент услуги: ресурсы) ----- */
.bulk-picker { margin-top: 10px; border: 1px dashed var(--color-border); border-radius: 6px; padding: 8px 12px; }
.bulk-picker summary { cursor: pointer; font-size: 13px; font-weight: 600; }
.bulk-picker-search { width: 100%; margin: 8px 0; padding: 7px 10px; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 13px; }
.bulk-picker-list { max-height: 220px; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 6px; }
.bulk-picker-row { display: flex; align-items: center; gap: 10px; padding: 6px 10px; font-size: 12.5px; border-bottom: 1px solid var(--color-border); }
.bulk-picker-row:last-child { border-bottom: none; }
.bulk-picker-row:hover { background: var(--color-bg); }
.bulk-picker-row .bp-title { font-weight: 600; }
.bulk-picker-row .bp-sub { color: var(--color-muted); }
.bulk-picker-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; gap: 10px; }

/* ----- Рассылка клиентам: редактор форматирования и область охвата ----- */
.ml-editor-toolbar {
    display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
    border: 1px solid var(--color-border); border-bottom: none; border-radius: 6px 6px 0 0;
    padding: 6px 8px; background: var(--color-bg);
}
.ml-editor-toolbar button {
    min-width: 30px; padding: 4px 8px; border: 1px solid transparent; border-radius: 4px;
    background: none; cursor: pointer; font-size: 13px;
}
.ml-editor-toolbar button:hover { background: var(--color-surface); border-color: var(--color-border); }
.ml-toolbar-sep { width: 1px; height: 20px; background: var(--color-border); margin: 0 4px; }
.ml-editor-body {
    min-height: 180px; max-height: 400px; overflow-y: auto; border: 1px solid var(--color-border);
    border-radius: 0 0 6px 6px; padding: 12px 14px; font-size: 14px; line-height: 1.5;
}
.ml-editor-body:focus { outline: none; border-color: var(--color-primary); }
.ml-editor-body ul, .ml-editor-body ol { padding-left: 22px; margin: 8px 0; }
.ml-editor-body h3 { margin: 10px 0 6px; font-size: 16px; }
.ml-editor-body p { margin: 0 0 8px; }
.ml-editor-body a { color: var(--color-primary); }

.ml-scope-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 8px; }
.ml-scope-list { max-height: 220px; overflow-y: auto; border: 1px solid var(--color-border); border-radius: 6px; padding: 8px 10px; margin-top: 4px; font-size: 13px; }

.ml-recipient-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: 13px; border-bottom: 1px solid var(--color-border); }
.ml-recipient-row:last-child { border-bottom: none; }
.ml-recipient-row.ml-no-email { color: var(--color-muted); }

/* ----- Менеджер IP-сетей: визуальная сетка адресов подсети ----- */
.ip-grid {
    display: flex; flex-wrap: wrap; gap: 3px; margin-top: 8px;
    max-height: 520px; overflow-y: auto; padding: 4px;
    border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-bg);
}
.ip-cell {
    width: 46px; height: 30px; flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    font-size: 10.5px; border-radius: 4px; cursor: pointer;
    background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text);
    overflow: hidden; white-space: nowrap;
}
.ip-cell:hover { box-shadow: 0 0 0 2px var(--color-primary) inset; }
/* В визуальной нарезке (подбор маски вложенной подсети) занятые блоки не
   выбираются — в отличие от обычной сетки подсети, где по занятой ячейке
   можно кликнуть для просмотра/изменения записи. */
#ipv-child-mask-grid .ip-cell-assigned { cursor: not-allowed; opacity: .65; }
#ipv-child-mask-grid .ip-cell-assigned:hover { box-shadow: none; }
.ip-cell-assigned { background: var(--color-occupied-server); border-color: transparent; }
.ip-cell-reserved { background: var(--color-occupied-network); border-color: transparent; }
.ip-cell-legend {
    display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin: 0 2px -1px 6px;
    border: 1px solid var(--color-border);
}
.ip-cell-legend.ip-cell-free { background: var(--color-surface); }
.ip-cell-legend.ip-cell-assigned { background: var(--color-occupied-server); border-color: transparent; }
.ip-cell-legend.ip-cell-reserved { background: var(--color-occupied-network); border-color: transparent; }

/* Табличный (excel-подобный) вид подсети — та же цветовая логика, что и у
   сетки, но лёгкой заливкой строки, чтобы занятость было видно с первого
   взгляда даже в обычной таблице. */
.ipv-row-assigned { background: #eff6ff; }
.ipv-row-reserved { background: #fffbeb; }
.ipv-row-assigned:hover, .ipv-row-reserved:hover { filter: brightness(0.97); }

/* Мигающая подсветка строки/ячейки адреса при переходе по ссылке из
   "NAT / Проксирование" (open_address) — вместо открытия редактирования
   просто указываем пользователю, где именно этот адрес расположен. */
@keyframes ipv-flash {
    0%, 100% { background-color: #fef08a; }
    50% { background-color: transparent; }
}
tr.ipv-flash-highlight,
.ip-cell.ipv-flash-highlight {
    animation: ipv-flash 0.8s ease-in-out 4;
    outline: 2px solid #f59e0b;
    outline-offset: -2px;
}

/* Разделитель между блоками адресов, "нарезанными" в отдельную вложенную
   подсеть — виден прямо в общей таблице родительской подсети. */
.ipv-child-divider td {
    background: var(--color-bg); font-size: 12px; font-weight: 600;
    border-top: 2px solid var(--color-primary); border-bottom: 1px solid var(--color-border);
    padding: 6px 12px;
}
.ipv-child-divider a { font-weight: 600; }

/* Нижняя граница блока вложенной подсети в табличном виде — последняя строка
   блока (broadcast-адрес/конец диапазона) перед возвратом к адресам родителя
   или началом следующей вложенной подсети. Симметрично верхнему разделителю
   (.ipv-child-divider), чтобы весь блок был визуально обведён. */
.ipv-child-block-end td { border-bottom: 2px solid var(--color-primary); }

/* Ячейка сетки, с которой начинается блок вложенной подсети — толстая левая
   граница как визуальный маркер (аналог разделителя в табличном виде). */
.ip-cell-child-start { box-shadow: -2px 0 0 var(--color-primary); }
/* Последняя ячейка блока вложенной подсети — правая граница, симметрично левой. */
.ip-cell-child-end { box-shadow: 2px 0 0 var(--color-primary); }
.ip-cell-child-start.ip-cell-child-end { box-shadow: -2px 0 0 var(--color-primary), 2px 0 0 var(--color-primary); }

/* Drag-select диапазона адресов — подсветка выбираемых ячеек/строк и запрет
   выделения текста мышью во время протягивания (иначе браузер выделяет
   текст ячеек вместо диапазона адресов). */
.ip-cell.ipv-drag-selected { outline: 2px solid var(--color-primary); outline-offset: -2px; }
tr.ipv-drag-selected td { background: #dbeafe !important; }
body.ipv-dragging { user-select: none; }
#ipv-table tbody tr[data-idx] { cursor: pointer; }

/* Карточки разделов на входном экране "IP-сети" — раздел → список подсетей
   внутри него; отдельный, более "спокойный" стиль, чем перетаскиваемые
   карточки дашборда (.card), т.к. тут просто кликабельная папка. */
.ipn-section-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; margin-top: 12px; }
.ipn-section-card {
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    padding: 14px 16px; cursor: pointer; transition: border-color .15s, box-shadow .15s;
}
.ipn-section-card:hover { border-color: var(--color-primary); box-shadow: 0 0 0 1px var(--color-primary); }
.ipn-section-card-name { font-weight: 600; font-size: 15px; }
.ipn-section-card-desc { font-size: 12px; color: var(--color-muted); margin-top: 4px; }
.ipn-section-card-count { font-size: 12px; color: var(--color-muted); margin-top: 8px; }
.ipn-section-card-all { border-style: dashed; }

/* ----- План зала (drag-drop схема расположения стоек) ----- */
.hall-layout-toolbar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 16px;
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    padding: 8px 12px; margin-bottom: 12px;
}
.hall-layout-toolbar-group { display: flex; align-items: center; gap: 6px; }
.hall-layout-legend { margin-left: auto; gap: 14px; font-size: 12px; color: var(--color-muted); }
.hlp-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 4px; vertical-align: -1px; }
.hlp-swatch-ok { background: var(--color-success); }
.hlp-swatch-warn { background: #cbd5e1; }
.hlp-swatch-bad { background: var(--color-danger); }

.hall-layout-body { display: flex; gap: 16px; align-items: flex-start; }
.hall-layout-sidebar {
    width: 220px; flex-shrink: 0; background: var(--color-surface); border: 1px solid var(--color-border);
    border-radius: var(--radius); padding: 12px; max-height: 78vh; overflow-y: auto;
}
.hlp-unplaced-list { display: flex; flex-direction: column; gap: 6px; }
.hlp-unplaced-item {
    border: 1px solid var(--color-border); border-radius: var(--radius); padding: 6px 8px;
    font-size: 12px; cursor: grab; background: var(--color-bg);
}
.hlp-unplaced-item:hover { border-color: var(--color-primary); }
.hlp-unplaced-item strong { display: block; font-size: 13px; }

.hall-layout-canvas-wrap {
    flex: 1; min-width: 0; max-height: 78vh; overflow: auto;
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
}
.hall-layout-canvas {
    position: relative;
    background-image:
        linear-gradient(to right, var(--color-border) 1px, transparent 1px),
        linear-gradient(to bottom, var(--color-border) 1px, transparent 1px);
    background-color: #fafbfc;
}
.hall-layout-canvas.hlp-drag-over-valid { outline: 2px dashed var(--color-primary); outline-offset: -2px; }

/* Изометрический 3D-вид плана (read-only) — переключается кнопкой
   "🧊 3D-вид", строится из тех же racks/objects, что и обычный 2D-канвас,
   см. renderIso() в hall-layout.js. Обёртка стилистически повторяет
   .hall-layout-canvas-wrap (та же рамка/фон/скролл), просто показывает SVG
   вместо HTML-грида. */
.hall-layout-iso-wrap {
    flex: 1; min-width: 0; max-height: 78vh; overflow: auto;
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    padding: 10px;
}
.hall-layout-iso-canvas svg { display: block; }
.hlp-iso-face { stroke: rgba(0,0,0,.18); stroke-width: 1; stroke-linejoin: round; }
/* Наведение подсвечивает все 3 грани стойки разом (группа .hlp-iso-rack),
   а не отдельную грань под курсором — иначе "мигает" только одна плоскость
   при движении мыши по объёму, что читается хуже, чем цельная подсветка. */
.hlp-iso-rack { cursor: pointer; }
.hlp-iso-rack:hover .hlp-iso-face { filter: brightness(1.12); }
.hlp-iso-rack-label { font-size: 10px; font-weight: 700; fill: rgba(15,23,42,.85); pointer-events: none; }
.hlp-iso-rack-sub { font-size: 9px; fill: rgba(15,23,42,.6); pointer-events: none; }
.hlp-iso-object-label { font-size: 10px; fill: var(--color-muted); pointer-events: none; }
.hlp-iso-object-icon { font-size: 9px; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
    .hlp-iso-rack:hover .hlp-iso-face { filter: none; }
}

/* "Полное" 3D (read-only, WebGL) — переключается кнопкой "🎮 Полное 3D",
   см. public/js/hall-layout-3d.js (рендерер) + renderFull3d() в
   hall-layout.js (данные). Та же рамка/фон, что у 2D-плана и изометрии, но
   без внутреннего скролла — камера здесь "гуляет" сама, а не холст. */
.hall-layout-full3d-wrap {
    flex: 1; min-width: 0; max-height: 78vh;
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius);
    padding: 10px; display: flex; flex-direction: column; gap: 6px;
}
.hlp-full3d-stage {
    /* Отдельная позиционирующая обёртка вокруг канвы — сама канва больше не
       flex-элемент напрямую, т.к. поверх неё абсолютно позиционируются
       кнопка тура и D-pad панорамы (см. ниже); без этой обёртки они бы
       позиционировались относительно всего hall-layout-full3d-wrap. */
    position: relative; flex: 1; min-height: 420px;
}
.hall-layout-full3d-canvas {
    position: absolute; inset: 0; width: 100%; height: 100%; display: block;
    border-radius: calc(var(--radius) - 2px); touch-action: none; /* touch-action:none — иначе браузер сам пытается скроллить/зумить страницу вместо вращения камеры одним пальцем */
}
.hall-layout-full3d-hint { font-size: 11px; margin: 0; text-align: center; }
.hall-layout-full3d-unsupported { font-size: 12px; margin: 40px auto; max-width: 320px; text-align: center; }
/* Подсказка AR-режима "наведите на пол и коснитесь экрана" (одиннадцатый
   раунд) — плавающая плашка внизу-по-центру сцены, поверх реального
   видеопотока камеры во время AR-сессии (см. onArChange/
   onArPlacementChange в hall-layout.js — видна только пока сессия открыта
   И зал ещё не "поставлен"). Тот же стиль полупрозрачной тёмной плашки,
   что и у минимапы (.hlp-full3d-minimap), для визуальной согласованности
   оверлеев поверх канвы. */
.hlp-full3d-ar-hint {
    position: absolute; left: 50%; bottom: 16px; z-index: 2;
    transform: translateX(-50%);
    margin: 0; padding: 8px 14px; max-width: calc(100% - 32px);
    background: rgba(15, 18, 24, 0.78); color: #fff;
    font-size: 12px; text-align: center; border-radius: 8px;
    pointer-events: none;
}

/* ----- AR-скан стойки по QR (тринадцатый раунд) -----
   Полноэкранный оверлей поверх ВСЕЙ страницы (fixed, не привязан к плану
   зала под ним) с видео камеры на весь экран, рамкой-прицелом по центру
   и — после распознавания — плашкой результата снизу, не перекрывающей
   видео целиком (см. #hlp-rackscan-panel ниже: max-height + собственный
   скролл, а не "во весь экран"). Тот же язык полупрозрачных тёмных
   плашек, что и у .hlp-full3d-ar-hint выше — визуальная согласованность
   с уже существующим AR-режимом полного 3D, хотя технически это два
   независимых механизма (тут — обычная camera+jsQR, не WebXR). */
.hlp-rackscan-overlay {
    position: fixed; inset: 0; z-index: 1000;
    background: #000;
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
}
.hlp-rackscan-overlay video {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; /* заполняем весь экран кадром камеры, а не вписываем с полосами */
}
/* Рамка-прицел — чисто визуальная подсказка "куда навести", сама по себе
   не участвует в распознавании (jsQR сканирует весь кадр целиком, не
   только эту область) — как и у .hlp-full3d-ar-hint, pointer-events:none,
   чтобы не мешать никаким кликам под ней. */
.hlp-rackscan-frame {
    position: absolute; top: 50%; left: 50%;
    width: min(64vw, 320px); height: min(64vw, 320px);
    transform: translate(-50%, -50%);
    border: 3px solid rgba(255, 255, 255, 0.85);
    border-radius: 16px;
    box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.35); /* затемнение всего, что вне рамки */
    pointer-events: none;
}
/* AR-стикер 3D-стойки (тринадцатый раунд, см. подробный комментарий у
   #hlp-rackscan-3d в hall_layout.php и у computeQrStickerTransform() в
   hall-layout.js). CSS-размер (width/height) на самом деле выставляется
   ИНЛАЙН из JS (RACKSCAN_3D_BASE_W/H в hall-layout.js) — единый источник
   истины с формулой масштаба, здесь только позиционирование/поведение,
   не геометрия. left:0;top:0 + transform-origin:center — вместе с
   translate(calc(...% )) в JS позволяют позиционировать/масштабировать/
   вращать canvas вокруг ЕГО ЖЕ центра одной строкой transform, не трогая
   layout (см. updateStickerTransform()). pointer-events:none — стикер
   декоративный поверх видео, не должен перехватывать ни клик по видео
   (закрывает сканер), ни клик по крестику/панели результата поверх него. */
.hlp-rackscan-3d {
    position: absolute; left: 0; top: 0;
    transform-origin: center center;
    pointer-events: none;
    z-index: 1; /* над видео (z-index:auto), под диагностикой/крестиком/панелью результата (z-index:2 у них) */
    opacity: 0;
    transition: opacity 0.35s ease;
    will-change: transform;
}
.hlp-rackscan-3d.is-visible { opacity: 1; }
.hlp-rackscan-close {
    position: absolute; top: 12px; right: 12px; z-index: 2;
    width: 36px; height: 36px; font-size: 18px;
    background: rgba(15, 18, 24, 0.6); color: #fff; border-color: rgba(255,255,255,0.35);
}
/* Пауза автовращения 3D-стикера — раньше сидела наверху рядом с крестиком
   закрытия (36×36px), что на мобильном телефоне оказалось: (а) физически
   неудобно дотянуться большим пальцем до самого верха экрана, и (б) сама
   кнопка была мельче рекомендуемого минимума touch-target (~44px), из-за
   чего промах пальцем чаще всего попадал по video ПОД кнопкой — а клик по
   video закрывает весь сканер (см. rackScanVideo.addEventListener('click',
   closeRackScanner) в hall-layout.js), что читалось как "тапнул паузу —
   меня выкинуло на 2D план". Перенесена в правую часть экрана на уровень
   чуть ВЫШЕ максимальной высоты плашки результата (55vh, см.
   .hlp-rackscan-panel) — не перекрывается панелью ни при каком её реальном
   размере, и физически в зоне, которую большой палец достаёт без
   перехвата телефона (в отличие от самого верха). Размер увеличен до
   52×52px — сама кнопка, а не только её иконка, теперь не мельче
   рекомендуемого минимума. */
/* Селектор с двумя классами (а не просто .hlp-rackscan-pause) — размер
   должен побеждать .hlp-full3d-icon-btn (30×30px) ниже по каскаду
   независимо от порядка правил в файле: тот класс тоже висит на этой
   кнопке (общий базовый стиль иконки-кнопки), и при равной специфичности
   выигрывает более позднее по тексту файла правило — тут это как раз не
   то, что нужно. */
.hlp-full3d-icon-btn.hlp-rackscan-pause {
    position: absolute; bottom: calc(55vh + 14px); right: 14px; z-index: 2;
    width: 52px; height: 52px; font-size: 22px;
    background: rgba(15, 18, 24, 0.7); color: #fff; border-color: rgba(255,255,255,0.35);
}
.hlp-rackscan-pause.is-paused { background: var(--color-primary); border-color: var(--color-primary); }
/* "Заморозить" стикер (четырнадцатый раунд) — та же логика размера/
   позиционирования/причины (см. комментарий у .hlp-rackscan-pause выше),
   просто соседняя кнопка чуть выше по экрану, чтобы обе оставались в
   пределах лёгкой досягаемости большого пальца и не перекрывали друг
   друга. */
.hlp-full3d-icon-btn.hlp-rackscan-freeze {
    position: absolute; bottom: calc(55vh + 14px + 60px); right: 14px; z-index: 2;
    width: 52px; height: 52px; font-size: 22px;
    background: rgba(15, 18, 24, 0.7); color: #fff; border-color: rgba(255,255,255,0.35);
}
.hlp-rackscan-freeze.is-frozen { background: var(--color-primary); border-color: var(--color-primary); }
.hlp-rackscan-hint {
    z-index: 2; bottom: auto; top: 16px; /* над рамкой, не перекрывается плашкой результата снизу */
}
/* Диагностическая строка (счётчики кадров/распознаваний) — всегда видна
   во время скана, не только для админов (в отличие от 🐞-панели у WebXR
   AR): маленькая, чтобы не мешать, но достаточно контрастная, чтобы её
   можно было сфотографировать/переписать при жалобе "ничего не находит". */
.hlp-rackscan-diag {
    /* Прижата к верху (не к низу, где плашка результата/ошибки — иначе
       она перекрывала бы диагностику ровно тогда, когда та нужнее всего,
       например при "стойка не найдена"). */
    position: absolute; left: 0; right: 0; top: 44px; z-index: 2;
    margin: 0; padding: 4px 10px; font: 11px/1.4 monospace;
    color: #9ef29e; background: rgba(0, 0, 0, 0.6); text-align: center;
    word-break: break-all;
}
.hlp-rackscan-diag:empty { display: none; }
/* Плашка результата — распознанная стойка (через renderRackInfoBody(), см.
   hall-layout.js) или короткое сообщение об ошибке/"не QR стойки".
   Прижата к низу, не выше ~55% экрана, чтобы часть видео с рамкой
   оставалась видна для наведения на СЛЕДУЮЩУЮ стойку без закрытия панели. */
.hlp-rackscan-panel {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
    max-height: 55vh; overflow-y: auto;
    background: rgba(15, 18, 24, 0.92); color: #fff;
    padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
    border-radius: 16px 16px 0 0;
}
/* Панель переиспользует ту же renderRackInfoBody(), что и обычная модалка
   стойки (.hlp-rackinfo-*) — та вёрстка рассчитана на светлый фон модалки,
   здесь фон тёмный (поверх видео), поэтому переопределяем только текстовые
   цвета/границы, не саму структуру. */
.hlp-rackscan-panel .hlp-rackinfo-header h2,
.hlp-rackscan-panel .hlp-rackinfo-stats,
.hlp-rackscan-panel .hlp-rackinfo-sides { color: #fff; }
.hlp-rackscan-panel .muted { color: rgba(255, 255, 255, 0.65); }
.hlp-rackscan-panel .hlp-rackinfo-comment { background: rgba(255, 255, 255, 0.08); color: rgba(255,255,255,0.85); }
.hlp-rackscan-panel .hlp-rackinfo-close { color: rgba(255, 255, 255, 0.75); }
.hlp-rackscan-panel .hlp-rackinfo-close:hover { color: #fff; background: rgba(255,255,255,0.12); }
.hlp-rackscan-panel .modal-actions { justify-content: flex-start; }
.hlp-rackscan-error {
    margin: 0; font-size: 13px; color: #fca5a5;
}
/* Клик по юниту в списке — подсветка в 3D-стикере (четырнадцатый раунд,
   см. wireEquipmentHighlightClicks в hall-layout.js). Курсор pointer только
   у блоков С data-equipment-id — они кликабельны только в этой (тёмной,
   AR-стикер) панели; в обычной модалке стойки полного 3D тот же HTML
   генерируется, но клик там ничего не делает (см. комментарий у
   highlightTarget в hall-layout-3d.js), поэтому курсор/рамку туда
   намеренно не выносим — см. .hlp-rackscan-panel-префикс ниже. */
.hlp-rackscan-panel .rack-equipment-block[data-equipment-id] { cursor: pointer; }
.hlp-rackscan-panel .rack-equipment-block.is-highlighted {
    outline: 2px solid #fbbf24; outline-offset: -2px;
    box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.35);
}

/* Плавный переход между 2D/изометрией/полным 3D (setViewMode() в
   hall-layout.js) — раньше переключение было мгновенным (display:none
   в display:''). .hlp-view-fade-out играет перед тем, как элемент
   реально скрывается (display:none выставляется JS'ом уже после
   таймера, синхронизированного с длительностью этой анимации);
   .hlp-view-fade-in играет сразу после показа. */
.hlp-view-wrap.hlp-view-fade-out { animation: hlp-view-fade-out .16s ease both; }
.hlp-view-wrap.hlp-view-fade-in { animation: hlp-view-fade-in .18s ease both; }
@keyframes hlp-view-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes hlp-view-fade-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
    .hlp-view-wrap.hlp-view-fade-out, .hlp-view-wrap.hlp-view-fade-in { animation: none; }
}

/* Прицел + подсказка "на что смотрю" в режиме от первого лица
   (четырнадцатый раунд, follow-up). pointer-events:none на обоих — чисто
   информационные оверлеи, не должны перехватывать драг/клик мышью по
   канве под ними. */
.hlp-full3d-crosshair {
    position: absolute; left: 50%; top: 50%; z-index: 2;
    width: 18px; height: 18px; margin: -9px 0 0 -9px;
    pointer-events: none;
}
.hlp-full3d-crosshair::before, .hlp-full3d-crosshair::after {
    content: ''; position: absolute; background: rgba(255, 255, 255, 0.85);
    box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
}
.hlp-full3d-crosshair::before { left: 8px; top: 0; width: 2px; height: 18px; }
.hlp-full3d-crosshair::after { left: 0; top: 8px; width: 18px; height: 2px; }
.hlp-full3d-lookat {
    position: absolute; left: 50%; top: calc(50% + 22px); z-index: 2;
    transform: translateX(-50%);
    max-width: 70%; padding: 5px 12px; border-radius: 999px;
    background: rgba(15, 18, 24, 0.72); color: #fff;
    font-size: 12px; text-align: center; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
    pointer-events: none;
}

.hlp-full3d-topbar {
    position: absolute; top: 10px; right: 10px; z-index: 2;
    display: flex; align-items: center; gap: 6px;
}
.hlp-full3d-tour-btn {
    font-size: 12px; padding: 5px 10px;
}
.hlp-full3d-tour-btn.hlp-tour-active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.hlp-full3d-icon-btn {
    width: 30px; height: 30px; padding: 0; font-size: 14px; line-height: 1;
    border-radius: 6px; display: flex; align-items: center; justify-content: center;
}
.hlp-full3d-icon-btn[aria-pressed="true"] { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
/* Поиск стойки в топбаре полного 3D (четырнадцатый раунд) — тот же
   тёмный полупрозрачный тон, что и у остальных элементов топбара
   (.hlp-full3d-icon-btn/.hlp-full3d-tour-btn), но текстовый инпут не
   квадратный, поэтому своя ширина/паддинги, а не общий базовый класс. */
.hlp-full3d-search {
    width: 120px; font-size: 12px; padding: 5px 8px;
    border-radius: 6px;
}
.hlp-full3d-search:focus { width: 160px; }
/* Развёрнутая на весь экран сцена (Fullscreen API, см. hall-layout.js) —
   без этого явного растяжения обёртка внутри :fullscreen оставалась бы со
   своей обычной max-height (78vh у родителя) и просто "плавала" бы посреди
   чёрного экрана вместо честного заполнения всей площади монитора. */
.hlp-full3d-stage:fullscreen,
.hlp-full3d-stage:-webkit-full-screen {
    width: 100vw; height: 100vh; background: #0b0e14; padding: 0;
}
.hlp-full3d-stage:fullscreen .hall-layout-full3d-canvas,
.hlp-full3d-stage:-webkit-full-screen .hall-layout-full3d-canvas { border-radius: 0; }
.hlp-full3d-pad {
    position: absolute; left: 10px; bottom: 10px; z-index: 2;
    display: grid; grid-template-columns: 30px 30px 30px; grid-template-rows: 30px 30px 30px;
    grid-template-areas: ". fwd ." "left . right" ". back .";
    gap: 3px; opacity: .85;
}
.hlp-full3d-pad:hover { opacity: 1; }
.hlp-pad-btn {
    width: 30px; height: 30px; padding: 0; font-size: 13px; line-height: 1;
    border-radius: 6px; display: flex; align-items: center; justify-content: center;
}
.hlp-pad-fwd { grid-area: fwd; } .hlp-pad-left { grid-area: left; }
.hlp-pad-right { grid-area: right; } .hlp-pad-back { grid-area: back; }
/* Вертикальный D-pad (подъём/спуск камеры для осмотра стойки сверху донизу,
   см. panVertical() в hall-layout-3d.js) — отдельный маленький столбик рядом
   с основным пэдом панорамы, а не встроен в его 3×3-сетку, чтобы визуально
   не путать "переместить точку обзора" (горизонталь) с "поднять саму
   камеру" (вертикаль) — это разные операции над разными параметрами камеры. */
.hlp-full3d-vpad {
    position: absolute; left: 118px; bottom: 10px; z-index: 2;
    display: flex; flex-direction: column; gap: 3px; opacity: .85;
}
.hlp-full3d-vpad:hover { opacity: 1; }

/* Мини-карта (десятый раунд, только в полноэкранном full3D — см.
   setMinimapVisible() в hall-layout.js) — маленький квадратный оверлей в
   нижнем правом углу, поверх канвы (z-index выше самой канвы, но не выше
   D-pad'ов, они по разным углам и не пересекаются). */
.hlp-full3d-minimap {
    position: absolute; right: 10px; bottom: 10px; z-index: 2;
    width: 200px; height: 200px; border-radius: 8px; opacity: .92;
    box-shadow: 0 2px 10px rgba(0,0,0,.35);
}

.hlp-rack {
    position: absolute; box-sizing: border-box; border-radius: 4px;
    border: 1px solid rgba(0,0,0,.15); cursor: pointer; user-select: none;
    z-index: 1; /* стойки — над стенами/дверями по умолчанию (см. .hlp-object) */
}
.hlp-rack.hlp-editable { cursor: grab; }
.hlp-rack.hlp-editable:active { cursor: grabbing; }
.hlp-rack:hover { box-shadow: 0 0 0 2px var(--color-primary); z-index: 5; }
.hlp-rack.hlp-overlap { outline: 2px solid var(--color-danger); outline-offset: 1px; }
/* .hlp-rack-body — отдельная внутренняя обёртка с overflow:hidden (текст
   названия/метаданных должен обрезаться многоточием), а не сам .hlp-rack —
   иначе у маленьких (1 клетка) стоек значки поворота/снятия с плана,
   торчащие за пределы блока, обрезались бы вместе с текстом и были не
   видны при наведении почти на любом зуме меньше 100%. */
.hlp-rack-body {
    position: absolute; inset: 0; overflow: hidden; border-radius: inherit;
    background: var(--color-occupied-server);
    display: flex; flex-direction: column; justify-content: space-between;
    font-size: 11px; line-height: 1.25; padding: 4px 5px;
}
.hlp-rack-name {
    font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hlp-rack-meta { color: rgba(31,41,55,.7); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hlp-rack-fill {
    position: absolute; left: 0; bottom: 0; width: 100%; height: 4px; background: rgba(0,0,0,.12);
}
.hlp-rack-fill-bar { height: 100%; background: var(--color-primary); transition: width .3s ease; }
/* Потребление — ВСЕГДА видно цифрами (не только в подсказке при наведении),
   в правом нижнем углу стойки. ЗА пределами .hlp-rack-body (тот же приём,
   что и у .hlp-rack-controls чуть ниже) — так его не обрезает
   overflow:hidden. Текст бейджа переносится по словам (white-space: normal
   + max-width, вместо nowrap) — на самой маленькой (1 клетка, ~46px)
   стойке "⚡ 6.0 кВт" не помещается в одну строку и раньше вылезал ЗА ЛЕВЫЙ
   край блока (правый край был зафиксирован через right:2px, а ширина
   росла влево), из-за чего казался "оторванным" от стойки. С переносом
   бейдж остаётся строго внутри правого нижнего угла при любой ширине
   стойки, просто занимает 2 строки на самых маленьких. С названием (оно
   сверху) не пересекается ни при каком размере стойки. */
.hlp-power-badge {
    position: absolute; right: 2px; bottom: 7px; max-width: calc(100% - 4px);
    font-size: 9px; font-weight: 700; line-height: 1.2;
    padding: 2px 4px; border-radius: 3px; background: rgba(255,255,255,.92); color: var(--color-text);
    box-shadow: 0 0 0 1px rgba(0,0,0,.1); white-space: normal; text-align: right;
    word-break: break-word; box-sizing: border-box; z-index: 2;
    transition: background-color .25s ease, box-shadow .25s ease;
}
.hlp-power-badge.hlp-power-ok { color: #14532d; }
.hlp-power-badge.hlp-power-warn { color: var(--color-muted); }
.hlp-power-badge.hlp-power-bad { background: var(--color-danger-bg); color: var(--color-danger); }
/* Лёгкая пульсация бейджа при превышении лимита мощности — привлекает
   внимание на плане без анимации самой стойки (которая мешала бы читать
   план при большом количестве превышений сразу). */
.hlp-power-badge.hlp-power-bad { animation: hlp-power-pulse 1.6s ease-in-out infinite; }
@keyframes hlp-power-pulse {
    0%, 100% { box-shadow: 0 0 0 1px rgba(0,0,0,.1); }
    50% { box-shadow: 0 0 0 3px var(--color-danger); }
}
/* Короткая подсветка бейджа при точечном обновлении показания (см.
   refreshPower() в hall-layout.js, раз в 45 сек) — визуально видно, что
   цифра только что обновилась, без перерисовки всей схемы. */
.hlp-power-badge.hlp-power-flash { animation: hlp-power-flash .7s ease-out; }
@keyframes hlp-power-flash {
    0% { box-shadow: 0 0 0 3px var(--color-primary); }
    100% { box-shadow: 0 0 0 1px rgba(0,0,0,.1); }
}
@media (prefers-reduced-motion: reduce) {
    .hlp-power-badge.hlp-power-bad { animation: none; }
    .hlp-power-badge.hlp-power-flash { animation: none; }
}
/* Появление стоек/объектов при первой отрисовке плана (см. isInitialRender
   в render(), hall-layout.js) — со сдвигом по времени (animation-delay
   выставляется из JS), чтобы план "проявлялся" волной, а не мигал целиком
   разом. При последующих перерисовках (драг, поворот, авторасстановка) этот
   класс не навешивается — там вместо него плавный переход на новое место,
   см. flipAnimate() и .hlp-rack/.hlp-object transition ниже. */
/* Анимируем только opacity, не transform — двери/стены уже несут свой
   inline transform:rotate(...) (угол объекта), а стойки после перетаскивания
   могут получить transform от flipAnimate(); анимация transform здесь
   перезаписала бы то и другое на время анимации. */
.hlp-fade-in { animation: hlp-fade-in .35s ease both; }
@keyframes hlp-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .hlp-fade-in { animation: none; }
}
/* Значки поворота/снятия с плана — ЗА пределами .hlp-rack-body (не
   обрезаются), при необходимости чуть вылезают за границу самой стойки —
   это нормально и всегда видно, независимо от размера стойки на плане. */
.hlp-rack-controls {
    position: absolute; top: -8px; left: -8px; display: none; gap: 3px; z-index: 2;
}
.hlp-rack.hlp-editable:hover .hlp-rack-controls { display: flex; }
.hlp-rack-controls button {
    width: 17px; height: 17px; line-height: 15px; padding: 0; font-size: 11px;
    border: 1px solid rgba(0,0,0,.15); border-radius: 50%; background: var(--color-surface);
    color: var(--color-text); cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,.2);
}
.hlp-rack-controls button:hover { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.hlp-rack-controls button[data-unplace]:hover { background: var(--color-danger); border-color: var(--color-danger); }

.hlp-object {
    position: absolute; box-sizing: border-box;
    z-index: 3; /* стены/двери всегда видны — не прячутся под стойками, см. .hlp-rack */
    /* Поворот — вокруг верхнего левого угла (а не центра, как по умолчанию),
       чтобы x/y объекта оставались его "точкой привязки" на плане и
       поворот/растягивание в длину были предсказуемы и совпадали с тем,
       как их вычисляет hall-layout.js. */
    transform-origin: 0 0;
}
.hlp-object-door {
    border-bottom: 3px solid #8b5e34;
}
.hlp-object-door::after {
    /* Дуга открывания двери — стандартное архитектурное обозначение,
       вписана в собственный прямоугольник объекта, чтобы не наезжать на
       соседние объекты плана независимо от масштаба. Ось transform — левый
       нижний угол (там же "петля" двери на плане), чтобы при наведении дуга
       чуть довращивалась дальше, как будто дверь приоткрывается. */
    content: ''; position: absolute; left: 1px; bottom: 3px; width: 55%; height: 90%;
    border: 2px solid rgba(139,94,52,.55); border-right: none; border-bottom: none;
    border-radius: 100% 0 0 0; pointer-events: none;
    transform-origin: 0% 100%; transition: transform .35s ease;
}
.hlp-object-door:hover::after { transform: rotate(18deg); }
@media (prefers-reduced-motion: reduce) {
    .hlp-object-door::after { transition: none; }
    .hlp-object-door:hover::after { transform: none; }
}
.hlp-object-wall { background: repeating-linear-gradient(45deg, #d1d5db, #d1d5db 3px, #e5e7eb 3px, #e5e7eb 6px); border: 1px solid #9ca3af; border-radius: 2px; }
/* Точечные объекты детализации зала (восьмой раунд) — камера/пожаротушение/
   датчик/CRAC: все ставятся одним кликом (как подпись), фиксированного
   небольшого размера, круглая "плашка" с иконкой по центру — единый визуальный
   язык для "маленького служебного объекта на плане", отличный и от двери
   (линия с дугой), и от стены (штриховка), и от стойки (заполненный
   прямоугольник). Цвет плашки берётся из OBJECT_TYPE_DEFS в hall-layout.js
   через инлайновый style, тут только форма/geometry.
   .hlp-object-point задаёт всю "коробку" — сама эта коробка растянута под
   зону покрытия у камеры (см. renderObjectHtml), поэтому кружок-плашка —
   отдельный вложенный .hlp-object-point-badge по фиксированному размеру. */
.hlp-object-point { background: transparent; border: none; }
.hlp-object-point-badge {
    position: absolute; left: 0; top: 0; width: 26px; height: 26px;
    display: flex; align-items: center; justify-content: center; font-size: 14px;
    border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,.35);
    /* Иконка сама НЕ вращается вместе с объектом (transform родителя её бы
       перевернул вверх ногами при 180°) — поворот несёт только "хвостик"-
       стрелка направления снизу да зона покрытия камеры. */
    transform: rotate(calc(var(--hlp-point-rotation, 0deg) * -1));
}
.hlp-object-point-tag {
    position: absolute; left: 50%; top: 28px; transform: translateX(-50%);
    font-size: 9px; white-space: nowrap; background: rgba(255,255,255,.9); color: var(--color-text);
    padding: 1px 4px; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,.15); pointer-events: none;
}
.hlp-object-camera .hlp-object-point-badge { background: #7c3aed; }
.hlp-object-fire_suppression .hlp-object-point-badge { background: #dc2626; }
.hlp-object-sensor .hlp-object-point-badge { background: #0891b2; }
.hlp-object-crac .hlp-object-point-badge { background: #0284c7; }
/* Зона покрытия камеры — залитый сектор (conic-gradient, вершина в точке
   установки камеры), рисуется под плашкой-иконкой. Направление/угол/дальность
   передаются инлайновыми CSS-переменными из renderObjectHtml() —
   один универсальный блок правил, а не отдельный класс на градус. */
.hlp-object-camera-cone {
    position: absolute; left: 13px; top: 13px; z-index: -1; pointer-events: none;
    width: calc(var(--hlp-cone-range, 3) * 1px * var(--hlp-cell-px, 46) * 2);
    height: calc(var(--hlp-cone-range, 3) * 1px * var(--hlp-cell-px, 46) * 2);
    margin-left: calc(var(--hlp-cone-range, 3) * -1px * var(--hlp-cell-px, 46));
    margin-top: calc(var(--hlp-cone-range, 3) * -1px * var(--hlp-cell-px, 46));
    border-radius: 50%;
    background: conic-gradient(from calc(var(--hlp-cone-dir, 0deg) - var(--hlp-cone-fov, 70deg) / 2 - 90deg),
        rgba(124,58,237,.28) 0deg, rgba(124,58,237,.28) var(--hlp-cone-fov, 70deg),
        transparent var(--hlp-cone-fov, 70deg), transparent 360deg);
}
.hlp-object-label {
    display: flex; align-items: center; justify-content: center; text-align: center;
    font-size: 11px; font-weight: 600; color: var(--color-muted); background: rgba(255,255,255,.7);
    border: 1px dashed var(--color-border); border-radius: 4px; padding: 2px; white-space: nowrap;
}
.hlp-object.hlp-editable { cursor: grab; }
.hlp-object.hlp-editable:active { cursor: grabbing; }
.hlp-object-delete {
    position: absolute; top: -8px; right: -8px; width: 16px; height: 16px; line-height: 14px;
    border: none; border-radius: 50%; background: var(--color-danger); color: #fff; font-size: 10px;
    padding: 0; cursor: pointer; display: none; z-index: 2;
}
.hlp-object.hlp-editable:hover .hlp-object-delete { display: block; }
.hlp-object-delete:hover { background: #b91c1c; color: #fff; }
.hlp-object-rotate {
    position: absolute; top: -8px; left: -8px; width: 16px; height: 16px; line-height: 14px;
    border: none; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: 10px;
    padding: 0; cursor: pointer; display: none; z-index: 2;
}
.hlp-object.hlp-editable:hover .hlp-object-rotate { display: block; }
/* Явный :hover с той же специфичностью, что и класс — иначе общее правило
   "button:hover { background: var(--color-bg) }" из базовых стилей кнопок
   перебивает фон на почти белый, а значок при этом остаётся белым и
   становится не виден ("белый на белом"). */
.hlp-object-rotate:hover { background: var(--color-primary-dark); color: #fff; }
.hlp-object-resize-handle {
    position: absolute; top: 50%; right: -5px; width: 10px; height: 18px; margin-top: -9px;
    border-radius: 3px; background: var(--color-primary); cursor: ew-resize;
    display: none; z-index: 2;
}
.hlp-object-resize-handle::after {
    content: ''; position: absolute; left: 3px; top: 4px; width: 4px; height: 10px;
    border-left: 1px solid rgba(255,255,255,.8); border-right: 1px solid rgba(255,255,255,.8);
}
.hlp-object.hlp-editable:hover .hlp-object-resize-handle { display: block; }

.hlp-draw-preview {
    position: absolute; z-index: 4; pointer-events: none;
    background: rgba(37,99,235,.18); border: 2px dashed var(--color-primary); border-radius: 2px;
}

.hall-layout-error { margin-top: 10px; }

@media (max-width: 900px) {
    .hall-layout-body { flex-direction: column; }
    .hall-layout-sidebar { width: 100%; max-height: 220px; }
}

/* ----- Печать / экспорт плана зала (кнопки "Печать / PDF" и "Экспорт PNG"
   в тулбаре — см. hall-layout.js exportPlanPng() и window.print()). Сама
   печать использует уже существующий общий механизм .print-only/.no-print
   (см. блок @media print выше в файле) — здесь только специфика зала. ----- */
.hall-layout-print-header { display: none; margin-bottom: 12px; }
.hall-layout-print-header h2 { margin: 0 0 2px; }
/* Масштаб под размер листа считается в JS (updatePrintScale() в
   hall-layout.js, на 'beforeprint' + перед window.print()) — там же
   выбирается книжная/альбомная ориентация A4 через динамически
   вставляемое правило @page. Здесь только сброс переполнения обёртки,
   чтобы масштабированный (через transform) план не обрезался. */
@media print {
    /* Печать/PNG-экспорт всегда идут с обычного 2D-плана, даже если на
       экране сейчас включён 3D-вид (у него нет своего экспорта/масштабирования
       под A4 — см. renderIso() в hall-layout.js) — !important здесь
       обязателен, т.к. переключение видов ставит inline display:none/flex
       на #hlp-canvas-wrap/#hlp-iso-wrap напрямую через JS, а обычная
       специфичность класса инлайн-стиль не перебивает. */
    .hall-layout-canvas-wrap { display: block !important; max-height: none !important; overflow: visible !important; border: none; }
    .hall-layout-body { display: block; }
    .hall-layout-sidebar { display: none !important; }
    .hlp-rack-controls, .hlp-object-rotate, .hlp-object-delete, .hlp-object-resize-handle { display: none !important; }
}

/* ----- Модалка содержимого стойки (клик по стойке на плане) -----
   Показывает ту же визуальную схему "юниты + блоки оборудования", что и
   полная страница стойки (public/js/racks-view.js) — переиспользуем те же
   CSS-классы (.rack-schema/.rack-unit-row/.rack-equipment-block/...), но в
   уменьшенном масштабе и БЕЗ интерактивности (drag-drop/удаление/попап
   blade-серверов — тут только просмотр). Компактные размеры заданы через
   вложенные под .hlp-rackinfo-schema селекторы, чтобы не задеть исходные
   стили полноразмерной схемы на странице стойки. */
/* Раньше max-width был 480px/520px — при длинных именах оборудования текст
   в блоках схемы обрезался/переносился совсем впритык ("слишком узко").
   680px даёт схеме заметно больше места по ширине под подписи, оставаясь
   в пределах .modal-overlay { padding: 40px 16px } так что на маленьких
   экранах модалка всё равно ужимается через .modal { max-width:100% } в
   media-запросе выше по файлу. */
.hlp-rackinfo-box { max-width: 680px; }
.hlp-rackinfo-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.hlp-rackinfo-close {
    border: none; background: transparent; font-size: 16px; line-height: 1; cursor: pointer;
    color: var(--color-muted); padding: 2px 4px;
}
.hlp-rackinfo-close:hover { color: var(--color-text); background: var(--color-bg); border-radius: 4px; }
.hlp-rackinfo-stats { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; margin: 8px 0; }
.hlp-rackinfo-comment {
    font-size: 12px; color: var(--color-muted); background: var(--color-bg); border-radius: var(--radius);
    padding: 6px 8px; margin: 8px 0;
}
.hlp-rackinfo-sides { display: flex; gap: 6px; margin-bottom: 8px; }
.hlp-rackinfo-side-btn.btn-primary { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
/* PDU здесь НЕ рисуются боковыми полосами, как на полной странице стойки
   (.rack-pdu-strip) — эта модалка сама прокручивается по вертикали
   (overflow-y:auto), а при этом любой overflow-x, кроме visible,
   по правилам CSS перестаёт быть по-настоящему visible и обрезает то, что
   торчит за пределы контейнера — полосы PDU с отрицательными left/right
   были бы обрезаны или недоступны прокруткой. Поэтому PDU показываются
   обычным блоком в общей сетке юнитов (см. buildRackSchemaHtml() —
   isPdu-ветка), с мощностью прямо в подписи блока — см. .type-pdu ниже. */
/* Раньше здесь был max-height:420px + overflow-y:auto — при высокой стойке
   (48U и выше) схема обрезалась и прокручивалась ВНУТРИ модалки отдельным
   скроллом, из-за чего стойка целиком не помещалась в видимую область и
   выглядела "обрезанной" — пользователь явно попросил, чтобы стойка была
   видна целиком, как единая картинка. Модалка сама по себе не скроллится
   изнутри — вместо этого позволяем окну вырасти на всю высоту содержимого
   и используем уже существующий скролл всего оверлея (.modal-overlay
   { overflow-y: auto } в общих стилях модалок) — так при большой стойке
   прокручивается вся страница/модалка целиком, а не маленькое окошко
   внутри неё, и сама схема стойки не обрезается. */
.hlp-rackinfo-schema-wrap { overflow: visible; padding: 4px 6px; }
.hlp-rackinfo-schema .rack-schema { width: 100%; padding: 6px; }
.hlp-rackinfo-schema .rack-unit-row { height: 13px; border-bottom-color: rgba(71,85,105,.6); }
.hlp-rackinfo-schema .rack-unit-label { width: 22px; font-size: 9px; padding-right: 3px; }
.hlp-rackinfo-schema .rack-equipment-block { left: 22px; font-size: 10px; padding: 0 5px; }
.hlp-rackinfo-schema .rack-equipment-block .eq-title { font-size: 10px; padding: 0 3px; }
.hlp-rackinfo-schema .rack-equipment-block.type-pdu { background: #fbbf24; }
.hlp-rackinfo-empty-side { padding: 24px; text-align: center; }
/* Появление модалки — фон плавно затемняется, само окно чуть увеличивается
   из центра с одновременным fade-in (те же принципы, что и hlp-fade-in на
   плане: только opacity/transform, ничего "прыгающего"). Закрытие — то же
   в обратную сторону, класс на overlay меняется в JS перед скрытием, чтобы
   успела доиграть анимация (см. closeRackInfoModal()). */
#hlp-rack-info-modal.hlp-modal-opening { animation: hlp-modal-backdrop-in .18s ease both; }
#hlp-rack-info-modal.hlp-modal-opening .modal { animation: hlp-modal-box-in .22s ease both; }
#hlp-rack-info-modal.hlp-modal-closing { animation: hlp-modal-backdrop-in .15s ease reverse both; }
#hlp-rack-info-modal.hlp-modal-closing .modal { animation: hlp-modal-box-in .15s ease reverse both; }
@keyframes hlp-modal-backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes hlp-modal-box-in {
    from { opacity: 0; transform: scale(.96) translateY(4px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    #hlp-rack-info-modal.hlp-modal-opening,
    #hlp-rack-info-modal.hlp-modal-opening .modal,
    #hlp-rack-info-modal.hlp-modal-closing,
    #hlp-rack-info-modal.hlp-modal-closing .modal { animation: none; }
}
