/* ==========================================================================
   DARPANEL Design System
   Tokens → primitives → components. Every screen is built from what's below —
   no page defines its own one-off button, card, or spacing value.
   ========================================================================== */
/* Фирменная типографика — self-hosted woff2 (fontsource-подсеты с unicode-
   range, браузер тянет только нужное): панель не должна дёргать Google
   Fonts — офлайн и медленная сеть отдают мгновенный системный фолбэк без
   FOUT. Система (выбрана 2026-07-31): Geist — дисплейная (логотип 700,
   заголовки 600-700, кнопки 600), Onest — текстовая (меню/таблицы 500,
   текст/подписи 400-500), Geist Mono — код/терминал/логи. */
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/onest-400.woff2') format('woff2'); }
@font-face { font-family: 'Onest'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/onest-500.woff2') format('woff2'); }
/* Geist (display-гарнитура, v1.7+ — с переработанной кириллицей): latin и
   cyrillic — отдельные файлы с unicode-range, браузер тянет только нужное. */
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/geist-latin-400.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/geist-cyrillic-400.woff2') format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/geist-latin-500.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/geist-cyrillic-500.woff2') format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/geist-latin-600.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/geist-cyrillic-600.woff2') format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/geist-latin-700.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/geist-cyrillic-700.woff2') format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; }
/* Geist Mono — терминал, логи, код. */
@font-face { font-family: 'Geist Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/geist-mono-latin-400.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Geist Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/geist-mono-cyrillic-400.woff2') format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; }
@font-face { font-family: 'Geist Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/geist-mono-latin-500.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Geist Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/geist-mono-cyrillic-500.woff2') format('woff2'); unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

:root {
  /* --- Color tokens (DARPANEL Design System v1) --- */
  --bg: #0a0d13;
  --surface: #121722;
  --surface-elevated: #171d29;
  --surface-hover: #1e2534;
  --border: #1e2430;
  --border-hover: #2c3446;
  --line: rgba(255, 255, 255, .05); /* дословно --line из макета — волосяная линия там, где без неё нельзя */

  --primary: #6366f1;
  --primary-hover: #818cf8;
  --primary-contrast: #ffffff;
  --primary-wash: rgba(99, 102, 241, .16);

  --success: #22c55e;
  --success-wash: rgba(34, 197, 94, .14);
  /* Оранжевого/жёлтого в палитре больше нет. «Требует внимания» — нейтральный
     светло-серый: не спорит с акцентом и не путается с реально сломанным
     (danger остаётся красным). Это же убирает жёлтую иконку папки в файловом
     менеджере — она красится через var(--warning), отдельного класса нет. */
  --warning: #cbd5e1;
  --warning-wash: rgba(203, 213, 225, .12);
  --danger: #ef4444;
  --danger-wash: rgba(239, 68, 68, .14);

  --text-primary: #e6edf3;
  --text-secondary: #9baaae;
  --text-tertiary: #5c6b74;

  /* --- Spacing (8pt grid) --- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 40px;
  --sp-8: 48px;
  --sp-9: 64px;

  /* --- Radii --- */
  --radius-card: 12px;
  --radius-control: 9px; /* дословно --r-ctl из макета, было 10px */
  --radius-sm: 6px; /* дословно --r-sm из макета, было 8px */
  /* Раньше 999px (полный круг/капсула) — иконки-кнопки и бейджи оказывались
     круглыми/овальными рядом с прямоугольными кнопками (--radius-control) —
     два разных радиуса на соседних элементах одной строки. Один токен на
     скругление всего в приложении: там, где реально нужна капсула (трек
     тумблера/прогресс-бар — они тоньше радиуса и всё равно останутся
     визуально круглыми по краям), это не ломается. */
  --radius-badge: var(--radius-control);

  /* --- Shadows (barely-there) --- */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, .32);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, .45);

  /* --- Производные цвета, зависящие от темы (затемнение под оверлеями,
     сетка/подписи canvas-графиков, подсветка синтаксиса, skeleton-шиммер).
     Светлая тема переопределяет ВЕСЬ этот набор — см. html[data-theme="light"]. --- */
  --backdrop: rgba(6, 7, 10, .7);
  --chart-grid: rgba(155, 170, 174, .18);
  --chart-label: rgba(155, 170, 174, .8);
  --tok-comment: #6b7686;
  --tok-string: #7ec699;
  --tone-blue: #38bdf8;
  --tone-blue-bg: rgba(56, 189, 248, .14);
  --skeleton-base: #1d2331;
  --skeleton-shine: #2a3242;

  /* Нативные скроллбары, чекбоксы и скролл-трэки под тему (Chromium/Firefox) */
  color-scheme: dark;

  /* --- Motion --- */
  --ease: cubic-bezier(.2, .8, .2, 1);
  --dur: 160ms;
  --dur-slow: 280ms;
  /* Свечение после ухода hover затухает медленно: базовый transition у
     интерактивных элементов использует --dur-glow, а на :hover вешается
     transition-duration: var(--dur) — вход быстрый, выход длинный. */
  --dur-glow: 600ms;

  /* --- Premium surface treatment ---
     Кнопки/карточки получают лёгкий вертикальный градиент вместо плоской
     заливки + внутренний светлый хайлайт по верхней кромке: то, что делает
     поверхность «физической» у Apple/Vercel/Linear. Токенами, а не хардкодом
     в каждом компоненте, чтобы светлая тема могла переопределить всё разом.
     Primary-градиент — только тёмные/насыщенные тона, никакого светлого
     стопа сверху (был #818cf8 у верхнего края — само наличие светлого тона
     в приманке читалось как «выцветшая» кнопка, а не как глубина). */
  --grad-primary: linear-gradient(135deg, var(--primary) 0%, #4338ca 60%, #3730a3 100%);
  --grad-surface: linear-gradient(180deg, rgba(255, 255, 255, .045) 0%, rgba(255, 255, 255, 0) 100%);
  --edge-highlight: inset 0 1px 0 rgba(255, 255, 255, .07);
  --edge-highlight-strong: inset 0 1px 0 rgba(255, 255, 255, .3);
  /* Дословно из макета (--lift/--lift-hi): трёхслойная тень карточки. */
  --lift: inset 0 1px 0 rgba(255, 255, 255, .06), 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .28);
  --lift-hi: inset 0 1px 0 rgba(255, 255, 255, .09), 0 2px 4px rgba(0, 0, 0, .45), 0 16px 40px rgba(0, 0, 0, .42);
  --glow-primary: 0 6px 20px rgba(99, 102, 241, .45), 0 2px 6px rgba(67, 56, 202, .4);
  --glow-primary-hover: 0 10px 28px rgba(99, 102, 241, .6), 0 4px 10px rgba(67, 56, 202, .5);
  /* Мягкое размытое свечение для hover на кнопках/иконках/карточках — НЕ то
     же самое, что жёсткое кольцо 0 0 0 Npx у фокуса инпутов (то — акцентный
     стандарт, здесь нужна именно мягкая растушёванная подсветка, иначе на
     прямоугольной кнопке кольцо визуально режет угол и выглядит дёшево,
     блочно, а не как свет). */
  --glow-soft: 0 0 18px 2px var(--primary-wash);
  --glow-soft-danger: 0 0 18px 2px var(--danger-wash);

  /* Фон-пятна body — токенами, чтобы светлая тема могла притушить их.
     Подняты против макета: на огромных пустых полях центр и углы были
     почти не видны — пятна должны намекать глубину, а не отсутствовать. */
  --bg-accent-1: rgba(99, 102, 241, .28);
  --bg-accent-2: rgba(34, 211, 238, .18);
  --bg-accent-3: rgba(168, 85, 247, .12);
  /* Стеклянная подложка сайдбара — токеном, чтобы светлая тема не получила
     тёмное полупрозрачное пятно поверх белого фона. */
  --sidebar-glass: rgba(18, 22, 31, .4);
  /* Единый «стеклянный» материал всех крупных поверхностей (карточки,
     плитки сайтов, stat-карточки): база --surface с alpha .58 — пятна фона
     и вертикальная глубина читаются СКВОЗЬ поверхность, текст остаётся
     контрастным (фон-пятна не сильнее .28). Blur у элементов, не у токена. */
  --surface-glass: rgba(18, 23, 34, .58);

  /* --- Type scale — 5 sizes max --- */
  --fs-h1: 24px;
  --fs-h2: 20px;
  --fs-h3: 16px;
  --fs-body: 13px; /* дословно --fs из макета, было 14px */
  --fs-caption: 12px;
  --fs-micro: 11px; /* дословно --fs-mi из макета, было 12px */

  /* --- Font families: три слота на ВСЮ панель ---
     display — заголовки, бренд, кнопки; body — текст/таблицы/формы;
     mono — терминал, код, логи. Сейчас = системный стек (как было);
     выбранная пара с font-preview.html подменяет первые имена токенов
     в одном месте — здесь, больше нигде в CSS семейства не хардкодим. */
  --font-display: 'Geist', -apple-system, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --font-body: 'Onest', -apple-system, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --font-mono: 'Geist Mono', 'Cascadia Code', 'JetBrains Mono', Consolas, monospace;

  /* Единая высота ВСЕХ контролов панели — инпутов И кнопок: одного ряда и
     на разных линиях. ЖЁЛЕЗНОЕ ПРАВИЛО (требование 2026-08-03): интерфейс
     не пляшет, разной высоты не бывает. Раньше инпуты были 48px при кнопках
     32px — лесенка в каждой форме. --control-h оставлен как алиас, чтобы не
     трогать все места использования. */
  --control-h: 32px;
  --control-h-sm: 32px; /* дословно --h из макета (было 34px) */
  --badge-h: 24px;
  /* Единая высота строки во всех списках/таблицах панели (сайты, плагины,
     DNS, файлы, почта, бэкапы): одна строка = один токен, столбики действий
     стоят ровно. Compact-плотность переопределяет на 36px. */
  --row-h: 44px;
  --sidebar-w: 216px; /* дословно --sidebar из макета, было 230px */
  /* Дословно из макета (--ws-col): один токен на ширину правой колонки, а не
     два независимых хардкода "290px" (в шапке и в теле) — иначе их легко
     развести на пиксель при следующей правке и левый край поиска перестаёт
     совпадать с левым краем панели ниже. */
  --aside-col: 276px; /* поиск 150 + группа (колокол/настройки/аватар ~112) + gap 8 + запас: при 264 поиск сидел вплотную к колокольчику (зазор 2px) */
  /* Дословно --card-pad из макета (было --sp-5=24px). */
  --card-pad: 18px;
  /* Единая минимальная высота карточек во всех .card-grid (Безопасность,
     Базы, Бэкапы, Файлы, Пользователи, сайты): монолитность между разделами.
     Ориентир — карточки «Безопасности», они задают потолок смысла. */
  --card-min-h: 240px;
  /* Единый внешний отступ приложения: края вьюпорта (лево/право/низ) и
     зазоры между колонками (сайдбар|контент, контент|правая панель) — одно
     число, один токен. Раньше это были четыре независимых значения (sp-6 у
     main, sp-3/sp-4 у сайдбара, sp-4 у грида обзора) — отсюда «слева меньше,
     справа больше» и разные зазоры между колонками. */
  --page-gutter: var(--sp-5);

  /* --- Z-шкала: единственные допустимые слои, без литералов 21/55/1000 --- */
  --z-sticky: 5;    /* топбар, сайдбар */
  --z-pop: 20;      /* дропдауны, попапы, док */
  --z-overlay: 50;  /* палитра Ctrl+K, оверлеи */
  --z-drawer: 60;   /* мобильный drawer (+ backdrop на 5 ниже) */
  --z-toast: 100;   /* тосты, контекстные меню — поверх всего */
}

/* ==========================================================================
   Светлая тема — html[data-theme="light"] переопределяет ВСЕ цветовые токены.
   Акцент (--primary и производные) перекрывается брендированием inline через
   documentElement.style (brandkit.js) — inline-стиль сильнее любого класса,
   поэтому бренд-цвет побеждает в обеих темах, это осознанный порядок.
   Здесь --primary оставлен тем же базовым #6366f1 (дефолт без брендинга),
   hover чуть темнее — на светлом фоне hover уходит вниз, а не вверх.
   ========================================================================== */
html[data-theme="light"] {
  --bg: #eef1f6;
  --surface: #ffffff;
  --surface-elevated: #f2f4f9;
  --surface-hover: #e6eaf3;
  --border: #dde1ea;
  --border-hover: #c3c9d8;

  --primary: #6366f1;
  --primary-hover: #4f46e5;
  --primary-contrast: #ffffff;
  --primary-wash: rgba(99, 102, 241, .12);

  --success: #16a34a;
  --success-wash: rgba(34, 197, 94, .15);
  --warning: #94a3b8;
  --warning-wash: rgba(148, 163, 184, .16);
  --danger: #dc2626;
  --danger-wash: rgba(239, 68, 68, .12);

  --text-primary: #1c2330;
  --text-secondary: #5b6472;
  --text-tertiary: #8b94a3;

  --shadow-sm: 0 1px 2px rgba(16, 24, 40, .08);
  --shadow-md: 0 8px 24px rgba(16, 24, 40, .14);
  --shadow-lg: 0 12px 32px rgba(16, 24, 40, .18);

  --backdrop: rgba(28, 35, 48, .45);
  --chart-grid: rgba(28, 35, 48, .12);
  --chart-label: rgba(91, 100, 114, .9);
  --tok-comment: #8b94a3;
  --tok-string: #15803d;
  --tone-blue: #0284c7;
  --tone-blue-bg: rgba(2, 132, 199, .12);
  --skeleton-base: #e6eaf3;
  --skeleton-shine: #f5f7fc;

  /* На светлом фоне белый хайлайт не читается — вместо него едва заметная
     тень снизу, а градиент поверхностей уходит в лёгкую серую подложку. */
  --grad-primary: linear-gradient(135deg, var(--primary) 0%, #4338ca 60%, #3730a3 100%);
  --grad-surface: linear-gradient(180deg, rgba(255, 255, 255, .8) 0%, rgba(28, 35, 48, .012) 100%);
  --edge-highlight: inset 0 1px 0 rgba(255, 255, 255, .9), 0 1px 2px rgba(16, 24, 40, .05);
  --edge-highlight-strong: inset 0 1px 0 rgba(255, 255, 255, .25);
  /* Макет не определяет светлую тему — тёмные --lift/--lift-hi читались бы
     грязным пятном на белом фоне, поэтому здесь тот же приём, что у
     --edge-highlight/-strong выше (единственная не буквальная замена в этом
     наборе, и только ради светлой темы, которой в макете просто нет). */
  --lift: var(--edge-highlight);
  --lift-hi: var(--edge-highlight-strong);
  --glow-primary: 0 4px 14px rgba(99, 102, 241, .25);
  --glow-primary-hover: 0 6px 20px rgba(99, 102, 241, .35);

  /* Тот же приём фон-пятен, но заметно тише — на светлом фоне насыщенность
     тёмной темы читалась бы как грязное пятно, а не подложка. */
  --bg-accent-1: rgba(99, 102, 241, .10);
  --bg-accent-2: rgba(34, 211, 238, .08);
  --bg-accent-3: rgba(168, 85, 247, .05);
  --sidebar-glass: rgba(255, 255, 255, .4);
  --surface-glass: rgba(255, 255, 255, .62);

  color-scheme: light;
}

* { box-sizing: border-box; }

/* Скроллбары — тонкие и невидимые в покое, появляются только при наведении
   на прокручиваемую область (сайдбар, список активности и т.п.), а не висят
   постоянной серой полосой сбоку. */
* { scrollbar-width: thin; scrollbar-color: transparent transparent; }
*:hover { scrollbar-color: var(--border-hover) transparent; }
::-webkit-scrollbar { width: 10px; height: 0; }
::-webkit-scrollbar-thumb { background: transparent; background-clip: padding-box; border: 3px solid transparent; border-radius: 99px; }
:hover::-webkit-scrollbar-thumb { background-color: var(--border-hover); }

/* Атрибут hidden обязан побеждать любые display-классы (grid/flex перебивают
   UA-стиль display:none — уже ловили это на .empty-state и .card-grid). */
[hidden] { display: none !important; }

html, body { height: 100%; overflow: hidden; } /* скролл живёт внутри <main>, у body его нет вообще */
/* Канвас всегда тёмный, даже если слои body глючат/CSS догружается: без этого
   при композитинг-сбое фоновых градиентов (известный баг Chromium на Windows)
   страница оставалась серой до перезагрузки. */
html { background: var(--bg); }

body {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400; /* выбранная система: текст Onest 400-500, акцент 500 у b/strong */
  color: var(--text-primary);
  font-size: var(--fs-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  position: relative;
  /* Фон: три цветных пятна (акцент сверху слева, холодный оттенок снизу
     справа, пурпурное свечение в центре — оно заполняет «мёртвую» середину
     между карточками и доком) + едва заметная вертикальная глубина: верх
     на 2% светлее, низ уходит в темноту — подложка не плоская заливка,
     а пространство, с которым док делит палитру. */
  background:
    radial-gradient(1200px 800px at 6% -10%, var(--bg-accent-1), transparent 62%),
    radial-gradient(1000px 750px at 98% 110%, var(--bg-accent-2), transparent 58%),
    radial-gradient(900px 650px at 55% 45%, var(--bg-accent-3), transparent 62%),
    linear-gradient(180deg, rgba(255, 255, 255, .02), transparent 35%, rgba(0, 0, 0, .3)),
    var(--bg);
  /* background-attachment:fixed убран осознанно: body не скроллится вообще
     (overflow:hidden, скролл внутри .app-main), то есть fixed тут ничего не
     даёт — но регулярно провоцирует композитинг-баг Chromium, при котором
     градиенты не растеризуются и фон остаётся серым до перезагрузки. */
}
/* Лёгкое зерно поверх фон-подложки — не картинка, а текстура. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: .09;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/></filter><rect width='128' height='128' filter='url(%23n)'/></svg>");
  background-size: 128px 128px;
}
.app-shell { position: relative; z-index: 1; }

::selection { background: var(--primary-wash); color: var(--text-primary); }

/* ==========================================================================
   Typography
   ========================================================================== */

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: 0; color: var(--text-primary); }
h1 { font-size: var(--fs-h1); font-weight: 700; }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }
/* Семантический акцент — «жир» 500 поверх основного 400. */
b, strong { font-weight: 500; }
/* Один заголовочный шрифт на все места — h1..h4 и текстовые эквиваленты
   заголовков (.section-title = h4 по смыслу): Geist, дисплейные веса
   600-700 по выбранной системе. Текст — Onest. */
h1, h2, h3, h4, .section-title {
  font-family: var(--font-display);
}

.page-header { margin-bottom: var(--sp-6); }
.page-header h1 { margin-bottom: var(--sp-2); }
.page-header p { margin: 0; color: var(--text-secondary); font-size: var(--fs-body); }
.page-title-row { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-2); }
.page-title-row h1 { margin-bottom: 0; }

/* Site-detail header: back button on its own line, then title+actions on ONE
   line (not actions floated to the bottom of a taller left column via
   flex-end on the whole header — that was the "actions sit too low" bug),
   then an optional status line below. */
.site-detail-header { display: flex; flex-direction: column; gap: var(--sp-2); }
.site-detail-header #siteDetailBack { align-self: flex-start; }
/* Заголовок + вкладки + шапочные действия — одна строка, не три. Вкладки
   (иконки) не сжимаются и не переносятся; при нехватке места лучше
   горизонтальный скролл всей строки, чем перенос на вторую строку. */
.site-detail-title-row { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: nowrap; overflow-x: auto; }
.site-detail-title-row .page-title-row { margin-bottom: 0; flex-shrink: 0; }
.site-detail-title-row .chip-group { flex-shrink: 0; margin-bottom: 0; }
.site-detail-title-row .header-actions { flex-shrink: 0; margin-left: auto; }
/* Единый принцип строки: в шапке сайта все элементы одной высоты
   (--control-h-sm = 32px) — вкладки-чипы и статус-бейджи. Глобально
   .badge-icon = --badge-h (24px), здесь поднимается до высоты чипа,
   иначе два ряда кружков разного диаметра на одной линии читаются как
   лесенка. */
.site-detail-title-row .badge-icon { height: var(--control-h-sm); min-width: var(--control-h-sm); }
.site-detail-title-row .badge-icon .icon { width: 16px; height: 16px; }
/* Статус-бейджи (тип/SSL/WAF) и вкладки-навигация — одинаковый вид кружка,
   но разный смысл (одно — инфо/переключатель состояния сайта, другое —
   переход по разделам страницы): без разделителя они читаются как одна
   нерасчленимая цепочка одинаковых точек. Тонкая вертикальная черта между
   группами — единственное, что их размечает, не ломая "один язык иконок". */
.chip-group-icon { position: relative; padding-left: var(--sp-4); }
.chip-group-icon::before {
  /* Черта по центру 16px зазора между группами (row gap .site-detail-title-row),
     а не вплотную к первой иконке группы: зазор читается как 8|8 и ряд
     выглядит равномерным, без «большого отступа» после «Панель сайта». */
  content: ''; position: absolute; left: calc(-1 * var(--sp-4) / 2); top: 50%; transform: translateY(-50%);
  width: 1px; height: 22px; background: var(--border);
}
/* Пустая группа/бейджи не должны давать фантомный зазор и висячую черту. */
.chip-group-icon:empty, .site-detail-badges:empty { display: none; }
@media (max-width: 720px) { .site-detail-title-row { flex-direction: column; align-items: stretch; overflow-x: visible; } }

.muted { color: var(--text-secondary); font-size: var(--fs-body); }
.field-hint { display: block; font-size: var(--fs-micro); color: var(--text-tertiary); margin-top: var(--sp-1); font-weight: 400; }
.field-hint:empty { display: none; }
.error { color: var(--danger); font-size: var(--fs-caption); }
.error.secret-value { word-break: break-all; }
.banner { border-radius: var(--radius-sm); padding: var(--sp-3); margin-top: var(--sp-2); font-size: var(--fs-caption); }
.banner p { margin: 0 0 var(--sp-1); }
.banner ul { margin: 0 0 var(--sp-2); padding-left: 1.2em; }
.banner-warn { background: var(--warning-wash); border: 1px solid var(--warning); color: var(--text-primary); }
code {
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  padding: 1px 6px;
  border-radius: 5px;
  font-size: .92em;
  font-family: var(--font-mono);
}

/* ==========================================================================
   Layout shell
   ========================================================================== */

/* --- App shell: left sidebar + main column, sticky both --- */

.app-shell { display: flex; align-items: stretch; height: 100vh; overflow: hidden; } /* ровно экран: иначе шелл растёт вместе с контентом и внутренний скролл <main> не срабатывает */

/* Шелл — просто раскладка (лого + плавающая панель), сам по себе без фона:
   лого живёт на обычном фоне страницы, не внутри стеклянной панели. */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  background: transparent;
  border-right: 0;
  /* Левый и нижний края — тот же --page-gutter, что справа у main и снизу у
     дока: равные внешние отступы по всем краям вьюпорта. Справа паддинга нет
     осознанно — зазор до контента целиком даёт padding-left у main (тот же
     --page-gutter), чтобы не складывать два полу-отступа. */
  padding: 0 0 var(--page-gutter) var(--page-gutter);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: visible;
}
/* Стеклянная панель — тот же рецепт, что у .overview-aside справа (тот же
   --sidebar-glass/блюр/радиус): единый визуальный язык обеих боковых панелей.
   Верх — сразу под лого (та же линия, что верх .overview-aside); низ — на
   одной линии с нижним краем дока (тот же --page-gutter, что и у самого дока,
   а не резерв под всю его высоту — дока и панели не пересекаются по
   горизонтали). */
/* Никакой отдельной стеклянной коробки вокруг меню — в макете пункты меню
   сидят прямо на фоне страницы (.side это просто flex-колонка), своя
   поверхность/рамка есть только у карточки сервера внизу (.server-card). */
.sidebar-panel {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  /* Без overflow-x:hidden любой ребёнок, вылезший на 1px (свечение активного
     пункта, субпиксельное округление), рожал классический горизонтальный
     скроллбар полосой под карточкой сервера — «нижний сайдбар», которого
     быть не должно. По вертикали скролл остаётся. */
  overflow-x: hidden;
}

/* Высота строго равна высоте топбара (64px == 64px), поэтому первый пункт
   меню и первая карточка страницы начинаются на одной линии, без лесенки. */
/* padding-left: 0 + знак 32px по левому краю марки: оптический центр знака
   встаёт на ось иконок меню (24px gutter + 8px паддинг пункта + 8px
   половина иконки = 40px). Раньше знак 40px висел на sp-2 правее — лого
   смотрелось съехавшим относительно колонки меню. */
.sidebar-brand { display: flex; align-items: center; gap: var(--sp-3); height: 64px; padding: 0; flex-shrink: 0; }
.brand-mark {
  width: 40px; height: 40px; border-radius: var(--radius-sm);
  background: var(--primary); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight: 500; font-size: 14px; flex-shrink: 0;
}

/* Кастомный логотип клиента заменяет фирменный знак (брендирование из настроек) */
.brand-mark.has-logo { background: none; }
.brand-mark img { width: 40px; height: 40px; border-radius: var(--radius-sm); object-fit: cover; display: block; }
/* Фирменный знак — прозрачный webp, без подложки и кропа. 32px по левому
   краю марки — центр знака на оси иконок меню (см. .sidebar-brand выше).
   В свёрнутом сайдбаре марка центрируется целиком, там это не нужно. */
.app-shell:not(.collapsed) .brand-mark.has-logo { justify-content: flex-start; }
.brand-mark img.logo-dark, .brand-mark img.logo-light { width: 32px; height: 32px; object-fit: contain; border-radius: 0; }
/* Фирменный знак — две версии: светлые линии для тёмной темы, тёмные для
   светлой. Переключение классом темы на <html>, без JS. */
.logo-light { display: none; }
html[data-theme="light"] .logo-dark { display: none; }
html[data-theme="light"] .logo-light { display: block; }

/* margin-top = var(--sp-4): верхний паддинг main тоже var(--sp-4) (16px) —
   отступ меню под лого совпадает с паддингом страницы, иначе первый пункт
   меню и первая карточка стоят на разных линиях. */
.sidebar-nav { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-4); }
.nav-item {
  position: relative;
  isolation: isolate; /* под радиальное свечение активного пункта (::before, z-index:-1) */
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: var(--control-h-sm);
  padding: 0 var(--sp-2);
  border: 1px solid transparent; /* резерв под градиентный ring активного пункта — не сдвигать соседей */
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-body);
  font-weight: 500; /* выбранная система: меню — Onest 500 */
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-glow) var(--ease), border-color var(--dur-glow) var(--ease), color var(--dur-glow) var(--ease);
}
.nav-item:hover { transition-duration: var(--dur); }
/* Плашка вокруг иконки меню (была тонирована по образцу макета) на реальном
   экране выглядела грязно — вторая рамка внутри уже обведённого пункта.
   Плоская иконка на прозрачном фоне, без своей поверхности/тени. */
.nav-item > .icon {
  flex-shrink: 0; color: var(--text-secondary);
  transition: color var(--dur) var(--ease);
}
/* Hover на пунктах меню — та же градиентная кайма, что у иконок (общий блок
   «Единое выделение»), без серого фона-заливки. */
.nav-item:hover { color: var(--text-primary); }
.nav-item:hover > .icon { color: var(--text-primary); }
/* Focus-дисциплина как у .btn: клик мышью не оставляет браузерный focus-ring
   поверх свечения активного пункта (он и был «чужой рамкой» вокруг пункта
   меню), клавиатурная навигация — видимый контур в фирменном цвете.
   ::-moz-focus-inner — Firefox рисует внутренний пунктир кнопке в :focus. */
.nav-item:focus, .quick-action:focus, .quick-cfg-btn:focus, .dock-btn:focus { outline: none; }
.nav-item::-moz-focus-inner, .quick-action::-moz-focus-inner, .quick-cfg-btn::-moz-focus-inner, .dock-btn::-moz-focus-inner { border: 0; }
.nav-item:focus-visible, .quick-action:focus-visible, .quick-cfg-btn:focus-visible, .dock-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.nav-item.active:focus-visible { outline: none; } /* активный уже выделен — контур сверху даёт двойную рамку */
/* Активный пункт меню — единая градиентная кайма (общий блок «Единое
   выделение» ниже) и иконка в акценте. Заливки-плашки нет. Жирность НЕ
   меняем: скачок 500→600 на тёмном фоне рендерился грязным смазанным
   пятном — активность читается по цвету и кайме, не по весу. */
.nav-item.active {
  background: transparent;
  color: var(--text-primary);
  box-shadow: none;
}
.nav-item.active > .icon { color: var(--primary-hover); }

/* === Альтернативные стили выделения меню (личная настройка браузера,
   body-класс, выбор на /menu-preview.html). Дефолт без класса — «Полоса»
   (правила выше). === */
/* «Свечение»: НИ рамки, НИ заливки — текст и иконка в светлом индиго +
   мягкое радиальное свечение из-за пункта (тот же приём, что .dock-btn::before).
   Прежний стиль панели. */
.menu-glow .nav-item.active {
  color: #e0e7ff;
  background: transparent;
  font-weight: inherit;
}
.menu-glow .nav-item.active::before {
  inset: -2px; width: auto; border-radius: inherit;
  background: radial-gradient(ellipse 90% 120% at 25% 50%, rgba(129, 140, 248, .22), transparent 70%);
  box-shadow: none;
}
.menu-glow .nav-item.active > .icon { color: #a5b4fc; }
/* «Плашка»: стеклянная заливка + фирменная градиентная рамка — выделение
   в том же языке, что hover карточек сайтов. */
.menu-pill .nav-item.active { background: var(--surface-glass); color: var(--text-primary); }
.menu-pill .nav-item.active::before {
  inset: 0; width: auto; padding: 1px; border-radius: inherit; box-shadow: none;
  background: linear-gradient(135deg, var(--primary-hover), var(--primary) 55%, #22d3ee);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.menu-pill .nav-item.active > .icon { color: var(--primary-hover); }
/* «Призрак»: никакого фона вообще — яркий текст + иконка в акценте,
   минимализм Vercel. Без скачка жирности (см. .nav-item.active выше). */
.menu-ghost .nav-item.active { color: var(--text-primary); background: transparent; }
.menu-ghost .nav-item.active::before { display: none; }
.menu-ghost .nav-item.active > .icon { color: var(--primary-hover); }
/* Фирменная градиентная рамка выделения (индиго → циан) — только через
   mask-composite псевдоэлемент: градиент кладётся в 1px кромку, центр
   вырезается маской, фон элемента остаётся нетронутым. Тот же приём, что
   у .nav-item.active::before. Простой border-box градиент без маски заливал
   бы весь элемент — запрещён. Применяется к карточкам (меню — в стиле дока, выше). */
.site-card:hover::before,
.feature-card:hover::before,
.site-card:has(.site-select:checked)::before,
.site-card:has(.bk-select:checked)::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit; padding: 1px;
  background: linear-gradient(135deg, var(--primary-hover), var(--primary) 55%, #22d3ee);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.nav-item .cnt { margin-left: auto; font-size: var(--fs-micro); color: var(--text-tertiary); }
/* Скролл — на этом уровне (а не на <main>), чтобы полоса прокрутки отнимала
   ширину у топбара и у контента поровну: раньше скроллился только <main>,
   и его контент оказывался у'же топбара на ширину скроллбара — поиск и
   переключатель вида в топбаре «съезжали» левее правого края сетки карточек. */
.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; scrollbar-gutter: stable; } /* stable: появление/исчезновение скроллбара (полл, ресайз) не дёргает ширину контента */

.app-topbar {
  height: 64px;
  flex-shrink: 0;
  /* Та же двухколоночная сетка и тот же gap, что у .overview-layout ниже —
     раньше здесь был display:flex с justify-content:space-between, из-за
     чего правая ячейка топбара не резервировала тот же зазор, что грид
     тела, и левая колонка в шапке получалась на sp-5 шире, чем колонка
     карточек внизу: переключатель сетка/список (он живёт в левой ячейке,
     header-actions прижаты к её правому краю через .sp) утыкался прямо в
     поиск вместо границы карточек. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--aside-col);
  /* gap = --page-gutter, как у .overview-layout ниже: иначе левая колонка
     шапки и колонка карточек расходятся по правому краю, и переключатель
     сетка/список не встаёт по границе сетки карточек. */
  gap: var(--page-gutter);
  align-items: center;
  padding: 0 var(--page-gutter);
  /* Топбар — та же поверхность, что body с его фон-пятнами и шумом, без
     разделительной линии, без своей заливки и БЕЗ блюра: единый холст,
     а не плашка поверх подложки. */
  border-bottom: 0;
  background: transparent;
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}
/* Global server-status strip that replaces the old per-page breadcrumb — same
   content on every view, so the topbar never changes height when navigating. */
.topbar-actions { display: flex; align-items: center; gap: var(--sp-4); }

/* Главное действие раздела — в топбаре: пустой хост, куда moveViewHeader()
   (app.js) физически переносит .page-header текущего раздела при каждом
   switchView(). Ниже — не стили нового компонента, а переопределение того,
   как обычный «блочный» .page-header (задуман для тела страницы, с отступом
   снизу) ведёт себя, оказавшись строкой в 64px топбаре. */
/* overflow:visible, не hidden: popover «Вид» (фильтр/сортировка сайтов) живёт
   внутри переехавшего page-header и hidden его обрезал по краю топбара —
   панель открывалась «вникуда». Эллипсисы длинных текстов не пострадают:
   overflow:hidden задан точечно на самих .topbar-page-actions p/h1. */
.topbar-page-actions { display: flex; align-items: center; gap: var(--sp-3); flex: 1; min-width: 0; overflow: visible; }
.topbar-page-actions .page-header { margin: 0; flex: 1; display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
/* Шапка страницы сайта — flex-КОЛОНКА (.site-detail-header): общий
   align-items:center выше центрирует её детей по ГОРИЗОНТАЛИ, и строка
   иконок уезжала в середину топбара. Возвращаем влево — ровно над левым
   бордером превью сайта, на одной линии с поиском справа. */
.topbar-page-actions .site-detail-header { align-items: flex-start; }
.topbar-page-actions .page-header > div:first-child { min-width: 0; }
.topbar-page-actions .page-title-row { margin: 0; }
.topbar-page-actions h1 { font-size: var(--fs-h3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Центр топбара — единая статус-зона (активность + сервисные сообщения):
   ОДНА пилюля с рамкой, внутренние элементы — голые, без своих рамок/фона.
   Абсолютно центрирована поверх сетки топбара (не занимает и не сдвигает
   ячейки), внутри — flex-ряд: элементы не перекрывают друг друга. */
.topbar-center {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: var(--z-pop);
  display: flex; align-items: center; gap: var(--sp-2);
  height: var(--control-h-sm); padding: 0 var(--sp-3);
  border-radius: var(--radius-badge);
  background: var(--surface-glass); border: 1px solid var(--border);
  backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
}
/* Пустая зона (нет фоновых задач) — пустую рамку не рисуем. */
.topbar-center:not(:has(#activityWrap:not([hidden]))) { display: none; }
/* Внутри зоны пилюля без своей рамки/стекла — рамка одна, у зоны. */
.topbar-center .task-status {
  border: 0; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none;
  height: auto; padding: 0 var(--sp-1); box-shadow: none;
}
.topbar-center .task-status::after { display: none; }
/* Глобальный индикатор процесса: стеклянная пилюля с пульсирующей точкой
   и бегущей линией снизу (indeterminate-прогресс). */
.task-status {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: var(--control-h-sm); padding: 0 var(--sp-3);
  border-radius: var(--radius-badge);
  background: var(--surface-glass); border: 1px solid var(--border);
  backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
  font-size: var(--fs-caption); font-weight: 400; color: var(--text-secondary);
  overflow: hidden; white-space: nowrap;
}
.task-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--primary); flex-shrink: 0; animation: taskPulse 1s ease-in-out infinite; }
.task-status::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--primary), transparent);
  background-size: 45% 100%; background-repeat: no-repeat;
  animation: taskSlide 1.3s linear infinite;
}
@keyframes taskPulse { 50% { opacity: .3; } }
@keyframes taskSlide { from { background-position: -50% 0; } to { background-position: 150% 0; } }

/* Панель активности: выпадающий список под пилюлей — позиционируется от
   .topbar-center (containing block): под низ статус-зоны, по её центру. */
/* display:contents — wrap НЕ грид-элемент топбара: оба ребёнка (пилюля
   .task-status и .activity-panel) живут внутри .topbar-center: пилюля —
   flex-итем, панель — absolute от той же обёртки. [hidden] перекрываем явно:
   author-стиль display:contents сильнее UA-правила [hidden]{display:none}. */
.activity-wrap { display: contents; }
.activity-wrap[hidden] { display: none; }
/* Рамка ровно одна — у зоны .topbar-center; пилюля внутри своей не рисует
   (иначе на активной задаче видна двойная обводка). */
.activity-wrap .task-status { cursor: pointer; font: inherit; }
.activity-panel { top: calc(100% + 10px); right: auto; left: 50%; transform: translateX(-50%); width: 360px; }
.act-row { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-1); font-size: var(--fs-caption); }
.act-row + .act-row { border-top: 1px solid var(--border); }
.act-row .task-dot { animation: taskPulse 1s ease-in-out infinite; }
.act-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); }
.act-done .act-label { color: var(--text-secondary); }
.act-row .muted { flex-shrink: 0; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.act-ok { color: var(--success); font-weight: 700; }
.act-bad { color: var(--danger); font-weight: 700; }
.act-empty { padding: var(--sp-3) var(--sp-1); font-size: var(--fs-caption); }
/* Кликабельные строки — ведут на страницу операции (сайт+вкладка/раздел). */
.act-link { cursor: pointer; border-radius: var(--radius-control); }
.act-link:hover { background: var(--surface-hover); }
.topbar-page-actions p { margin: 0; font-size: var(--fs-micro); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.topbar-page-actions .header-actions { margin-left: auto; flex-shrink: 0; }
/* Правая ячейка топбара — той же ширины (--aside-col), что правая панель: поиск
   прижат к её левому краю (тот же X, что левый край .overview-aside), а не
   болтается в общей кучке с колокольчиком/аватаркой. */
.topbar-aside-col { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); width: var(--aside-col); flex-shrink: 0; }
.topbar-aside-col-r { display: flex; align-items: center; gap: var(--sp-2); }
/* Поиск — утопленное поле с подписью и хоткеем (как в макете), а не просто
   иконка: акцентный глиф + подсветка на hover, чтобы не сливалось с фоном. */
.topbar-search {
  display: flex; align-items: center; gap: 6px;
  height: var(--control-h-sm); width: 150px; padding: 0 var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--radius-control);
  background: rgba(0, 0, 0, .28); color: var(--text-primary);
  /* Тихое поле в покое (тонкая кромка, без постоянного свечения); hover —
     градиентная кайма из общего блока «Единое выделение». */
  box-shadow: none;
  font: inherit; font-size: var(--fs-caption); cursor: pointer; flex-shrink: 0;
  transition: box-shadow var(--dur) var(--ease);
}
.topbar-search .icon { color: var(--primary); flex-shrink: 0; }
.topbar-search:hover { border-color: transparent; }
.topbar-search kbd {
  margin-left: auto; background: var(--surface-hover); border-radius: 4px;
  padding: 1px 5px; font-size: 10px; font-family: inherit; color: var(--text-tertiary);
}
html[data-theme="light"] .topbar-search { background: rgba(0, 0, 0, .05); box-shadow: inset 0 1px 2px rgba(16, 24, 40, .08); }
/* Иконки справа топбара (колокольчик, настройки) — прозрачные как .ov-icon-btn,
   без рамки .btn; hover — общая градиентная кайма. */
.topbar-aside-col-r .btn-icon { background: transparent; box-shadow: none; border: 0; }
.whoami { font-size: var(--fs-caption); color: var(--text-secondary); }
.whoami b { color: var(--text-primary); font-weight: 500; }

main {
  flex: 1;
  min-height: 0;
  width: 100%; /* контент доходит до того же правого края, что и топбар */
  /* Верх — компактные 16px (sp-4): меньше мёртвого воздуха между шапкой и
     контентом; края — единый --page-gutter, как у сайдбара слева и дока
     снизу. */
  padding: var(--sp-4) var(--page-gutter);
  /* Снизу — запас под плавающий док (36px кнопки + 10px паддинг + отступ до
     низа), иначе последний элемент страницы (например, «Пополнить баланс»)
     прячется под него. */
  padding-bottom: calc(var(--page-gutter) + 64px);
  /* Скролл — на .app-main (см. выше), не здесь: иначе своя полоса прокрутки
     отъедала бы ширину только у <main>, а топбар оставался бы шире контента. */
}
/* Главная сама рассчитана по высоте ровно до нижней линии дока (#view-overview),
   поэтому резерв под док у main здесь лишний — иначе сумма превышает высоту
   экрана, появляется вертикальный скроллбар, и его 10px-гуттер сдвигает правый
   край контента относительно топбара (тот не скроллится) — «лесенка». */
main:has(> #view-overview:not([hidden])) { padding-bottom: var(--page-gutter); }

.view { display: block; }

/* Микро-переход между разделами: fade + лёгкий slide-up при появлении view
   (снятие hidden). Тот же паттерн, что dialog-in у диалогов. */
main > .view:not([hidden]) { animation: view-in var(--dur) var(--ease); }
@keyframes view-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  main > .view:not([hidden]) { animation: none; }
  dialog[open] { animation: none; }
}
.view[hidden] { display: none; }

/* ==========================================================================
   Floating dock — быстрые действия, всегда под рукой внизу по центру.
   absolute к .app-shell (он position:relative), а не fixed к окну: иначе на
   узком/невысоком вьюпорте док уезжает за нижний край независимо от контента.
   ========================================================================== */
.dock {
  position: absolute; bottom: var(--page-gutter); left: 50%; transform: translateX(-50%);
  z-index: var(--z-pop); display: flex; align-items: center; gap: var(--sp-1);
  padding: 5px; border-radius: 14px; overflow: hidden;
  isolation: isolate;
  /* Матовое стекло: blur 14px достаточен, чтобы док читался слоем ПЕРЕД
     контентом, но не превращался в плиту (blur 36px над тёмным канвасом
     давал непрозрачную плашку при любой альфе; без блюра док растворялся —
     текст карточки читался насквозь). Свечение прижато к доку (18px,
     низкая альфа): размазанное на 44px читалось дешёвой подсветкой. */
  background: rgba(10, 13, 19, .22);
  border: 0;
  backdrop-filter: blur(14px) saturate(1.3);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .4),
    -5px 4px 18px rgba(99, 102, 241, .14),
    5px -3px 18px rgba(34, 211, 238, .09);
}
html[data-theme="light"] .dock { background: rgba(255, 255, 255, .35); }
html[data-theme="light"] .dock {
  border-color: rgba(15, 23, 42, .1);
  box-shadow: var(--shadow-md);
}
.dock-btn {
  width: 36px; height: 36px; display: grid; place-items: center; border: 0; border-radius: 10px;
  background: transparent; color: var(--text-secondary); cursor: pointer; position: relative; isolation: isolate;
  transition: color var(--dur-glow) var(--ease);
}
.dock-btn .icon { width: 18px; height: 18px; }
.dock-btn:hover { color: var(--text-primary); transition-duration: var(--dur); }
.dock-btn.acc { color: var(--primary); }
.dock-btn.acc:hover { color: var(--text-primary); }
.dock-sep { width: 1px; height: 18px; margin: 0 3px; background: linear-gradient(180deg, transparent, var(--border-hover), transparent); }
/* Обёртка контекстной иконки «Действия сайта» — сама не флекс-элемент,
   её .dock-sep/.dock-btn должны сидеть в общем ряду дока с тем же gap,
   что и остальные кнопки, поэтому не inline/block, а contents. */
#dockSiteCtxWrap:not([hidden]) { display: contents; }
/* Авто-скрытие: скролл вниз — док уплывает за нижний край, скролл вверх (или
   упор в начало) — возвращается. Класс вешает scroll-обработчик .app-main
   в app.js. Поверх контента в середине скролла он больше не висит. */
.dock { transition: transform var(--dur-slow) var(--ease), opacity var(--dur-slow) var(--ease); }
.dock.dock-hidden { transform: translateX(-50%) translateY(calc(100% + var(--page-gutter) + 10px)); opacity: 0; pointer-events: none; }
@media (max-width: 700px) { .dock { display: none; } }

/* ==========================================================================
   Icons — inline sprite, one consistent outline set (see #icon-sprite in html)
   ========================================================================== */

.icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.3; /* дословно из макета, было 2 — визуально жирнее/крупнее */
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* .icon-sm нигде не была объявлена — использовалась в разметке сотни раз
   (icon icon-sm) без всякого эффекта, все "маленькие" иконки рендерились в
   полный размер базового .icon. Дословно из макета: 14px. */
.icon-sm { width: 14px; height: 14px; }
.icon-lg { width: 22px; height: 22px; }
.icon-xl { width: 32px; height: 32px; }

/* ==========================================================================
   Buttons
   ========================================================================== */

/* Вторичная кнопка — не залита и в покое вообще без контура (иначе рамки
   вокруг каждой иконки в топбаре читаются как «прямоугольники», а не как
   отдельные кнопки) — кольцо появляется только на hover, как подсказка,
   что элемент интерактивный. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--control-h-sm);
  padding: 0 var(--sp-4);
  border-radius: var(--radius-control);
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  font-size: var(--fs-caption);
  font-weight: 600;
  font-family: var(--font-display); /* выбранная система: кнопки — Geist 600 */
  white-space: nowrap;
  /* Дословно из макета: контур виден в покое (не только на hover) —
     inset 0 0 0 1px rgba(255,255,255,.11). .btn-ghost остаётся без него
     (box-shadow:none), это отдельный вариант и в макете, и здесь. */
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .11);
  /* Свечение/заливка затухают длинно (--dur-glow), на :hover duration ужат —
     вход резкий, выход медленный. Transform (press) всегда быстрый. */
  transition: background-color var(--dur-glow) var(--ease), box-shadow var(--dur-glow) var(--ease),
    color var(--dur-glow) var(--ease), transform var(--dur) var(--ease), filter var(--dur-glow) var(--ease);
}
.btn:hover { color: var(--text-primary); background-color: rgba(255, 255, 255, .04); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22); transition-duration: var(--dur), var(--dur), var(--dur), var(--dur), var(--dur); }
.btn:active { transform: scale(.985); }
.btn:disabled { opacity: .4; cursor: not-allowed; transform: none; box-shadow: none; }
/* Клик мышью не должен оставлять видимый focus-ring — только клавиатурная
   навигация (:focus-visible). Без явного :focus{outline:none} браузер (в
   первую очередь Firefox) рисует свой прямоугольный focus-контур поверх
   скруглённой кнопки при обычном клике — читалось как случайная рамка. */
.btn:focus { outline: none; }
.btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Главная кнопка — дословно из макета (.btn-primary), значения не адаптированы:
   тело #151a26 (не --surface-elevated — в макете это отдельный хардкод-тон,
   темнее любой поверхности приложения), рамка — линейный градиент со стопом
   на 50%, тень 0 4px 14px rgba(0,0,0,.45). */
.btn-primary {
  background: linear-gradient(#151a26, #151a26) padding-box,
    linear-gradient(135deg, #818cf8, #4338ca 50%, #22d3ee) border-box;
  border: 1px solid transparent;
  color: #dfe4ff;
  font-weight: 600;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
  --ring: 0deg;
}
/* Hover дословно из макета: живой контур — conic-gradient крутится через
   --ring (свойство типизировано @property, чтобы браузер анимировал угол,
   а не мгновенно скакал), тело кнопки не двигается. */
@property --ring { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
.btn-primary:hover {
  color: #fff;
  background: linear-gradient(#151a26, #151a26) padding-box,
    conic-gradient(from var(--ring), #818cf8, #22d3ee, #4338ca, #818cf8) border-box;
  box-shadow: 0 6px 20px rgba(99, 102, 241, .3);
  animation: btn-primary-ring 3.2s linear infinite;
}
@keyframes btn-primary-ring { to { --ring: 360deg; } }
.btn-primary:active { filter: brightness(.95); animation: none; }

.btn-danger { background: transparent; color: var(--danger); border: 0; box-shadow: none; }
.btn-danger:hover { background-color: var(--danger-wash); box-shadow: 0 0 18px var(--danger-wash); }

.btn-ghost { background: transparent; border: 0; box-shadow: none; }
.btn-ghost:hover { background-color: var(--surface-hover); color: var(--text-primary); box-shadow: none; }

.btn-sm { height: var(--control-h-sm); padding: 0 var(--sp-3); font-size: var(--fs-micro); }

.btn-icon { width: var(--control-h-sm); padding: 0; }
/* Полноширинная кнопка (форма входа): высота и кегль как у полей ввода
   рядом (control-h/fs-body), а не «мелкая» control-h-sm — иначе кнопка
   визуально проваливалась под инпутами. */
.btn-block { width: 100%; height: var(--control-h); font-size: var(--fs-body); }

/* ==========================================================================
   Form primitives — the fix for the "staircase": every field is a column
   (label above input, hint below), so a grid of fields always keeps every
   input on the same baseline regardless of hint text length.
   ========================================================================== */

.field { display: flex; flex-direction: column; gap: var(--sp-2); margin: 0 0 var(--sp-4); }
.field-label { font-size: var(--fs-caption); font-weight: 400; color: var(--text-secondary); }
.field-full { grid-column: 1 / -1; }

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

input, select, textarea {
  font-family: inherit;
}

.field input,
.field select,
.field textarea,
.input {
  height: var(--control-h);
  padding: 0 var(--sp-4);
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-primary);
  font-size: var(--fs-body);
  width: 100%;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.field textarea { height: auto; padding-top: var(--sp-3); padding-bottom: var(--sp-3); resize: vertical; }
.field input:hover, .field select:hover { border-color: var(--border-hover); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--primary);
  background: var(--surface-elevated);
  box-shadow: 0 0 0 3px var(--primary-wash);
}
.field input::placeholder { color: var(--text-tertiary); }
.field label.checkbox { flex-direction: row; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--text-primary); }
.field label.checkbox input { width: auto; height: auto; }

.input-group { position: relative; display: flex; align-items: center; }
.input-group input { padding-right: 76px; }
/* Одна иконка-кнопка внутри поля — не резервировать место под две. */
.input-group.one-action input { padding-right: calc(var(--control-h-sm) + var(--sp-3)); }
/* Три иконки (доступ к БД: глаз/генерация/копия) — резерв под три кнопки,
   иначе последняя вываливалась за правый край инпута «вторым рядом». */
.input-group.three-actions input { padding-right: calc(3 * 32px + 2 * 2px + 12px); }
.input-group-actions { position: absolute; right: 6px; display: flex; gap: 2px; }
.input-group-actions .btn-icon { width: 32px; height: 32px; }

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--sp-6);
}
@media (max-width: 720px) { .form-grid { grid-template-columns: 1fr; } }
.form-grid.cols-2-1 { grid-template-columns: 2fr 1fr; }
@media (max-width: 720px) { .form-grid.cols-2-1 { grid-template-columns: 1fr; } }

/* Compact inline row of short controls (DNS record, mailbox login+password,
   tariff price/currency/date) — same field system, laid out horizontally
   instead of one-off flex/gap/margin per instance. Widths come from this
   fixed set (grow/wide/fixed), never a one-off pixel guess. */
.field-row { display: flex; gap: var(--sp-3); flex-wrap: wrap; align-items: flex-end; margin-bottom: var(--sp-4); }
.field-row > label, .field-row > .field { margin: 0; flex: 1 1 200px; }
.field-row > label:not(.checkbox):not(.switch) {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  font-size: var(--fs-caption);
  font-weight: 400;
  color: var(--text-secondary);
}
.field-row .field-wide { flex: 2 1 240px; }
.field-row .field-fixed { flex: 0 0 110px; }
.field-row .btn { flex: 0 0 auto; }
/* Self-contained input styling so a .field-row looks right whether it sits
   inside a <dialog> or a plain .card — doesn't lean on ambient context rules.
   В селекторе и поля деплоя (.sd-gh-row/.sd-copy-row) и инлайн-редактор
   DNS-записи (.dns-table input): иначе они получают голый браузерный стиль
   с желтоватым фоном — вне нашего дизайн-языка. */
.field-row input, .field-row select, .sd-gh-row input, .sd-copy-row input, .dns-table input {
  height: var(--control-h);
  padding: 0 var(--sp-4);
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-primary);
  font-size: var(--fs-body);
  font-family: inherit;
  width: 100%;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.field-row input:focus, .field-row select:focus, .sd-gh-row input:focus, .sd-copy-row input:focus, .dns-table input:focus {
  outline: none;
  border-color: var(--primary);
  background: var(--surface-elevated);
  box-shadow: 0 0 0 3px var(--primary-wash);
}
.field-row input::placeholder, .sd-gh-row input::placeholder, .sd-copy-row input::placeholder, .dns-table input::placeholder { color: var(--text-tertiary); }

/* Textarea вне .field-row (сырой конфиг/скрипт-поля) — тот же язык инпутов,
   не голый браузерный textarea без скругления. */
.field-textarea {
  display: block; width: 100%; padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-control); border: 1px solid var(--border);
  background: var(--surface); color: var(--text-primary);
  font-family: inherit; font-size: var(--fs-body); resize: vertical;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.field-textarea:focus {
  outline: none; border-color: var(--primary); background: var(--surface-elevated);
  box-shadow: 0 0 0 3px var(--primary-wash);
}
.field-textarea::placeholder { color: var(--text-tertiary); }

/* Standalone checkbox field, outside the .field/dialog contexts (settings blocks). */
label.checkbox { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-body); color: var(--text-primary); cursor: pointer; }
label.checkbox input { width: auto; height: auto; }

/* Sub-section heading inside a card (e.g. "Домены с почтой" inside the Mail card) —
   replaces one-off <h3 style="margin:...">. */
.section-title { margin: var(--sp-5) 0 var(--sp-3); display: flex; align-items: center; gap: var(--sp-2); }
.section-title:first-child { margin-top: 0; }

/* Per-site-type panel in "New Site" — each type gets its own dedicated block,
   swapped in/out as a whole rather than showing/hiding a few fields inside one
   shared shape. */
.type-panel { margin-bottom: var(--sp-3); }
.type-panel:last-of-type { margin-bottom: 0; }

/* Header + inline action on one line (dialog title with a button, card sub-header
   with a button) — replaces one-off <div style="display:flex;justify-content:..."> */
.title-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.title-row h2, .title-row h3 { margin: 0; }

.page-header.with-action { display: flex; align-items: flex-end; justify-content: space-between; }

.scroll-area { overflow: auto; }
.scroll-area-lg { overflow: auto; max-height: 70vh; }

.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }

/* Без border-top: разделитель над кнопками в форме создания сайта был лишней
   горизонтальной полосой — кнопки держатся отступом, а не чертой. */
.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}

/* Segmented control (site type etc.) — same markup as before (.chip-group/.chip),
   restyled as one cohesive control instead of loose pill buttons. */
.chip-group {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  margin-bottom: var(--sp-5);
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--control-h-sm);
  padding: 0 var(--sp-4);
  border-radius: var(--radius-sm);
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-caption);
  font-weight: 400;
  cursor: pointer;
  transition: background var(--dur-glow) var(--ease), color var(--dur-glow) var(--ease), box-shadow var(--dur-glow) var(--ease);
}
.chip:hover { color: var(--text-primary); transition-duration: var(--dur); }
.chip.active { background: var(--primary-wash); color: var(--primary); }

/* Пилюли выбора типа сайта в диалоге создания: отдельно стоящие кнопки
   с воздухом, без общей подложки-рамки. Активная получает фирменную
   градиентную кайму и лёгкое свечение — тот же язык, что у карточек. */
.chip-group.pills { gap: var(--sp-2); padding: 0; background: none; border: none; }
.chip-group.pills .chip {
  position: relative;
  isolation: isolate;
  border: 1px solid var(--border);
  border-radius: var(--radius-badge);
  background: var(--surface-glass);
  transition: border-color var(--dur-glow) var(--ease), color var(--dur-glow) var(--ease), box-shadow var(--dur-glow) var(--ease);
}
.chip-group.pills .chip:hover { border-color: var(--border-hover); color: var(--text-primary); transition-duration: var(--dur); }
.chip-group.pills .chip.active {
  border-color: transparent;
  color: var(--text-primary);
  box-shadow: 0 0 18px rgba(99, 102, 241, .18);
}
/* Кайма живёт всегда (opacity 0) — активность включается/гаснет плавно,
   а не щелчком (та же механика, что у иконок-вкладок). Двухслойная
   (::before градиент + ::after непрозрачная крышка с inset 1px), НЕ
   mask-composite: маска даёт рваный пиксельный край. */
.chip-group.pills .chip::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  border-radius: inherit;
  background: linear-gradient(135deg, var(--primary-hover), var(--primary) 55%, #22d3ee);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-glow) var(--ease);
}
.chip-group.pills .chip::after {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: calc(var(--radius-badge) - 1px);
  background: var(--surface);
  pointer-events: none;
}
.chip-group.pills .chip.active::before { opacity: 1; transition-duration: var(--dur); }

/* Иконки-вкладки страницы сайта — та же роль, что .badge-icon рядом с ними
   (статус-иконки типа/SSL/WAF у заголовка): один визуальный язык на всю
   строку, а не сегмент-контрол в рамке рядом с плоскими кружками. Никакого
   контейнера — просто ряд одинаковых иконок-кнопок, активная выделена
   заливкой того же тона, что и везде (surface-elevated + primary-акцент). */
.chip-group-icon { margin-bottom: 0; gap: 6px; padding: 0; background: none; border: none; border-radius: 0; }
.chip-group-icon .chip {
  width: var(--control-h-sm); height: var(--control-h-sm); padding: 0; gap: 0;
  justify-content: center;
  position: relative; isolation: isolate;
  /* text-secondary, не tertiary: на тёмном стекле #5c6b74 превращал иконки
     в едва различимые призраки (жалоба «хуёво видно»). */
  border-radius: var(--radius-badge); background: transparent; color: var(--text-secondary);
}
/* Градиентная кайма — НЕ mask-composite: маска режется попиксельно без
   сглаживания и выглядит рваной. Вместо неё два слоя: ::before — градиент
   на всю кнопку (opacity анимируется), ::after — непрозрачная крышка
   интерьера с inset 1px. Кайма — реально отрисованные пиксели с нормальным
   AA, гладкая на любой плотности экрана. */
.chip-group-icon .chip::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  border-radius: inherit;
  background: linear-gradient(135deg, var(--primary-hover), var(--primary) 55%, #22d3ee);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-glow) var(--ease);
}
.chip-group-icon .chip::after {
  content: "";
  position: absolute;
  inset: 1px;
  z-index: -1;
  border-radius: calc(var(--radius-badge) - 1px);
  background: var(--surface-hover);
  pointer-events: none;
  /* Крышка НЕ постоянная: в покое вкладка прозрачная (как действия справа),
     плашка+кайма проявляются на hover/active. */
  opacity: 0;
  transition: opacity var(--dur-glow) var(--ease);
}
/* Hover — приглушённая кайма + крышка интерьера (градиент не проступает). */
.chip-group-icon .chip:hover {
  color: var(--text-primary);
  box-shadow: 0 0 10px rgba(99, 102, 241, .10);
}
.chip-group-icon .chip:hover::before { opacity: .55; transition-duration: var(--dur); }
.chip-group-icon .chip:hover::after { opacity: 1; transition-duration: var(--dur); }
/* Активная иконка — та же кайма на полной силе + свечение. */
.chip-group-icon .chip.active {
  color: var(--text-primary);
  box-shadow: 0 0 14px rgba(99, 102, 241, .18);
}
.chip-group-icon .chip.active::before { opacity: 1; transition-duration: var(--dur); }
.chip-group-icon .chip.active::after { opacity: 1; }

/* Главная кнопка страницы сайта — лейбленная «Панель сайта» слева от ленты
   иконок. Та же высота/радиус, что у иконок-вкладок (одна строка — один
   язык), с текстом. Активное состояние — РОВНО то же, что у иконок-вкладок:
   непрозрачный интерьер + градиентная кайма + свечение. Кайма — двухслойная
   (::before градиент + ::after крышка с inset 1px), НЕ mask-composite:
   маска даёт рваный пиксельный край. */
/* Полоса вторичной навигации и быстрых действий сайта — во всю ширину тела
   страницы, между шапкой и контентом вкладок. Слева разделы (чипы), справа
   действия (иконки); обе группы на одной базовой линии. */
/* Споты-якоря переноса subbar — не участвуют в раскладке (перенос через
   spot.after(subbar) работает и с display:none). */
/* Страница сайта: запас снизу под плавающий док (~48px + его отступ) —
   тайлы/контент не должны уезжать под него при прокрутке до конца. */
#view-site { padding-bottom: 72px; }

/* Тайлы быстрого доступа на «Обзоре» сайта (PHP/DNS/Почта/БД/WP/Бэкап/файлы):
   компактная сетка под инфо-рядом, во всю ширину. */
.ov-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--sp-2); grid-column: 1 / -1; }
.ov-tile {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--sp-2) var(--sp-2);
  border: 0; border-radius: var(--radius-control);
  background: transparent; color: var(--text-secondary);
  font: inherit; font-size: var(--fs-caption); font-weight: 500;
  text-align: left; cursor: pointer;
  transition: color var(--dur) var(--ease);
}
.ov-tile .icon { width: 16px; height: 16px; color: var(--primary); }
.ov-tile small { font-size: var(--fs-micro); color: var(--text-tertiary); font-weight: 400; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-tile > span { color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-tile:hover { color: var(--text-primary); }
.ov-tile:hover .icon { color: var(--primary-hover); }
/* Лента операций — полный ряд сетки обзора (.site-detail-layout) под превью
   и инфо-карточкой. */
#siteDetailEventsCard { grid-column: 1 / -1; }

/* Лента последних операций сайта (аудит-лог по домену). */
.sd-events { display: flex; flex-direction: column; }
.sd-event { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); }
.sd-event:last-child { border-bottom: none; }
.sd-event .icon { width: 14px; height: 14px; color: var(--text-tertiary); flex: none; }
.sd-event-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sd-event-time { margin-left: auto; flex: none; font-size: var(--fs-caption); color: var(--text-tertiary); font-variant-numeric: tabular-nums; }

/* ==========================================================================
   Целостность файлов / FIM (вкладка «Безопасность»)
   ========================================================================== */
.fim-badge-crit { background: var(--danger-wash); color: var(--danger); font-weight: 600; }
.fim-entry { padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); font-size: var(--fs-caption); }
.fim-entry:last-child { border-bottom: none; }
.fim-entry summary { cursor: pointer; }
.fim-cat { margin: var(--sp-2) 0 var(--sp-2) var(--sp-3); }
.fim-cat ul { margin: var(--sp-1) 0 0; padding-left: 1.2em; max-height: 220px; overflow-y: auto; }
.fim-cat li { overflow-wrap: anywhere; }

/* ==========================================================================
   WordPress-вкладка (гигиена: ядро, плагины, темы, checksums)
   ========================================================================== */
.wp-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.wp-head-actions { display: flex; align-items: center; gap: var(--sp-2); }
.wp-section { margin-top: var(--sp-5); }
.wp-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-2); }
.wp-section-head h4 { margin: 0; font-size: var(--fs-body); font-weight: 600; color: var(--text-secondary); }
.wp-table { width: 100%; border-collapse: collapse; }
.wp-table td { padding: 0 var(--sp-2) 0 0; height: var(--row-h); border-top: 1px solid var(--border); font-size: var(--fs-body); font-weight: 500; }
.wp-table tr:first-child td { border-top: none; }
.wp-row-actions { text-align: right; width: 1%; white-space: nowrap; }
/* Действия строки — ТРИ фиксированных слота [обновить][тумблер][удалить]:
   отсутствующий контрол занимает пустой слот той же ширины, поэтому столбики
   ровные по всем строкам (никакой лесенки иконок). */
.wp-slot {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--control-h-sm); height: var(--control-h-sm);
  vertical-align: middle; margin-left: var(--sp-2);
}
.wp-slot:first-child { margin-left: 0; }
/* Слот тумблера — по ширине трека (36px), а не квадрата кнопок. */
.wp-slot-sw { width: 36px; }
/* Результат проверки целостности: пустой — не занимает место вообще. */
.wp-checksum { margin-top: var(--sp-2); font-size: var(--fs-caption); }
.wp-checksum:empty { display: none; }
.wp-section-actions { display: flex; align-items: center; gap: var(--sp-2); }
/* label как кнопка загрузки — кликабельна целиком, input скрыт внутри */
.wp-head-actions label.btn, .wp-section-actions label.btn { cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
/* Бейдж «→ версия» — primary, обновление это акцентное действие. */
.wp-upd-badge { color: var(--primary); font-weight: 600; white-space: nowrap; }
.wp-status-on { color: var(--success); }
.wp-status-bad { color: var(--danger); font-weight: 600; }
.wp-bad-files { margin: var(--sp-2) 0 0; padding-left: var(--sp-5); max-height: 200px; overflow: auto; }
.wp-bad-files li { margin: 2px 0; }
.wp-bad-files code { font-size: var(--fs-caption); color: var(--text-secondary); }

/* ==========================================================================
   TTFB-спарклайн на «Обзоре» (S3): линия primary + заливка градиентом в
   прозрачность — тот же визуальный язык, что активные элементы интерфейса.
   ========================================================================== */
.ttfb-block { margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border); }
.ttfb-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.ttfb-vals { font-size: var(--fs-caption); white-space: nowrap; }
.ttfb-vals b { color: var(--text-primary); font-weight: 600; }
.ttfb-chart { display: block; width: 100%; height: 56px; }

/* ==========================================================================
   Консоль SSL-операции: терминал внутри карточки «Сертификат». Полный журнал
   (этапы/вывод certbot/рекомендации) + вердикт — никаких загадок для
   пользователя, ощущение полного контроля.
   ========================================================================== */
.ssl-console {
  margin-top: var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background: rgba(0, 0, 0, .38);
  overflow: hidden;
}
.ssl-console-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border);
  background: rgba(255, 255, 255, .03);
}
.ssl-console-title { font-size: var(--fs-caption); font-weight: 600; color: var(--text-primary); }
.ssl-console-elapsed { font-size: var(--fs-micro); color: var(--text-tertiary); white-space: nowrap; }
.ssl-console-body {
  max-height: 320px; overflow: auto;
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  line-height: 1.55;
}
.sl-line { display: flex; gap: var(--sp-3); padding: 1px 0; }
.sl-ts { flex-shrink: 0; width: 62px; color: var(--text-tertiary); user-select: none; }
.sl-text { min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
/* Уровни журнала: этап — яркий primary, вывод программы — приглушён,
   успех — зелёный, ошибка — красный, рекомендация — warning. */
.sl-step .sl-text { color: var(--primary); font-weight: 600; }
.sl-raw .sl-text { color: var(--text-tertiary); }
.sl-info .sl-text { color: var(--text-secondary); }
.sl-ok .sl-text { color: var(--success); font-weight: 700; }
.sl-err .sl-text { color: var(--danger); }
.sl-tip .sl-text { color: var(--warning); }
.sl-cursor {
  display: inline-block; width: 8px; height: 14px;
  background: var(--primary); border-radius: 1px;
  animation: slBlink 1s steps(2, start) infinite;
}
@keyframes slBlink { 50% { opacity: 0; } }
/* Вердикт: финальный аккорд операции. Успех — зелёное свечение, ошибка —
   красная плашка; читается с расстояния вытянутой руки. */
.ssl-console-verdict {
  display: flex; align-items: baseline; gap: var(--sp-3);
  padding: var(--sp-3);
  border-top: 1px solid var(--border);
}
.sl-verdict-text { font-family: var(--font-display); font-size: var(--fs-h2); font-weight: 700; letter-spacing: .04em; }
.ssl-console-verdict.is-bad { background: rgba(239, 68, 68, .08); }
.ssl-console-verdict.is-bad .sl-verdict-text { color: var(--danger); text-shadow: 0 0 24px rgba(239, 68, 68, .45); }
.sl-verdict-sub { font-size: var(--fs-caption); color: var(--text-secondary); }

/* ==========================================================================
   Cards / panels
   ========================================================================== */

/* Единый рецепт карточки: стекло + рамка 1px --border + паддинг --card-pad.
   Рамка у всех карточек одинаковая — монолитные ряды/столбцы читаются по
   границам, а не угадываются по теням. */
.panel, .card {
  /* Стекло: полупрозрачная подложка + blur — фон с пятнами читается сквозь
     карточку, материал совпадает с доком и правой панелью. Blur умеренный
     (14px): backdrop-filter пересчитывается при скролле, жирный блюр на
     десятке карточек убил бы слабые машины. */
  background: var(--surface-glass);
  background-image: var(--grad-surface);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--card-pad);
  margin-bottom: var(--sp-5);
  box-shadow: var(--edge-highlight);
}

.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: var(--sp-4); grid-auto-rows: minmax(var(--card-min-h), 1fr); } /* 232px — дословно из макета (.cards), было 280px; grid-auto-rows:minmax(min,1fr) — ВСЕ ряды одной высоты с единым минимумом (--card-min-h), карточки не «прыгают» ни от ряда к ряду, ни от раздела к разделу */

.header-actions { display: flex; align-items: center; gap: var(--sp-3); }

/* View mode switcher (table/grid) — same segmented look as .chip-group. */
/* Высота ровно как у соседних кнопок (фильтр/сортировка/поиск) — было
   32px кнопки + 3px паддинг с каждой стороны + 1px рамка = на 8px выше
   соседей. Без рамки, паддинг съедает часть той же control-h-sm, а не
   добавляется поверх нее. */
.view-toggle { display: inline-flex; align-items: center; gap: 2px; height: var(--control-h-sm); padding: 3px; background: var(--surface); border-radius: var(--radius-control); box-sizing: border-box; }
.view-toggle-btn {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 100%; border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-secondary); cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.view-toggle-btn:hover { color: var(--text-primary); }
/* Активный сегмент — единая градиентная кайма (общий блок ниже), без
   собственной заливки. */

/* Site card — grid-view counterpart to a .sites-table row, same data/actions. */
.site-card {
  position: relative;
  display: flex; flex-direction: column; gap: var(--sp-3);
  background: var(--surface-glass); background-image: var(--grad-surface);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--border); border-radius: var(--radius-card); padding: var(--card-pad);
  box-shadow: var(--lift);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
    transform var(--dur-slow) var(--ease), box-shadow var(--dur-slow) var(--ease);
}
/* Дословно из макета (.site:hover): свечение --lift-hi + glow-слой
   0 0 46px rgba(99,102,241,.32) + фирменная градиентная рамка (общий
   mask-composite шаблон, см. .nav-item.active::before). ФОН НЕ ТРОГАЕМ. */
.site-card:hover {
  box-shadow: var(--lift-hi), 0 0 46px rgba(99, 102, 241, .32);
}
/* Пустой слот «Добавить сайт» — последней карточкой в гриде, дословно из
   макета (.add-slot/.add-shot): тусклее обычных карточек в покое, обычная
   яркость и акцентный цвет плюсика на hover. */
.site-card.add-slot { opacity: .55; transition: opacity var(--dur) var(--ease); cursor: pointer; align-items: center; justify-content: center; }
.site-card.add-slot:hover { opacity: 1; }
/* Плюс один на всю карточку: flex:1 центрирует его по вертикали вне
   зависимости от высоты соседей в ряду. */
.add-shot { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--text-tertiary); padding: var(--sp-6) 0; transition: color var(--dur) var(--ease); }
.site-card.add-slot:hover .add-shot { color: var(--primary); }
.site-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
/* fs-body не fs-h3 и вес 500 не 600 — длинные технические домены
   (test.xxx-xxx-xxx-xxx.sslip.io) с более крупным жирным шрифтом переносились
   на вторую строку, ломая высоту всего ряда карточек. */
.site-card-domain { display: flex; align-items: center; gap: var(--sp-2); font-weight: 500; font-size: var(--fs-body); line-height: 1.2; flex: 1; min-width: 0; }
.site-card-domain a[data-open-detail] { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Выделение карточки — левая полоса-акцент на всю высоту карточки + клик
   в любом свободном месте (JS в sitesContainer) переключает этот чекбокс.
   Сам чекбокс не занимает места в раскладке — заголовку больше не нужен
   зазор под невидимый квадратик слева. */
.site-card > .site-select, .site-card > .bk-select { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
/* Выбрано — ТОЛЬКО фирменная градиентная рамка (общий mask-composite
   шаблон) + свечение, фон карточки не меняется вообще. */
.site-card:has(.site-select:checked), .site-card:has(.bk-select:checked) {
  box-shadow: var(--edge-highlight), 0 0 28px rgba(99, 102, 241, .35);
}
.site-card-preview-wrap, .site-card-head, .site-card-badges { cursor: pointer; }
.site-card-domain a[data-open-detail] { display: inline-block; line-height: 1.2; }
.site-card-badges { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.site-card-meta { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--fs-caption); color: var(--text-secondary); }
/* Действия карточки — файлы/DNS/SSL/почта. space-between: первая иконка
   стоит точно по левому краю контента карточки, последняя — по правому,
   промежутки между ними равные. Раньше кнопки были резиновыми (flex:1) и
   центрировали глифы внутри себя — крайние иконки висели в воздухе,
   не совпадая с краями карточки. */
.site-card-links { display: flex; justify-content: space-between; gap: var(--sp-2); padding-top: var(--sp-3); border-top: 1px solid var(--border); margin-top: auto; }
.site-card-links a.ov-icon-btn { text-decoration: none; }
.site-card-links .ov-icon-btn { flex: 0 0 auto; width: var(--control-h-sm); height: var(--control-h-sm); }
.site-card-links .ov-icon-btn .icon { width: 16px; height: 16px; }
.site-card-head-actions { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
.site-card-menu-wrap { flex-shrink: 0; }
/* Иконка-статус (тип сайта/SSL/WAF/rate-limit) — как badge, но чисто иконка,
   без слова: наводишь — title подсказывает, что это. */
.badge-icon {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  height: var(--badge-h); padding: 0 8px; min-width: var(--badge-h);
  border-radius: var(--radius-badge); background: var(--surface-hover); color: var(--text-tertiary);
  font-size: var(--fs-micro); font-variant-numeric: tabular-nums; text-decoration: none;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.badge-icon .icon { width: 14px; height: 14px; }
.badge-icon.ok { background: var(--success-wash, rgba(34, 197, 94, .14)); color: var(--success); }
.badge-icon.off { background: var(--surface-hover); color: var(--text-tertiary); }

/* ==========================================================================
   Единый язык взаимодействия — hover и "выбрано" везде одинаково: только
   свет/цвет, никогда движение (translateY/scale). Наведение на любую иконку-
   кнопку в приложении — один и тот же переход в тон светлее + мягкое кольцо
   тем же primary, что и акцентная кнопка. "Выбрано"/активно — та же primary-
   заливка + кольцо, чтобы вкладка, сегмент-контрол и пункт меню читались как
   один механизм, а не три разных. Раньше это было россыпью: где-то прыжок
   translateY, где-то scale, где-то shadow-sm, где-то ничего.
   Иконки-чипы (.chip-group-icon .chip) сюда не входят: у них градиентная
   кайма ::before (см. выше), wash-кольцо давало двойную рамку. */
a.badge-icon:hover {
  background: var(--surface-elevated); color: var(--text-primary);
  box-shadow: 0 0 0 3px var(--primary-wash);
}
/* Danger — та же общая кайма, отличается только цвет иконки. */
.ov-icon-btn.danger:hover { color: var(--danger); }
/* Активные чипы сюда больше не входят: у них фирменная градиентная кайма
   (::before mask-composite), а не inset-линия. */
.fm-list-item.active, .mail-domain.active {
  box-shadow: inset 0 0 0 1.5px var(--primary);
}
/* Иконки внутри ячейки таблицы — тот же ритм, что и в карточке: один gap,
   одна базовая линия, без «лесенки» из разновысоких элементов. */
.cell-icons { display: inline-flex; align-items: center; gap: 6px; }
.cell-icons .ov-icon-btn { flex-shrink: 0; }
/* Превью-скриншот сайта (thum.io, разрешён в CSP img-src) */
.site-card-preview {
  display: block; border-radius: var(--radius-control); overflow: hidden;
  border: 1px solid var(--border); aspect-ratio: 16 / 10; background: var(--surface-hover); /* дословно из макета (.shot), было 16/9 */
}
.site-card-preview img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; transition: transform .3s var(--ease); }
.site-card-preview:hover img { transform: scale(1.03); }
/* Нет скриншота — прямоугольник НЕ схлопываем (раньше display:none на
   .no-preview обрушал блок, и бейджи/действия под ним прыгали при каждом
   рефреше списка). Aspect-ratio держит геометрию, внутри — приглушённый
   глиф глобуса вместо картинки. */
.site-card-preview.no-preview img { display: none; }
.site-card-preview.no-preview,
.site-detail-preview.no-preview {
  background: var(--surface-hover) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c6b74' stroke-width='1.2'><circle cx='12' cy='12' r='9'/><path d='M3 12h18M12 3c3 3.5 3 14 0 18M12 3c-3 3.5-3 14 0 18'/></svg>") center / 36px 36px no-repeat;
}
/* Домен в шапке карточки — ссылка на подробную страницу, но выглядит как текст */
.site-card-domain a[data-open-detail] { color: inherit; text-decoration: none; }
.site-card-domain a[data-open-detail]:hover { color: var(--primary); }

/* Подробная страница сайта (view-site) */
.site-detail-badges { display: inline-flex; flex-wrap: wrap; gap: 6px; }
/* Сетка обзора: ряд 1 = превью | инфо (stretch — соседи одной высоты),
   ряд 2 = быстрые действия на всю ширину, ряд 3 = лента операций на всю
   ширину. Раньше колонки были независимыми flex-стопками — все границы
   шли лесенкой. */
.site-detail-layout {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: var(--sp-5);
  align-items: stretch;
}
.site-detail-layout .card { margin-bottom: 0; }
.site-detail-preview {
  display: block; border-radius: var(--radius-card); overflow: hidden;
  border: 1px solid var(--border); aspect-ratio: 16 / 10; background: var(--surface-hover);
  /* min-height: 100% — превью тянется до высоты инфо-карточки, если та
     выше 16/10; img с object-fit: cover заполняет без искажения.
     max-width: 100% — кап по ширине колонки: aspect-ratio + высокая инфо
     раньше раздували превью шире колонки, и оно наезжало на инфо-панель. */
  min-height: 100%;
  max-width: 100%;
}
.site-detail-preview img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; transition: transform .3s var(--ease); }
.site-detail-preview:hover img { transform: scale(1.02); }
.site-detail-preview.no-preview img { display: none; } /* сам блок не прячем — см. .site-card-preview.no-preview */
.site-detail-preview-wrap { position: relative; }
/* Селектор с потомком не случаен: единый блок выделения ниже по файлу задаёт
   всем .ov-icon-btn position: relative и с равной специфичностью побеждал —
   кнопка вываливалась из превью в поток под ним. */
.site-detail-preview-wrap > .site-detail-shot-refresh {
  position: absolute; top: var(--sp-2); right: var(--sp-2);
  width: var(--control-h-sm); height: var(--control-h-sm);
  background: rgba(0, 0, 0, .55); color: #fff; border-radius: var(--radius-sm);
  opacity: 0; transition: opacity var(--dur) var(--ease), background var(--dur) var(--ease);
}
.site-detail-preview-wrap:hover .site-detail-shot-refresh,
.site-detail-shot-refresh:focus-visible { opacity: 1; }
.site-detail-shot-refresh:hover { background: rgba(0, 0, 0, .8); color: #fff; }
/* Та же кнопка обновления скриншота, что на странице сайта — теперь и на
   карточке в гриде, тем же языком (появляется по hover, не торчит всегда). */
.site-card-preview-wrap { position: relative; }
.site-card-preview-wrap > .site-card-shot-refresh {
  position: absolute; top: var(--sp-2); right: var(--sp-2);
  background: rgba(0, 0, 0, .55); color: #fff;
  opacity: 0; transition: opacity var(--dur) var(--ease), background var(--dur) var(--ease);
}
.site-card-preview-wrap:hover .site-card-shot-refresh,
.site-card-shot-refresh:focus-visible { opacity: 1; }
.site-card-shot-refresh:hover { background: rgba(0, 0, 0, .8); color: #fff; }
.sd-info-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); font-size: var(--fs-body); }
.sd-info-row:last-child { border-bottom: none; }
/* Действия админки WP в строке инфо-панели — две иконки вплотную, не на всю ширину. */
.sd-admin-actions { display: inline-flex; gap: var(--sp-2); }
.sd-info-label { color: var(--text-secondary); font-size: var(--fs-caption); flex-shrink: 0; }
/* Первая строка инфо-панели: домен вместо пары «Сайт/Домен» — он и есть заголовок. */
.sd-info-domain { font-weight: 600; color: var(--text-primary); }
/* Ряд быстрых иконок под доменом: второстепенные действия сайта (деплой,
   логи, обслуживание, cron, клон, удаление). Тот же ритм строки, что у
   .sd-info-row, но контент прижат влево. */
.sd-info-actions { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); }
.sd-info-value { text-align: right; }
.sd-info-value .icon { width: 14px; height: 14px; vertical-align: -2px; color: var(--text-tertiary); }

/* Редкие действия (бэкапы/cron/SFTP/клон/поддомен/защита/кэш) — не в шапке
   и не вторым рядом навигации, а лентой иконок под превью (те же
   .ov-icon-btn, что в карточках сайтов — единый ряд одинаковых квадратов).
   Всё ещё попапы (см. openBackups/openCron/...), просто не оторваны от
   контекста сайта. Скан сюда не входит — он в «Безопасности» (Этап 8). */
.site-detail-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }

/* Git-деплой: readonly-поле + кнопка копирования в одну строку; история деплоев. */
/* Плитки параметров деплоя: компактная сетка вместо растянутых на всю
   строку форм — лейбл мелким, значение моно, копия иконкой. */
.sd-copy-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--sp-3); margin-top: var(--sp-2); }
.sd-copy-tile { display: flex; flex-direction: column; gap: var(--sp-1); }
.sd-copy-label { font-size: var(--fs-micro); color: var(--text-tertiary); }
.sd-copy-row { display: flex; align-items: center; gap: var(--sp-2); }.sd-copy-row input { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: var(--fs-caption); }
.sd-copy-row .chip { flex-shrink: 0; }
.sd-copy-row .chip.copied { color: var(--success); border-color: var(--success); }
.sd-deploy-history { display: flex; flex-direction: column; gap: var(--sp-2); font-size: var(--fs-caption); }
.sd-deploy-entry { display: flex; align-items: baseline; gap: var(--sp-2); }
.sd-deploy-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); flex-shrink: 0; transform: translateY(-1px); }
.sd-deploy-dot.err { background: var(--danger); }
.sd-deploy-err { color: var(--danger); }
.sd-deploy-rollback { margin-left: var(--sp-2); height: 24px; padding: 0 var(--sp-2); font-size: var(--fs-micro); }
/* GitHub-автопривязка: строка токена/логина и ручной режим под спойлером. */
.sd-gh-row { display: flex; align-items: center; gap: var(--sp-2); max-width: 560px; /* подключение токена — компактная строка, не форма на всю ширину */ }
/* Единый стандарт форм подключения: URL манифеста сервисов — та же
   компактная строка 560px, что и PAT GitHub, а не на всю карточку. */
#servicesReposCard .field-row { max-width: 560px; }
.sd-gh-row input { flex: 1; min-width: 0; }
.sd-gh-head { justify-content: space-between; margin-bottom: var(--sp-2); max-width: none; }
.field-row > label.sd-gh-repo { flex: 0 1 420px; }
/* Деплой: две равные отдельные панели-способы привязки репозитория, под
   ними на всю ширину — карточка настройки вебхука и истории деплоев. */
.sd-deploy-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: var(--sp-4); align-items: stretch; }
.sd-deploy-grid > .error { grid-column: 1 / -1; }
.sd-deploy-panel { min-width: 0; }
.sd-deploy-wide { grid-column: 1 / -1; }
@media (max-width: 1100px) {
  .sd-deploy-grid { grid-template-columns: 1fr; }
}
@media (max-width: 900px) {
  /* Одна колонка: превью возвращается к чистому 16/10. */
  .site-detail-layout { grid-template-columns: 1fr; }
  .site-detail-preview { min-height: 0; }
}

/* ==========================================================================
   Toggle switch
   ========================================================================== */

/* Мини-шкала прогресса (квота сайта, тариф) — вставляется голыми span'ами. */
.stat-track { flex: 1; height: 4px; border-radius: var(--radius-badge); background: var(--surface); overflow: hidden; }
.stat-fill { height: 100%; width: 0%; background: var(--success); transition: width var(--dur) var(--ease), background var(--dur) var(--ease); }
.stat-fill.warn { background: var(--warning); }
.stat-fill.danger { background: var(--danger); }

.switch { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-caption); color: var(--text-secondary); cursor: pointer; user-select: none; }
/* Стек тумблеров в настройках: вертикальный воздух между строками — раньше
   шли вплотную друг к другу (колонка .settings-block без gap). */
.settings-block > .switch + .switch { margin-top: var(--sp-4); }
.switch input { display: none; }
.switch .track { width: 36px; height: 20px; border-radius: var(--radius-badge); background: var(--surface-elevated); border: 1px solid var(--border); position: relative; transition: background var(--dur) var(--ease); flex-shrink: 0; }
.switch .track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--text-secondary); transition: transform var(--dur) var(--ease), background var(--dur) var(--ease); }
.switch input:checked + .track { background: var(--success-wash); border-color: var(--success); }
.switch input:checked + .track::after { transform: translateX(16px); background: var(--success); }

/* ==========================================================================
   Badges
   ========================================================================== */

/* Одна высота с .badge-icon — иначе текстовый бейдж рядом с иконочным
   встаёт на пару пикселей выше и весь ряд читается как «лесенка». */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: var(--badge-h);
  font-size: var(--fs-micro);
  font-weight: 400;
  padding: 0 10px;
  border-radius: var(--radius-badge);
  background: var(--surface-elevated);
  color: var(--text-secondary);
  text-decoration: none;
}
.badge.wp { background: var(--primary-wash); color: var(--primary); }
.badge.node { background: var(--success-wash); color: var(--success); }
.badge.ssl { background: var(--success-wash); color: var(--success); }
.badge.nossl { background: var(--danger-wash); color: var(--danger); }
/* Предупреждение (не ошибка): часть бэкапа пропущена, конфиг применён с
   оговоркой. Тот же токен, что у .banner-warn — второго «жёлтого» не заводим. */
.badge.warn { background: var(--warning-wash); color: var(--warning); }

.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-tertiary); display: inline-block; flex-shrink: 0; }
.dot.running { background: var(--success); box-shadow: 0 0 0 3px var(--success-wash); }

/* ==========================================================================
   Data grid (sites table etc.)
   ========================================================================== */

.data-grid-wrap { overflow-x: auto; }
.sites-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
.sites-table thead th {
  position: sticky;
  top: 0;
  text-align: left;
  font-size: var(--fs-micro);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-tertiary);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: var(--sp-3) var(--sp-3);
  white-space: nowrap;
}
.sites-table td { padding: 0 var(--sp-3); height: var(--row-h); border-bottom: 1px solid var(--border); vertical-align: middle; color: var(--text-primary); font-weight: 500; /* выбранная система: таблицы — Onest 500 */ }
.sites-table tbody tr { transition: background var(--dur) var(--ease); }
.sites-table tbody tr:hover { background: var(--surface-elevated); box-shadow: inset 2px 0 0 var(--primary); }
.sites-table tbody tr:last-child td { border-bottom: none; }
/* Текстовые ссылки в ячейках (домен, владелец). Иконочные бейджи/кнопки —
   НЕ сюда: общий margin-right и font-size ломали ровную геометрию
   .cell-icons (у «якорного» бейджа gap съезжал 6→18px, ряд выглядел кривым). */
.sites-table a:not(.badge-icon):not(.ov-icon-btn) { color: var(--primary); text-decoration: none; margin-right: var(--sp-3); font-size: var(--fs-caption); cursor: pointer; }
.sites-table a:not(.badge-icon):not(.ov-icon-btn):hover { text-decoration: underline; }
.sites-table th:last-child, .sites-table td:last-child { text-align: right; }

/* ==========================================================================
   Empty state
   ========================================================================== */

/* Единый принцип отображения: контент прижат влево, как карточки в
   «Сайтах» — никаких центрированных пустышек. */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  gap: var(--sp-3);
  padding: var(--sp-9) 0;
  color: var(--text-secondary);
}
.empty-state .icon-xl { color: var(--text-tertiary); margin-bottom: var(--sp-2); }
.empty-state[hidden] { display: none; }
.empty-state h3 { color: var(--text-primary); }
.empty-state p { margin: 0; max-width: 420px; font-size: var(--fs-body); }

/* ==========================================================================
   Feature cards (security tools etc.)
   ========================================================================== */

.feature-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  /* Единый рецепт карточки (как .card): стекло + рамка + --card-pad. */
  background: var(--surface-glass);
  background-image: var(--grad-surface);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--card-pad);
  box-shadow: var(--lift);
  transition: box-shadow var(--dur-slow) var(--ease);
}
/* Тот же ховер, что у карточек файлов/сайтов: свечение + градиентная кромка
   (общий mask-composite шаблон выше). */
.feature-card:hover { box-shadow: var(--lift-hi), 0 0 46px rgba(99, 102, 241, .32); }
.feature-card-head { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.feature-card-head .icon { color: var(--primary); }
/* h3 — резиновый и умеет сжиматься: иначе длинное имя (unattended-upgrades,
   smartmontools) выталкивало бейджи «DARPANEL»/«установлен» за правый край
   карточки и самой страницы (scrollWidth у main рос на ~50px). */
.feature-card-head h3 { margin: 0; flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.feature-card p { margin: 0; color: var(--text-secondary); font-size: var(--fs-caption); flex: 1; }
.feature-card pre {
  margin: 0;
  max-height: 220px;
  overflow: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--sp-3);
  font-size: var(--fs-micro);
  color: var(--text-secondary);
}
.feature-card-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); }

/* ==========================================================================
   Timeline (audit log)
   ========================================================================== */

.timeline { position: relative; padding-left: var(--sp-6); }
.timeline::before { content: ''; position: absolute; left: 5px; top: 4px; bottom: 4px; width: 1px; background: var(--border); }
.timeline-item { position: relative; padding-bottom: var(--sp-5); }
.timeline-item:last-child { padding-bottom: 0; }
.timeline-item::before {
  content: '';
  position: absolute;
  left: calc(var(--sp-6) * -1 + 1px);
  top: 4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--surface-elevated);
  border: 2px solid var(--primary);
}
.timeline-time { font-size: var(--fs-micro); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.timeline-body { font-size: var(--fs-body); color: var(--text-primary); margin-top: 2px; }
.timeline-body b { font-weight: 500; }

/* Тулбар nginx-логов в «Журнале»: два селекта + icon-refresh в одну линию,
   компактная высота control-h-sm. */
.audit-log-toolbar { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.audit-log-toolbar .input { height: var(--control-h-sm); padding: 0 var(--sp-3); font-size: var(--fs-caption); width: auto; }
.audit-log-toolbar #nginxLogSite { flex: 1 1 200px; min-width: 0; }
.audit-log-toolbar #nginxLogType { flex: 0 0 auto; min-width: 110px; }
.audit-log-toolbar .ov-icon-btn { flex-shrink: 0; }

/* ==========================================================================
   Settings sections
   ========================================================================== */

.settings-extra-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: var(--sp-5); }
/* Единый рецепт карточки (как .card): стекло + рамка + --card-pad.
   flex-колонка: футер действий прижат к низу (margin-top:auto) — у всех
   блоков ряда кнопки стоят на одной линии независимо от объёма контента. */
.settings-block {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--radius-card); padding: var(--card-pad);
  background: var(--surface-glass); background-image: var(--grad-surface);
  backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
}
.settings-block h3 { margin: 0 0 var(--sp-2); display: flex; align-items: center; gap: var(--sp-2); }
/* Статус-бейдж («задан»/«не задан») — в шапке блока, прижат вправо. */
/* Широкий блок — во всю ширину сетки настроек (Real IP и подобные). */
.settings-block-full { grid-column: 1 / -1; }
.settings-block h3 .badge { margin-left: auto; flex-shrink: 0; }
.settings-block .icon { color: var(--primary); }
/* Футер действий: внизу блока, без разделительной черты диалогов. */
.settings-block .dialog-actions { margin-top: auto; padding-top: var(--sp-3); border-top: 0; }
.settings-block .dialog-actions + .field-hint { margin-top: var(--sp-1); }
/* Подчинённые контролы при выключенном родителе — disabled + приглушены. */
.settings-block label:has(> input:disabled),
.settings-block label:has(> select:disabled),
.settings-block label:has(> textarea:disabled) { opacity: .5; }
.settings-list { display: flex; flex-direction: column; gap: var(--sp-2); }
.settings-list-row {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: var(--row-h); box-sizing: border-box;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-hover); border-radius: var(--radius-control);
}
.settings-list-row .settings-list-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.settings-list-empty { color: var(--text-tertiary); font-size: var(--fs-caption); }
/* Компактный вариант — всё в один ряд, без вертикального растягивания на
   пустой ширине карточки (было: время / ретеншн / дни / чекбокс / кнопка
   каждое на своей строке с большими отступами при почти пустом контенте). */
/* PHP-настройки на странице сайта — плотная сетка, а не колонка на весь экран */
.sd-php-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-3); margin-top: var(--sp-3);
}
.sd-php-field { display: flex; flex-direction: column; gap: var(--sp-1); }
.sd-php-field span { font-size: var(--fs-micro); color: var(--text-secondary); }
.sd-php-field input {
  height: var(--control-h); padding: 0 var(--sp-3); border-radius: var(--radius-control);
  border: 1px solid var(--border); background: var(--surface-elevated); color: var(--text-primary);
}
.sd-php-field input:focus { outline: none; border-color: var(--primary); }

.sd-php-version { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); font-size: var(--fs-caption); }
.sd-php-version .badge { font-weight: 400; }
/* Селект версии PHP — в языке .field select, но компактный, без обёртки label. */
.sd-php-version-select {
  height: var(--control-h-sm); padding: 0 var(--sp-2); border-radius: var(--radius-control);
  border: 1px solid var(--border); background: var(--surface); color: var(--text-primary);
  font: inherit; font-size: var(--fs-caption); cursor: pointer;
}
.sd-php-version-select:hover { border-color: var(--border-hover); }
.sd-php-version-select:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-wash); }

/* Site-detail settings, tabbed: was one full-width card after another all the
   way down the page — everything below "Действия" now lives in one of four
   tabs (chip-group above) and lays out 2-up instead of 1-up. Cards keep their
   own `hidden` from render logic (site type doesn't need this card at all);
   .tab-hidden is layered on top for "not the active tab" — the two reasons
   for being invisible don't fight each other. */
.site-settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  gap: var(--sp-5);
  /* stretch, не start: карточки в одном ряду были разной высоты (одна —
     абзац текста + селект, другая — одна кнопка), из-за чего нижний край
     ряда шёл лесенкой. Стретч выравнивает низ ряда всегда, вне зависимости
     от объёма контента внутри конкретной карточки. */
  align-items: stretch;
}
.site-settings-grid .card { margin-bottom: 0; }
/* Вкладка «Обслуживание»: своя двухколоночная раскладка внутри общей сетки —
   слева бэкапы/страницы ошибок/nginx, справа режим обслуживания с превью.
   Раньше обе карточки были соседями в auto-fit ряду: правая уезжала вниз
   полотном, слева висела пустота. */
.maint-cols {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  gap: var(--sp-5);
  align-items: start;
}
.maint-col { display: flex; flex-direction: column; gap: var(--sp-5); min-width: 0; }
.maint-col .card { margin-bottom: 0; }
/* Статус автосохранения в шапке карточки («Сохранение…»/«Сохранено»). */
.save-status { align-self: center; }

/* Плашка «новая версия панели»: вкладка, открытая до деплоя, держит старый
   DOM — checkUiVersion показывает её при расхождении ?v с /api/ui-version.
   Всплывашка справа снизу (тот же тост-рецепт, что scan-toast), шапку не
   занимает. Статус сайта — рядом, выше. */
.ui-update-banner {
  position: fixed; right: var(--sp-4); bottom: var(--sp-4); z-index: var(--z-toast);
  display: flex; align-items: center; gap: var(--sp-2);
  background: var(--surface-elevated); color: var(--text-primary);
  border: 1px solid var(--primary); border-radius: var(--radius-control);
  padding: var(--sp-3) var(--sp-4);
  font-family: inherit; font-size: var(--fs-caption); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  box-shadow: var(--shadow-md), 0 0 18px rgba(99, 102, 241, .25);
  transition: box-shadow var(--dur) var(--ease);
}
.ui-update-banner .ui-update-ico {
  width: 28px; height: 28px; flex-shrink: 0;
  display: grid; place-items: center;
  border-radius: var(--radius-sm);
  background: var(--primary-wash); color: var(--primary);
}
.ui-update-banner .ui-update-ico .icon { width: 16px; height: 16px; color: var(--primary); }
.ui-update-banner:hover { box-shadow: var(--shadow-md), 0 0 26px rgba(99, 102, 241, .45); }
/* Статус сайта — над баннером обновления, если оба живы. */
#siteStatusToast { bottom: calc(var(--sp-4) + 56px); }
/* Логи в две колонки: лог-вьювер шире (строки длинные), операции справа. */
.logs-cols { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: var(--sp-5); align-items: start; }
.logs-cols > .card { margin-bottom: 0; min-width: 0; }
@media (max-width: 1100px) { .logs-cols { grid-template-columns: 1fr; } }

/* Вкладка «Безопасность» — две равные колонки (мониторинг/скан/заголовки |
   ограничение/Basic-Auth/custom nginx), компактно в один экран. */
.sec-cols { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); align-items: start; }
.sec-col { display: flex; flex-direction: column; gap: var(--sp-5); min-width: 0; }
.sec-col .card { margin-bottom: 0; }
@media (max-width: 1100px) { .sec-cols { grid-template-columns: 1fr; } }

/* Верх вкладки «Домен» — три колонки (канонический | поддомены+docroot |
   редиректы); DNS-редактор ниже на всю ширину. */
.domain-top-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-5); grid-column: 1 / -1; }
.domain-top-grid > .card { margin-bottom: 0; min-width: 0; }
@media (max-width: 1100px) { .domain-top-grid { grid-template-columns: 1fr; } }
/* Инлайн-форма добавления записи (DNS, редиректы): все элементы в одну
   линию высоты 32 — лейблы в placeholder, инпуты/селекты ростом с чипы. */
.form-inline-sm > label { font-size: 0; gap: 0; }
.form-inline-sm input:not([type="hidden"]), .form-inline-sm select { height: var(--control-h-sm); padding-top: 0; padding-bottom: 0; font-size: var(--fs-caption); }
.form-inline-sm .chip-group { align-items: center; margin-bottom: 0; }

/* DNS-таблица (домены/IP/TTL в 4 колонки) физически не помещается в треть
   строки сетки — значения переносились по 3-4 строки каждое. Во всю ширину
   ряда, независимо от того, сколько соседних карточек рядом. */
#siteDetailDns { grid-column: 1 / -1; }
[data-site-tab].tab-hidden { display: none; }

/* Почта — пошаговый флоу. Каждый шаг с номером, следующий не показывается,
   пока не сделан предыдущий (см. applyMailProgress в app.js). */
.mail-step-head { display: flex; align-items: flex-start; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.mail-step-head h3 { margin: 0 0 4px; }
.mail-step-head .field-hint { margin: 0; }
.mail-step-head > div { flex: 1; min-width: 0; }
.mail-step-num {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--primary-wash); color: var(--primary);
  font-size: var(--fs-caption); font-weight: 500;
}
.mail-step-num.done { background: var(--success-wash); color: var(--success); }
.mail-status { flex-shrink: 0; }
.mail-domains { display: flex; flex-direction: column; gap: var(--sp-2); }
.mail-domain {
  display: flex; align-items: center; gap: var(--sp-3);
  min-height: var(--row-h); box-sizing: border-box;
  padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-control);
  border: 1px solid var(--border); background: var(--surface-elevated);
  cursor: pointer; font-size: var(--fs-body);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.mail-domain:hover { border-color: var(--border-hover); background: var(--surface-hover); }
.mail-domain.active { border-color: var(--primary); background: var(--primary-wash); }
.mail-domain .icon { color: var(--text-tertiary); flex-shrink: 0; }
.mail-domain.active .icon { color: var(--primary); }
.mail-domain span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Домены с почтой — те же карточки, что у сайтов (.site-card), для единого
   визуального языка вместо голых строк-«не пойми чего подключено». */
.mail-domain-grid { margin-top: var(--sp-2); }
.mail-domain-card { cursor: pointer; }
.mail-domain-card.active { box-shadow: var(--edge-highlight), 0 0 28px rgba(99, 102, 241, .35); }
/* Раздел почты — ДВЕ ровные колонки: слева настройка (быстрый сетап,
   сервер, домены), справа рабочая область (выбранный домен, диагностика).
   Скрытые шаги (hidden) в гриде не участвуют — колонки не ломаются.
   margin-bottom карточек гасим — ритм держит grid-gap, иначе двойные
   отступы и неровные ряды. Инпуты — стандартной компактной ширины, не во
   всю карточку. */
#mailInstalledBlock { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); align-items: start; }
#mailInstalledBlock > .card { margin-bottom: 0; }
#mailQuickSetup, #mailStepHostname, #mailStepDomains { grid-column: 1; }
#mailDomainDetail, #mailAdvanced { grid-column: 2; }
@media (max-width: 1280px) {
  #mailInstalledBlock { grid-template-columns: 1fr; }
  #mailQuickSetup, #mailStepHostname, #mailStepDomains, #mailDomainDetail, #mailAdvanced { grid-column: auto; }
}
#view-mail .field-row > label { flex: 0 1 340px; }
/* Ящики, алиасы и домены почты — те же site-card, что у сайтов (единый язык,
   никаких голых строк во всю ширину). Контента одна-две строки, поэтому
   высоту ряда отдаёт контент (auto), а не общий --card-min-h (240px под
   превью сайтов) — иначе карточки-пустышки. Длинные адреса режем многоточием,
   полный адрес — в title. */
.mail-item-grid, .mail-domain-grid { grid-auto-rows: auto; }
.mail-item-card .site-card-domain span,
.mail-item-card .site-card-meta span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mail-advanced summary {
  cursor: pointer; font-size: var(--fs-h3); font-weight: 400; color: var(--text-secondary);
  list-style: none; display: flex; align-items: center; gap: var(--sp-2);
}
.mail-advanced summary::-webkit-details-marker { display: none; }
.mail-advanced summary::before { content: '▸'; transition: transform var(--dur) var(--ease); }
.mail-advanced[open] summary::before { transform: rotate(90deg); }
.mail-advanced summary:hover { color: var(--text-primary); }

/* Почта — установка стека в один клик с живым логом (в .empty-state). */
#mailInstallProgress { width: 100%; max-width: 720px; }
.mail-install-status { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-caption); color: var(--text-secondary); margin-bottom: var(--sp-2); }
#mailInstallLog { max-height: 320px; text-align: left; }

/* Один ровный ряд: у всех групп лейбл наверху (одна типографика —
   fs-caption/500/secondary у ВСЕХ, включая текст-ноды .field-fixed),
   контролы снизу прижаты к общей базовой линии (flex-end). */
.bk-sched-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-4); margin: 0; }
.bk-sched-row .field-fixed {
  display: flex; flex-direction: column; gap: var(--sp-1); flex: 0 0 100px;
  font-size: var(--fs-caption); font-weight: 400; color: var(--text-secondary);
}
.bk-sched-row .field-fixed input {
  height: var(--control-h); padding: 0 var(--sp-3); border-radius: var(--radius-control);
  border: 1px solid var(--border); background: var(--surface-elevated); color: var(--text-primary);
  font-size: var(--fs-body); font-weight: 400;
}
.bk-sched-days-field { display: flex; flex-direction: column; gap: var(--sp-1); margin: 0; }
/* Чип-группа держит высоту контрола ряда, чипы центрируются внутри —
   контролы всех групп стоят на одной базовой линии, лесенки нет.
   margin-bottom:0 — глобальный отступ .chip-group (sp-5) здесь ломал
   базовую линию ряда. */
.bk-sched-days-field .chip-group { height: var(--control-h); align-items: center; margin-bottom: 0; }
/* Тумблер и «Сохранить» — сразу после дней, той же группой (никаких
   margin-left:auto сирот в другом конце строки). Тумблер центрируется
   по высоте контрола. */
.bk-sched-tail { display: flex; align-items: center; gap: var(--sp-3); }
.bk-sched-row .switch { height: var(--control-h); }
.bk-sched-row .btn { flex-shrink: 0; }

/* Карточка бэкапа: мета-строка футером с тонким разделителем (приём
   site-card-links) вместо второго ряда бейджей-пилюль. margin-top:auto
   прижимает футер к низу — карточки ряда держат одну геометрию. */
.bk-card .site-card-domain { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Имя базы — одна строка с ellipsis, длинные имена не ломают шапку карточки */
.db-card .site-card-domain code { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Действия карточки базы — нижний ряд: иконки ВИДИМЫЕ (чип с подложкой и
   нормальным цветом), а не едва различимые призраки. flex-wrap обязателен:
   у непривязанной базы 6 кнопок × 32px + gap'ы = 232px, на узкой колонке
   (минимум грида 232px минус паддинги) ряд без переноса вылезал за правый
   край карточки. margin-top:0 — прижимает к низу мета-строка, не этот ряд. */
.db-card-actions { justify-content: flex-start; flex-wrap: wrap; margin-top: 0; }
.db-card-actions .ov-icon-btn { background: var(--surface-hover); color: var(--text-primary); }
.db-card-actions .ov-icon-btn:hover { background: var(--surface-elevated); }
/* Мета-строка карточки базы (размер, пользователь) — тот же ритм, что
   bk-card-meta, но без своей линии: разделитель один, у ряда действий.
   margin-top:auto прижимает мета+действия к дну карточки. */
.db-card-meta {
  margin-top: auto; display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4);
  font-size: var(--fs-caption); color: var(--text-tertiary);
}
.db-card-meta span { display: inline-flex; align-items: center; gap: 4px; }
.db-card-meta .icon { width: 14px; height: 14px; }
.db-card-meta b { font-weight: 500; color: var(--text-secondary); }
/* Грид баз не растягиваем на всю высоту вьюпорта: общее правило .card-grid
   (minmax(--card-min-h, 1fr)) раздувало единственный ряд из двух карточек до
   300+px с гигантской дырой посередине. minmax(min, auto) держит ряды ровными
   и по контенту. */
#dbGrid { grid-auto-rows: minmax(var(--card-min-h), auto); }
.bk-card-meta {
  margin-top: auto; display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4);
  padding-top: var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--fs-caption); color: var(--text-tertiary);
}
.bk-card-meta span { display: inline-flex; align-items: center; gap: 4px; }
.bk-card-meta .icon { width: 13px; height: 13px; }
.bk-card-meta b { font-weight: 500; color: var(--text-secondary); }

/* Бэкап крутится — иконка refresh тоже. */
@keyframes spin { to { transform: rotate(360deg); } }
.ov-icon-btn.is-busy { pointer-events: none; }
.ov-icon-btn.is-busy .icon { animation: spin .8s linear infinite; }

/* Строка бэкапа на странице сайта: дата слева, иконки-действия справа
   (единый .ov-icon-btn, сгруппированные: скачать / ссылка+восстановить /
   выгрузки). Раньше — текстовая простыня «файлы · поделиться · …». */
.bk-row { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); margin-top: var(--sp-2); min-height: var(--row-h); box-sizing: border-box; }
.bk-row-date { font-size: var(--fs-caption); color: var(--text-secondary); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.bk-row-actions { display: flex; align-items: center; }
.bk-row-actions .bk-group { display: flex; align-items: center; gap: 2px; margin-left: var(--sp-3); }
.bk-row-actions .bk-group:first-child { margin-left: 0; }
/* «Выгружено» — зелёная галка тем же размером, что кнопки: ряд не прыгает. */
.bk-done { display: inline-flex; align-items: center; justify-content: center; width: var(--control-h-sm); height: var(--control-h-sm); color: var(--success); }
.bk-done .icon { width: 16px; height: 16px; }
.bk-done.bk-muted { color: var(--text-tertiary); }

/* ==========================================================================
   Dialogs (contextual, per-row actions only — see JS boundary comment)
   ========================================================================== */

dialog {
  border: none;
  border-radius: var(--radius-card);
  padding: 0;
  max-width: 440px;
  width: 92vw;
  background: var(--surface-elevated);
  color: var(--text-primary);
  box-shadow: var(--shadow-md);
  /* showModal() поднимает диалог в top layer браузера — там выше любого
     z-index; это значение страхует только немодальные show()-диалоги. */
  z-index: 2147483647;
}
dialog.wide { max-width: 880px; }
dialog.wide .dialog-body, dialog.wide form { padding: var(--sp-5); }
dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
dialog form, dialog .dialog-body { padding: var(--sp-6); }

/* Живой лог самоустановки (Флот): шапка + статус + лог во всю высоту */
dialog.install-log { max-width: 860px; }
dialog.install-log form { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-5); }
dialog.install-log .dlg-head { display: flex; align-items: center; gap: var(--sp-2); }
dialog.install-log .dlg-head h3 { margin: 0; }
dialog.install-log #fleetInstallLogStatus { margin: 0; font-size: 13px; }
dialog.install-log #fleetInstallLogText {
  height: 52vh;
  max-height: none;
  resize: vertical;
  font-size: 12px;
  line-height: 1.45;
  white-space: pre;
}
dialog.install-log #fleetInstallLogText:focus { outline: none; }
dialog.install-log .dialog-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); }
dialog h2 { margin: 0 0 var(--sp-5); }
dialog[open] { animation: dialog-in var(--dur) var(--ease); }
@keyframes dialog-in { from { opacity: 0; transform: translateY(4px) scale(.99); } to { opacity: 1; transform: none; } }

/* Текущие сохранённые логин/пароль в диалоге «Доступ в админку» — сжатая
   сводка над формой смены, не отдельная карточка: тот же серый фон, что и
   у прочих служебных врезок в диалогах. */
.wp-creds-current { background: var(--surface-hover); border-radius: var(--radius-control); padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-5); }
.wp-creds-current-row { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-caption); padding: 3px 0; }
.wp-creds-current-row .muted { flex-shrink: 0; min-width: 110px; }
.wp-creds-current-row code { font-size: var(--fs-caption); color: var(--text-primary); }
.wp-creds-current-row .ov-icon-btn { margin-left: auto; }
.wp-creds-current-note { font-size: var(--fs-micro); margin: var(--sp-2) 0 0; }
dialog label { display: block; font-size: var(--fs-caption); color: var(--text-secondary); margin-bottom: var(--sp-4); font-weight: 400; }
dialog label.checkbox { display: flex; align-items: center; gap: var(--sp-2); color: var(--text-primary); }
dialog label.checkbox input { width: auto; }
dialog label.btn { margin-bottom: 0; }
dialog input, dialog select, dialog textarea {
  width: 100%;
  margin-top: var(--sp-2);
  height: var(--control-h);
  padding: 0 var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  font-size: var(--fs-body);
  background: var(--surface);
  color: var(--text-primary);
  font-family: inherit;
  transition: border-color var(--dur) var(--ease);
}
dialog textarea { height: auto; padding-top: var(--sp-3); padding-bottom: var(--sp-3); }
dialog input:focus, dialog select:focus, dialog textarea:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-wash);
}
dialog input::placeholder { color: var(--text-tertiary); }

.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-3); margin-top: var(--sp-5); padding-top: var(--sp-5); border-top: 1px solid var(--border); }
.dialog-actions.justify-start { justify-content: flex-start; }

/* ==========================================================================
   Misc: credentials block, file manager, DNS table, PHP-limits grid
   ========================================================================== */

/* Вывод команды реестра (диалог палитры Ctrl+K): моноширинный JSON, скролл
   внутри — длинный ответ не должен растягивать диалог. */
.cmd-result {
  margin: var(--sp-3) 0 0;
  max-height: 320px; overflow: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--sp-3);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-caption); line-height: 1.5; white-space: pre-wrap; word-break: break-word;
  color: var(--text-secondary);
}
.cmd-result.error { color: var(--danger, #ef4444); border-color: var(--danger, #ef4444); }

.credentials { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-caption); line-height: 1.7; }
.credentials b { display: inline-block; width: 110px; color: var(--text-tertiary); font-weight: 400; }
.credentials + .cred-title { margin-top: var(--sp-3); }
/* Заголовок группы доступов — тот же мелкий uppercase, что у секций панели. */
.cred-title {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-micro); font-weight: 400; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-tertiary);
}
.credentials code { font-size: var(--fs-caption); }
/* Готовый к вставке конфиг (MCP): многострочный блок, который копируют целиком
   — переносы значимы, поэтому white-space: pre и свой горизонтальный скролл. */
.credentials code.ai-snippet {
  display: block; white-space: pre; overflow-x: auto;
  margin: var(--sp-2) 0 0; padding: var(--sp-2) var(--sp-3);
  background: var(--surface-hover); border-radius: var(--radius-sm); color: var(--text-secondary);
}
.cred-foot { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); }
/* Расхождение NS зоны с ожидаемыми — строка над таблицей DNS с кнопкой. */
.dns-ns-warn {
  display: flex; align-items: center; gap: var(--sp-3);
  margin-bottom: var(--sp-3); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--warning, var(--border)); border-radius: var(--radius-sm);
  background: var(--surface); font-size: var(--fs-caption); color: var(--text-secondary);
}
.dns-ns-warn .btn { margin-left: auto; flex-shrink: 0; }

/* Файловый менеджер — раздел панели (#view-fm).
   Трёхколоночный shell: закладки+действия | список файлов | редактор.
   Высота считается от вьюпорта, чтобы скроллились колонки, а не страница. */
.fm-shell {
  display: flex; flex-direction: column; gap: var(--sp-2);
  /* Высота = viewport − топбар (64) − паддинги main (sp-4 сверху, page-gutter
     снизу) − резерв под док (64): скроллятся колонки, а не страница. */
  height: calc(100vh - 64px - var(--sp-4) - var(--page-gutter) - 64px);
  min-height: 420px;
}
.fm-domain-chip {
  display: inline-flex; align-items: center; gap: 6px; margin-left: var(--sp-2); padding: 2px 10px;
  border-radius: var(--radius-badge); background: var(--primary-wash); color: var(--primary);
  font-size: var(--fs-caption); font-weight: 400; vertical-align: middle;
}
.fm-domain-chip .icon { width: 14px; height: 14px; flex-shrink: 0; }

/* Тулбар ФМ — внутри рабочей области и прижат ВЛЕВО, над списком файлов.
   Раньше он уезжал в глобальный топбар справа (moveViewHeader) и читался как
   настройки панели, а не как действия над текущей папкой. Группы разделены
   тонкими разделителями: навигация | создание | буфер | поиск. */
.fm-bar {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  position: relative;
}
.fm-bar-group { display: inline-flex; align-items: center; gap: 2px; }
.fm-bar-sep { width: 1px; height: 18px; background: var(--border); flex-shrink: 0; }
.fm-bar > .ov-icon-btn, .fm-bar > .fm-search { flex-shrink: 0; }
.fm-bar .btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
/* Действия над выбранным — в конце ряда, чтобы не прыгали кнопки навигации. */
.fm-bar-bulk { display: inline-flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.fm-bar-bulk[hidden] { display: none; }
/* Недоступная кнопка тулбара (нечего копировать) — приглушена, но на месте:
   исчезающие иконки заставляют каждый раз заново искать нужную. */
.fm-bar .ov-icon-btn[disabled] { opacity: .35; pointer-events: none; }
/* Крошки + закрепления одной строкой под тулбаром. */
.fm-crumbs-row { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.fm-crumbs-row .fm-path { flex: 1; min-width: 0; margin: 0; }
/* Панель «Найти и заменить» — выпадает карточкой из кнопки-иконки в тулбаре
   ФМ (не отдельная строка над списком): два поля со стрелкой между ними и
   явное превью совпадений перед необратимой правкой десятков файлов разом. */
.fm-replace-bar {
  position: absolute; top: calc(100% + var(--sp-2)); left: 0; z-index: 20;
  display: flex; flex-direction: column; gap: var(--sp-3);
  width: 480px; max-width: calc(100vw - var(--page-gutter) * 2);
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-card);
  background: var(--surface); background-image: var(--grad-surface);
  box-shadow: var(--shadow-popover, 0 8px 24px rgba(0, 0, 0, 0.35));
}
.fm-replace-bar .icon { color: var(--text-tertiary); flex-shrink: 0; }
.fm-replace-bar-fields { display: flex; align-items: center; gap: var(--sp-2); }
.fm-replace-bar input {
  height: var(--control-h-sm); flex: 1 1 0; min-width: 0;
  padding: 0 var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-control);
  background: var(--surface); color: var(--text-primary); font-size: var(--fs-caption);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.fm-replace-bar input::placeholder { color: var(--text-tertiary); }
.fm-replace-bar input:hover { border-color: var(--border-hover); }
.fm-replace-bar input:focus {
  outline: none; border-color: var(--primary); background: var(--surface-elevated);
  box-shadow: 0 0 0 2px var(--primary-wash);
}
.fm-replace-bar-filter { width: 100%; }
.fm-replace-bar-actions { display: flex; align-items: center; gap: var(--sp-2); }
.fm-replace-bar .field-hint { flex: 1 1 auto; margin: 0; min-width: 0; }
/* Закреплённые папки — чипами справа от крошек. Пустой список скрыт целиком:
   панель-заглушка с подсказкой «открой папку и нажми иконку» висела мусором
   на каждом экране. */
.fm-bm-list { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; max-width: 60%; overflow-x: auto; }
.fm-bm-list[hidden] { display: none; }
.fm-bm {
  display: flex; align-items: center; gap: 6px; flex-shrink: 0;
  padding: var(--sp-1) var(--sp-2); border-radius: var(--radius-sm);
  background: var(--surface-hover);
  font-size: var(--fs-micro); color: var(--text-secondary); cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.fm-bm:hover { background: var(--surface-hover); color: var(--text-primary); }
.fm-bm .icon { color: var(--primary); flex-shrink: 0; width: 14px; height: 14px; }
.fm-bm span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.fm-bm-del {
  background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0;
  color: var(--text-tertiary); opacity: 0; transition: opacity var(--dur) var(--ease), color var(--dur) var(--ease);
}
.fm-bm-del .icon { width: 12px; height: 12px; color: inherit; }
.fm-bm:hover .fm-bm-del { opacity: 1; }
.fm-bm-del:hover { color: var(--danger); }

.fm-layout {
  flex: 1; display: flex; gap: 0; min-width: 0;
  border: 1px solid var(--border); border-radius: var(--radius-card);
  overflow: hidden; position: relative;
  background: var(--surface); background-image: var(--grad-surface);
  box-shadow: var(--edge-highlight);
}
/* Список занимает всю ширину, пока не открыт файл: раньше он был зажат в 280px
   ради вечно пустого редактора справа. Открыли файл — колонка сжимается до
   рабочей ширины, редактор забирает остальное. */
.fm-list-col { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.fm-layout.with-editor .fm-list-col { flex: 0 0 clamp(280px, 32%, 460px); border-right: 1px solid var(--border); }
/* Скроллится контейнер вместе с шапкой колонок (она sticky) — иначе паддинг
   списка и полоса прокрутки сдвигали заголовки относительно строк. */
.fm-list-scroll { flex: 1; overflow-y: auto; min-height: 0; padding: 0 var(--sp-2) var(--sp-2); scrollbar-gutter: stable; }
.fm-list { padding: 0; }

/* Строки — сетка колонок (имя / размер / дата / права), заголовок и строки
   делят одну grid-разметку, поэтому колонки не разъезжаются. В узкой колонке
   (открыт редактор) размер/дата/права скрываются — остаётся имя. */
.fm-head, .fm-list-item {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) 84px 132px 52px 28px;
  align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-3); box-sizing: border-box;
}
.fm-head {
  position: sticky; top: 0; z-index: 1;
  min-height: 32px; border-bottom: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--fs-micro); color: var(--text-tertiary);
  letter-spacing: .04em; text-transform: uppercase;
}
.fm-head-check { display: flex; align-items: center; }
.fm-head-check input { margin: 0; width: 14px; height: 14px; accent-color: var(--primary); cursor: pointer; }
.fm-head-cell {
  display: flex; align-items: center; gap: 4px; padding: 0; min-width: 0;
  background: none; border: 0; font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; cursor: pointer; text-align: left;
}
.fm-head-cell .icon { width: 11px; height: 11px; opacity: 0; transition: opacity var(--dur) var(--ease); }
.fm-head-cell:hover { color: var(--text-secondary); }
.fm-head-cell:hover .icon { opacity: .5; }
.fm-head-cell.sorted { color: var(--primary); }
.fm-head-cell.sorted .icon { opacity: 1; }
.fm-head-cell.desc .icon { transform: scaleY(-1); }
span.fm-head-cell { cursor: default; }
.fm-col-size, .fm-col-date { justify-content: flex-end; }
.fm-col-mode { justify-content: flex-end; font-variant-numeric: tabular-nums; }

.fm-list-item {
  min-height: var(--row-h); border-radius: var(--radius-sm);
  cursor: pointer; font-size: var(--fs-caption);
}
.fm-list-item .icon { color: var(--text-tertiary); flex-shrink: 0; }
.fm-list-item .fm-check { margin: 0; width: 14px; height: 14px; accent-color: var(--primary); cursor: pointer; }
/* Имя = иконка + текст в одной ячейке сетки. */
.fm-name { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.fm-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fm-cell { font-size: var(--fs-micro); color: var(--text-tertiary); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Папки — янтарные, файлы — нейтральные: явное визуальное отличие */
.fm-list-item[data-isdir="true"] .icon { color: var(--warning); }
/* Только для чтения (снят бит записи) — видно списком, а не только в меню. */
.fm-list-item.readonly .fm-cell-mode { color: var(--warning); }
.fm-item-more {
  background: none; border: none; cursor: pointer; justify-self: end;
  padding: 2px; color: var(--text-tertiary); opacity: 0; transition: opacity var(--dur) var(--ease), color var(--dur) var(--ease);
  border-radius: var(--radius-sm);
}
.fm-item-more .icon { width: 14px; height: 14px; }
.fm-list-item:hover .fm-item-more, .fm-list-item.active .fm-item-more { opacity: 1; }
.fm-item-more:hover { color: var(--primary); background: var(--surface-hover); }
.fm-list-item:hover { background: var(--surface-hover); }
.fm-list-item.active { background: var(--primary-wash); color: var(--primary); }
.fm-list-item.active .icon { color: var(--primary); }
/* Выбранное чекбоксом — нейтральный wash, отличим от .active (открытый файл) */
.fm-list-item.selected { background: var(--surface-hover); box-shadow: inset 2px 0 0 var(--primary); }
/* Вырезанное в буфер — приглушено, как в проводнике. */
.fm-list-item.cut { opacity: .5; }
/* Узкая колонка рядом с редактором: служебные колонки убираем, имя остаётся. */
.fm-layout.with-editor :is(.fm-col-size, .fm-col-date, .fm-col-mode, .fm-cell) { display: none; }
.fm-layout.with-editor :is(.fm-head, .fm-list-item) { grid-template-columns: 20px minmax(0, 1fr) 28px; }
/* Пустая папка и результаты поиска — служебные строки без сетки колонок. */
.fm-list-item.fm-plain { display: flex; cursor: default; color: var(--text-tertiary); }
.fm-status {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--fs-micro); color: var(--text-tertiary); min-height: 32px; box-sizing: border-box;
}
.fm-bulk-count { font-size: var(--fs-micro); color: var(--text-tertiary); }
.fm-dirty-dot { color: var(--warning); margin-left: 6px; }
.fm-editor { flex: 1; display: flex; flex-direction: column; padding: var(--sp-3); min-width: 0; }
.fm-editor[hidden] { display: none; }
/* Шапка редактора: имя файла слева, действия справа — одной строкой вместо
   ряда кнопок внизу пустой области. */
.fm-editor-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding-bottom: var(--sp-2); margin-bottom: var(--sp-2); border-bottom: 1px solid var(--border);
}
.fm-editor-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-caption); color: var(--text-secondary);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.fm-editor-actions { display: inline-flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
/* Хлебные крошки — строго одна строка на одной базовой линии: nowrap + clip,
   длинный путь обрезается, а не пляшет переносами и разными высотами. */
.fm-path {
  display: flex; align-items: center; flex-wrap: nowrap; overflow: hidden; white-space: nowrap;
  min-height: 20px; font-size: var(--fs-caption); color: var(--text-tertiary);
}
.fm-path .fm-crumb { min-width: 0; }
.fm-path .fm-crumb span { overflow: hidden; text-overflow: ellipsis; }
.fm-crumb-sep { flex-shrink: 0; }
.fm-dirty-dot { flex-shrink: 0; }

/* Хлебные крошки пути в файловом менеджере */
.fm-crumb { color: var(--text-tertiary); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
a.fm-crumb:hover { color: var(--primary); }
.fm-crumb-root .icon { width: 12px; height: 12px; }
.fm-crumb-current { color: var(--text-secondary); }
.fm-crumb-sep { margin: 0 2px; color: var(--text-tertiary); opacity: .6; }

.fm-textarea {
  flex: 1; width: 100%; background: var(--bg); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-size: var(--fs-caption); padding: var(--sp-3); resize: none;
}

/* Редактор с подсветкой: textarea с прозрачным текстом поверх подсвеченного <pre> */
.fm-editor-wrap { position: relative; flex: 1; min-height: 0; }
.fm-highlight, .fm-editor-wrap .fm-textarea {
  position: absolute; inset: 0; margin: 0;
  font-family: var(--font-mono); font-size: var(--fs-caption); line-height: 1.5; tab-size: 4;
  padding: var(--sp-3); white-space: pre; word-wrap: normal; overflow: auto;
}
.fm-highlight {
  background: var(--bg); color: var(--text-primary);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  overflow: hidden; pointer-events: none; display: none;
}
.fm-editor-wrap.hl-on .fm-highlight { display: block; }
.fm-editor-wrap.hl-on .fm-textarea { background: transparent; border-color: transparent; color: transparent; caret-color: var(--text-primary); }

/* Цвета токенов подсветки */
.fm-editor .tok-comment { color: var(--tok-comment); font-style: italic; }
.fm-editor .tok-string { color: var(--tok-string); }
.fm-editor .tok-number { color: var(--warning); }
.fm-editor .tok-keyword { color: var(--primary-hover); }

/* Индикатор загрузки файлов — всплывающий тост снизу справа */
.fm-upload {
  position: fixed; right: var(--sp-4); bottom: var(--sp-4); z-index: var(--z-toast);
  width: 320px; max-width: calc(100vw - 32px);
  background: var(--surface-elevated); border: 1px solid var(--border-hover);
  border-radius: var(--radius-control); padding: var(--sp-3) var(--sp-4);
  box-shadow: var(--shadow-lg);
  animation: dialog-in var(--dur) var(--ease);
}
.fm-upload-info {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-micro); color: var(--text-tertiary); margin-bottom: 4px;
}
.fm-upload-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fm-upload-pct { font-variant-numeric: tabular-nums; flex-shrink: 0; }
/* Крестик — тост можно закрыть досрочно, не дожидаясь автo-hide */
.fm-upload-close {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 18px; height: 18px; margin: -2px -4px -2px 0; padding: 0;
  background: none; border: none; border-radius: var(--radius-sm);
  color: var(--text-tertiary); cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.fm-upload-close:hover { color: var(--text-primary); background: var(--surface-hover); }
.fm-upload-close .icon { width: 12px; height: 12px; }
.fm-upload-bar {
  height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--border);
}
.fm-upload-fill {
  height: 100%; width: 0%;
  background: var(--primary); border-radius: 3px;
  transition: width 120ms linear, background var(--dur) var(--ease);
}
.fm-upload.success .fm-upload-fill { background: var(--success); }
.fm-upload.error .fm-upload-fill { background: var(--danger); }
.fm-upload.success .fm-upload-label { color: var(--success); }
.fm-upload.error .fm-upload-label { color: var(--danger); }
@keyframes upload-pop {
  0% { transform: scale(.6); opacity: 0; }
  60% { transform: scale(1.15); }
  100% { transform: scale(1); opacity: 1; }
}
.fm-upload.success .fm-upload-pct,
.fm-upload.error .fm-upload-pct { animation: upload-pop 300ms var(--ease); font-weight: 500; }
.fm-upload.success .fm-upload-pct { color: var(--success); }
.fm-upload.error .fm-upload-pct { color: var(--danger); }

/* Кнопка «в закладки» — активное состояние залито: видно, что папка уже в закладках */
#fmBookmarkAdd.active .icon { fill: currentColor; }

/* Тонкая полоса прогресса загрузки в шапке файлового менеджера (2px сверху
   рабочей области): дублирует тост и остаётся видна, даже если тост закрыли. */
.fm-shell { position: relative; }
.fm-progress {
  position: absolute; top: -2px; left: 0; right: 0; height: 2px; z-index: 25;
  border-radius: 2px; overflow: hidden;
}
.fm-progress-fill {
  height: 100%; width: 0%;
  background: var(--primary);
  transition: width 120ms linear, background var(--dur) var(--ease);
}
.fm-progress.success .fm-progress-fill { background: var(--success); }
.fm-progress.error .fm-progress-fill { background: var(--danger); }

/* Оверлей drag&drop загрузки поверх списка файлов и редактора */
.fm-drop-overlay {
  position: absolute; inset: 0; z-index: 20;
  display: flex; align-items: center; justify-content: center; text-align: center;
  padding: var(--sp-4);
  background: var(--backdrop);
  border: 2px dashed var(--primary); border-radius: var(--radius-control);
  color: var(--primary); font-size: var(--fs-body);
  pointer-events: none;
}
.fm-drop-overlay[hidden] { display: none; }
.fm-drop-overlay b { color: var(--primary-hover); font-weight: 400; }

/* Поиск по файлам: поле в тулбаре + результаты вместо списка */
.fm-search {
  display: inline-flex; align-items: center; gap: 6px; margin-left: auto;
  height: var(--control-h-sm); box-sizing: border-box; padding: 0 var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.fm-search:hover { border-color: var(--border-hover); }
.fm-search .icon { color: var(--text-tertiary); flex-shrink: 0; }
.fm-search input { border: none; background: none; padding: 0; width: 200px; font-size: var(--fs-caption); color: var(--text-primary); }
.fm-search input::placeholder { color: var(--text-tertiary); }
.fm-search input:focus { outline: none; }
.fm-search:focus-within {
  border-color: var(--primary); background: var(--surface-elevated);
  box-shadow: 0 0 0 3px var(--primary-wash);
}
/* Результаты поиска — не строки папки: своя раскладка (имя + строка с
   совпадением), поэтому выходим из grid-сетки колонок обратно во flex. */
.fm-search-item { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; padding: var(--sp-2) var(--sp-3); }
.fm-search-item .fm-search-name { flex-shrink: 0; max-width: 100%; }
.fm-search-line { flex-basis: 100%; padding-left: 26px; font-size: var(--fs-micro); color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fm-search-item mark { background: var(--primary-wash); color: var(--primary); border-radius: 2px; padding: 0 1px; }

/* Превью изображения в области редактора */
.fm-preview { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); overflow: auto; padding: var(--sp-3); }
.fm-preview[hidden] { display: none; }
.fm-preview img { max-width: 100%; max-height: calc(100% - 40px); object-fit: contain; }
.fm-preview-meta { font-size: var(--fs-micro); color: var(--text-tertiary); }

/* Tail-режим логов */
.fm-tail-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.fm-tail-wrap[hidden] { display: none; }
.fm-tail-view { flex: 1; min-height: 0; margin: 0; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); padding: var(--sp-3); font-family: var(--font-mono, monospace); font-size: var(--fs-caption); line-height: 1.5; white-space: pre-wrap; word-break: break-all; color: var(--text-secondary); }
.fm-tail-bar { display: flex; align-items: center; gap: var(--sp-2); }
.fm-tail-bar .btn.active { border-color: var(--primary); color: var(--primary); }
.fm-tail-info { font-size: var(--fs-micro); color: var(--text-tertiary); margin-left: auto; }

/* Drag&drop загрузки на карточку сайта (страница «Файлы») */
.site-card.drag-over { border-color: var(--primary); background: var(--primary-wash); }
.upload-toast-pct { font-variant-numeric: tabular-nums; color: var(--text-secondary); }

.scan-toast {
  position: fixed; right: var(--sp-4); bottom: var(--sp-4); z-index: var(--z-toast);
  display: flex; align-items: center; gap: var(--sp-2);
  background: var(--surface-elevated); border: 1px solid var(--border-hover);
  border-radius: var(--radius-control); padding: var(--sp-3) var(--sp-4);
  color: var(--text-primary); font-size: var(--fs-caption);
  box-shadow: var(--shadow-md); max-width: 360px;
}
.scan-toast.done { border-color: var(--primary); }
.scan-toast-close { background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 14px; padding: 0 0 0 var(--sp-2); }
.scan-toast-close:hover { color: var(--text-primary); }

/* Единые статусы операций — тосты справа снизу (см. notify() в app.js):
   зелёная кромка = успех, красная = ошибка, гаснут сами. Подняты над
   доком/scan-toast, клики пропускают (pointer-events: none). */
#appToasts {
  position: fixed; right: var(--sp-4); bottom: calc(var(--sp-4) * 4); z-index: var(--z-toast);
  display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-end;
  pointer-events: none;
}
.app-toast {
  max-width: 320px; padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-control); border: 1px solid var(--border); border-left: 2px solid var(--success);
  background: rgba(20, 25, 36, .92); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: var(--text-secondary); font-size: var(--fs-caption);
  opacity: .95; transition: opacity .4s var(--ease), transform .4s var(--ease);
}
.app-toast.err { border-left-color: var(--danger); color: var(--text-primary); }
.app-toast.out { opacity: 0; transform: translateY(4px); }

.fm-context-menu {
  position: fixed; z-index: var(--z-toast); min-width: 180px;
  background: var(--surface-elevated); border: 1px solid var(--border-hover);
  border-radius: var(--radius-control); padding: var(--sp-1);
  box-shadow: var(--shadow-md);
}
.fm-context-menu button {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  background: none; border: none; color: var(--text-primary);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm);
  font-size: var(--fs-caption); cursor: pointer; text-align: left;
}
.fm-context-menu button:hover { background: var(--primary-wash); color: var(--primary); }
.fm-context-menu button.danger:hover { background: var(--danger-wash); color: var(--danger); }
.fm-context-menu button .icon { color: var(--text-tertiary); flex-shrink: 0; }
.fm-context-menu button:hover .icon { color: var(--primary); }
.fm-context-menu button.danger:hover .icon { color: var(--danger); }
.fm-context-menu .fm-context-sep { height: 1px; background: var(--border); margin: var(--sp-1) var(--sp-2); }

/* Файловый менеджер — мобильная адаптация: shell разворачивается в колонку,
   боковая панель закладок уезжает наверх и не отъедает ширину. */
@media (max-width: 900px) {
  .fm-shell { height: auto; }
  .fm-layout { flex-direction: column; min-height: 70vh; }
  .fm-list-col { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
  .fm-list { width: 100%; max-height: 32vh; }
  .fm-editor { min-height: 40vh; }
  .fm-search { margin-left: 0; flex: 1 1 100%; }
  .fm-search input { width: 100%; }
}

.dns-table { width: 100%; border-collapse: collapse; font-size: var(--fs-caption); margin-top: var(--sp-3); }
.dns-table th, .dns-table td { text-align: left; padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); font-weight: 500; }
.dns-table td { height: var(--row-h); padding-top: 0; padding-bottom: 0; }
/* Колонке «Значение» — щедрая ширина: при инлайн-редактировании поле
   растягивается на всю ячейку, а не на ширину текста. */
.dns-table th:nth-child(3), .dns-table td:nth-child(3) { width: 42%; }
/* Кликабельное значение: курсор + пунктир намекают «нажми, чтобы изменить». */
.dns-value-edit { cursor: pointer; border-bottom: 1px dashed var(--border-hover); }
.dns-value-edit:hover { color: var(--primary); border-bottom-color: var(--primary); }
/* Чипы типа записи внутри .field-row: глобальный margin-bottom .chip-group
   ломал выравнивание строки формы. */
#dnsAddForm .chip-group { margin-bottom: 0; align-items: center; }
/* То же у чипов «Что бэкапить» в карточке бэкапов. */
#backupKindChips { margin-bottom: 0; }

.limits-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-1) var(--sp-5); }

.settings-grid-2 { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--sp-6); }
@media (max-width: 720px) { .settings-grid-2 { grid-template-columns: 1fr; } }
/* Bare <label>Text<input/></label> here had NO block/spacing rule at all —
   dialog label (below) only applies inside <dialog>, and this form isn't one —
   so the label text and input rendered inline, glued together. Same
   self-contained treatment as .field-row (works standalone, not just inside
   a dialog). This was a real layout bug, not a "could be nicer" nitpick. */
.settings-grid-2 > label {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  font-size: var(--fs-caption);
  font-weight: 400;
  color: var(--text-secondary);
  margin-bottom: var(--sp-4);
}
.settings-grid-2 input, .settings-grid-2 select {
  height: var(--control-h);
  padding: 0 var(--sp-4);
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-primary);
  font-size: var(--fs-body);
  font-family: inherit;
  width: 100%;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.settings-grid-2 input:focus, .settings-grid-2 select:focus {
  outline: none;
  border-color: var(--primary);
  background: var(--surface-elevated);
  box-shadow: 0 0 0 3px var(--primary-wash);
}
.settings-grid-2 input::placeholder { color: var(--text-tertiary); }

/* ==========================================================================
   Redesign v2 — тёмный дашборд (indigo): сайдбар, топбар со спарклайнами,
   страница «Обзор» с правой колонкой. Всё ниже использует те же токены.
   ========================================================================== */

/* --- Sidebar bottom: карточка сервера + служебные ссылки --- */
.sidebar-footer { margin-top: auto; display: flex; flex-direction: column; gap: var(--sp-3); padding-top: var(--sp-4); }
/* Отделяем блок быстрых действий от основного меню тонкой линией: это
   другая секция (настраиваемые ярлыки), а не продолжение навигации. */
.sidebar-quick { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--border); }
#quickList { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Заголовок секции — явная подпись, а не «серый пункт меню»: мельче любого
   пункта (10px), широкий трекинг, без строкового роста nav-item. */
.sidebar-quick-head {
  display: flex; align-items: center;
  font-size: 10px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-tertiary); padding: 0 var(--sp-2);
}
/* Никакого отдельного квадрата вокруг иконки — она стоит у правого края
   строки, как плюсики в самих кнопках ниже, без центрирующего бокса. */
.quick-cfg-btn { margin-left: auto; border: 0; background: transparent; padding: 0; cursor: pointer; display: flex; align-items: center; color: var(--primary); }
.quick-cfg-btn:hover { color: var(--text-primary); }

/* Попап настройки состава «Быстрых действий»/дока — общий для обоих,
   позиция зависит от того, что настраивается (см. dockCfg.dataset.target). */
.dock-cfg {
  position: fixed; z-index: var(--z-pop); width: 250px;
  background: rgba(20, 25, 36, .94); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
  border-radius: var(--radius-card);
  box-shadow: 0 20px 50px rgba(0, 0, 0, .65), inset 0 1px 0 rgba(255, 255, 255, .08);
  padding: var(--sp-3); display: flex; flex-direction: column; gap: 2px;
}
.dock-cfg[data-target="dock"] { bottom: 70px; left: 50%; transform: translateX(-50%); }
.dock-cfg[data-target="quick"] { bottom: 200px; left: var(--sp-3); }
/* Поповер сайтов из дока — та же пилюля-стекло, список со скроллом. */
.dock-sites { bottom: 70px; left: 50%; transform: translateX(-50%); width: 280px; max-height: 340px; overflow-y: auto; gap: var(--sp-2); }
.dock-cfg h4 { margin: 0; font-size: var(--fs-micro); letter-spacing: .06em; text-transform: uppercase; color: var(--text-tertiary); padding: 0 var(--sp-2) var(--sp-2); }
.dock-opt {
  display: flex; align-items: center; gap: var(--sp-3); height: var(--control-h-sm); padding: 0 var(--sp-2);
  border: 0; border-radius: var(--radius-control); background: transparent; color: var(--text-primary);
  font: inherit; font-size: var(--fs-caption); cursor: pointer; text-align: left; width: 100%;
}
.dock-opt:hover { background: var(--surface-hover); }
.dock-opt .sp { flex: 1; }
.dock-opt .mark { color: var(--primary); opacity: 0; display: flex; }
.dock-opt.on .mark { opacity: 1; }
.server-card {
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  box-shadow: var(--edge-highlight);
}
.server-card-top { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-caption); }
.server-card-top .icon-btn-inline { margin-left: auto; }
/* Ресурс: иконка слева на две строки, сверху цифра+«из N»+процент, снизу шкала
   — та же сетка, что в утверждённом макете. */
.res {
  display: grid;
  grid-template-columns: 26px auto 1fr auto;
  grid-template-areas: "i v u p" "i m m m";
  gap: 2px var(--sp-2);
  align-items: center;
}
.res-ico {
  grid-area: i; width: 26px; height: 26px; border-radius: var(--radius-sm);
  display: grid; place-items: center; background: var(--surface-hover); color: var(--text-secondary);
}
.res-ico .icon { width: 14px; height: 14px; }
.res b { grid-area: v; font-size: 14px; font-weight: 500; line-height: 1.1; color: var(--text-primary); }
.res-u { grid-area: u; font-size: 10px; color: var(--text-tertiary); white-space: nowrap; }
.res-p { grid-area: p; font-size: 10px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.res .meter { grid-area: m; margin-top: 1px; }
.meter { height: 3px; border-radius: 99px; background: var(--surface-hover); overflow: hidden; }
.meter > i { display: block; height: 100%; background: var(--primary); }
.meter > i.warn { background: var(--warning); }
.meter > i.ok { background: var(--success); }
.server-card-meta { font-size: var(--fs-micro); color: var(--text-secondary); word-break: break-all; display: flex; align-items: center; gap: 4px; }
.server-card #terminalBtn {
  width: 100%; margin-top: var(--sp-3); justify-content: center;
  background: var(--surface-hover); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3); color: var(--text-primary); font-size: var(--fs-caption);
}
.server-card #terminalBtn:hover { border-color: var(--border-hover); background: var(--primary-wash); color: var(--primary); }
.icon-btn-inline {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; padding: 0; border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-tertiary); cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.icon-btn-inline .icon { width: 12px; height: 12px; }
.icon-btn-inline:hover { color: var(--text-primary); background: var(--surface-hover); }
/* Мини-кнопка копирования у значений (IP, пароли, ящики). */
.copy-mini { vertical-align: -3px; margin-left: 4px; }
.copy-mini.copied { color: var(--success); }
/* Значение, копируемое кликом по себе (токен, логин, пароль, MCP-конфиг) —
   пунктир снизу и курсор подсказывают кликабельность; галочка — подсветкой. */
.copyable { cursor: copy; border-bottom: 1px dashed var(--border); transition: color .15s var(--ease), background .15s var(--ease); word-break: break-all; }
.copyable:hover { color: var(--text-primary); background: var(--surface-hover); }
.copyable.copied { color: var(--success); border-bottom-color: var(--success); background: transparent; }
/* Лейбл поля с кнопкой копии справа (Настройки). */
.label-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.label-row .copy-mini { margin-left: 0; }
.server-card .btn { margin-top: var(--sp-2); white-space: nowrap; padding: 0 var(--sp-2); font-size: 11px; gap: 6px; }

/* Collapsed sidebar — только иконки */
.app-shell.collapsed .sidebar { width: 68px; padding: var(--sp-5) var(--sp-2); }
.app-shell.collapsed .nav-item span,
.app-shell.collapsed .sidebar-footer { display: none; }
.app-shell.collapsed .nav-item { justify-content: center; padding: 0; }
.app-shell.collapsed .sidebar-brand { justify-content: center; padding: 0 0 var(--sp-5); }

.topbar-user { display: flex; align-items: center; gap: var(--sp-3); }
.topbar-user-meta { display: flex; flex-direction: column; line-height: 1.25; text-align: right; }
.topbar-user-name { font-size: var(--fs-caption); font-weight: 400; color: var(--text-primary); }
.topbar-user-role { font-size: 10px; color: var(--text-tertiary); }
.avatar {
  width: 34px; height: 34px; border-radius: var(--radius-control);
  background: var(--primary-wash); color: var(--primary);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-caption); font-weight: 500; flex-shrink: 0;
}
.bell-btn { position: relative; }
.bell-badge {
  position: absolute; top: -5px; right: -5px;
  min-width: 15px; height: 15px; padding: 0 4px;
  border-radius: var(--radius-badge);
  background: var(--danger); color: #fff;
  font-size: 10px; font-weight: 500; line-height: 15px; text-align: center;
}
.bell-badge[hidden] { display: none; }
.notif-panel {
  position: absolute; top: 44px; right: 0; z-index: var(--z-pop);
  width: 320px; max-height: 400px; overflow: auto;
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-md);
  padding: var(--sp-3);
}
.notif-panel[hidden] { display: none; }
/* Обобщённый поповер (фильтр/сортировка списка сайтов) — та же поверхность
   и тень, что у .notif-panel, только без фиксированной ширины и с левым, а
   не правым выравниванием (кнопки стоят в левой части шапки, не у аватара). */
.popover-wrap { position: relative; }
.popover-panel {
  position: absolute; top: calc(var(--control-h-sm) + var(--sp-2)); left: 0; z-index: var(--z-pop);
  min-width: 200px; background: var(--surface-elevated);
  border: 1px solid var(--border); border-radius: var(--radius-card);
  box-shadow: var(--shadow-md); padding: var(--sp-2);
}
.popover-panel[hidden] { display: none; }
/* Панель «Вид»: секции — колонки, перенос по ширине. Одна колонка-простыня
   на ~16 строк нарушала компактность — читается сетка 2-3 колонки.
   width явно: popover-wrap узкий (кнопка-иконка), shrink-to-fit абсолютно
   спозиционированной панели схлопывал flex в одну колонку. */
#sitesViewPanel { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5); align-items: flex-start; width: 600px; max-width: calc(100vw - 48px); }
/* Меню действий карточки сайта (⋮) — тот же popover-panel, что у «Вид»,
   но выровнен по правому краю: кнопка сидит у правого края карточки,
   левое выравнивание (базовое правило выше) уводило бы панель за пределы
   карточки вправо. Переопределение стоит ПОСЛЕ базового .popover-panel —
   иначе порядок в каскаде при равной специфичности отдаёт left:0 базовому
   правилу, что мы уже словили один раз (2026-08-06). */
.site-card-menu-panel { left: auto; right: 0; min-width: 220px; }
.site-card-menu-panel a.popover-opt { text-decoration: none; }
.site-card-menu-panel .popover-opt.danger { color: var(--danger); }
.site-card-menu-panel .popover-opt.danger:hover { background: var(--danger-wash, rgba(239, 68, 68, .12)); }
.popover-sec { min-width: 150px; }
.popover-sec-label { font-size: var(--fs-micro); color: var(--text-tertiary); padding: 4px 8px; text-transform: uppercase; letter-spacing: .06em; }
.popover-opt {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  height: var(--control-h-sm); padding: 0 var(--sp-2); border: 0; border-radius: var(--radius-control);
  background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--fs-caption);
  text-align: left; cursor: pointer;
}
.popover-opt:hover { background: var(--surface-hover); color: var(--text-primary); }
.popover-opt span { white-space: nowrap; }
.popover-opt.on { color: var(--primary); }
.popover-opt .sp { flex: 1; }
.popover-opt-toggle { cursor: default; }
.popover-opt-toggle:hover { background: transparent; color: var(--text-secondary); }
.popover-opt-toggle .switch { margin: 0; }
.popover-opt .mark { opacity: 0; }
.popover-opt.on .mark { opacity: 1; }
/* align-items:center + текст flex:1 — без этого кнопка "Исправить" тянулась
   по высоте строки (stretch по умолчанию) и стояла сразу после текста
   вместо прижатой к правому краю. */
.notif-item { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-2); border-radius: var(--radius-sm); font-size: var(--fs-caption); cursor: pointer; }
.notif-item > .icon { flex-shrink: 0; }
.notif-item > span { flex: 1; min-width: 0; }
/* «Исправить» — компактная пилюля на шкале бейджей (--badge-h), а не
   полноразмерная кнопка 32px: в узком дропдауне с caption-строками она
   раздувала ряд и висела глыбой. */
.notif-item [data-notif-fix] {
  flex-shrink: 0; height: var(--badge-h); padding: 0 var(--sp-3);
  border-radius: var(--radius-badge); background: var(--primary-wash); color: var(--primary);
  box-shadow: none; font-size: var(--fs-micro);
}
.notif-item [data-notif-fix]:hover { box-shadow: var(--glow-soft); }
.notif-item:hover { background: var(--surface-hover); }
.notif-wrap { position: relative; }

/* Направления выгрузки бэкапов — три колонки в один ряд (S3/Яндекс/Telegram).
   align-items: start — каждый блок высотой по своему контенту: растяжка по
   самому высокому (S3) давала пустые провалы в Яндекс.Диске и Telegram. */
/* Направления выгрузки: S3 — широкая левая колонка, Яндекс.Диск + Telegram —
   стопкой во второй (раньше три равные колонки в ряд: Telegram почти пустой,
   а S3 тесно). */
.bk-dest-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); align-items: start; margin-bottom: var(--sp-4); }
.bk-dest-col { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
@media (max-width: 1100px) { .bk-dest-grid { grid-template-columns: 1fr; } }

/* Пустое состояние раздела — КАРТОЧКА того же размера, что тайлы на
   «Главной» (360px × --card-min-h), стоит слева первой, не центрируется
   по странице. Контент внутри — по центру карточки. */
.empty-tile {
  width: 360px; max-width: 100%; min-height: var(--card-min-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-3); text-align: center; color: var(--text-secondary);
}
.empty-tile .icon-xl { color: var(--text-tertiary); }
.empty-tile h3 { margin: 0; color: var(--text-primary); }
.empty-tile[hidden] { display: none; }

/* --- Обзор: раскладка контент + правая колонка --- */
/* Колонки и gap — ДОСЛОВНО те же, что у .app-topbar: тогда левый край поиска
   совпадает с левым краем панели, а переключатель сетка/список встаёт ровно
   по правому краю сетки карточек. flex:1/min-height:0 — обзор живёт по
   правилу «без скролла страницы»: секция занимает ровно высоту viewport за
   вычетом топбара/паддингов, переполнение — только внутри списков. */
.overview-layout {
  position: relative;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--aside-col);
  gap: var(--page-gutter);
  align-items: stretch;
}
@media (max-width: 1000px) { .overview-layout { grid-template-columns: 1fr; } }
/* Единая «стеклянная» панель — тот же приём, что у .sidebar (см. --sidebar-glass):
   не стопка непрозрачных карточек разного веса, а один слой поверх страницы. */
.overview-aside {
  display: flex; flex-direction: column; gap: 0;
  /* static + height:100%: обзор без страничного скролла, списки внутри
     панели скроллятся сами (см. #view-overview ниже). */
  position: static; height: 100%; min-height: 0; overflow: hidden;
  background: var(--sidebar-glass);
  background-image: linear-gradient(180deg, rgba(255,255,255,.05), transparent 60%);
  backdrop-filter: blur(20px) saturate(1.3);
  -webkit-backdrop-filter: blur(20px) saturate(1.3);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  overflow: hidden;
}
/* Свернуть правую колонку — НАСТОЯЩЕЕ сворачивание: панель сжимается до
   полоски с кнопкой разворота, контент получает всю ширину (раньше прятались
   только карточки, а панель висела пустой). */
.overview-layout { transition: grid-template-columns var(--dur-slow) var(--ease); }
.overview-layout:has(> .overview-aside.collapsed) { grid-template-columns: minmax(0, 1fr) 44px; }
.overview-aside { transition: width var(--dur-slow) var(--ease); }
.overview-aside.collapsed { width: 44px; overflow: hidden; }
.overview-aside.collapsed .aside-title,
.overview-aside.collapsed .aside-badge,
.overview-aside.collapsed .aside-card { display: none; }
.overview-aside.collapsed .aside-head { height: auto; padding: var(--sp-2) 0 0; justify-content: center; }
.overview-aside-collapse-ico { transition: transform var(--dur) var(--ease); }
.overview-aside.collapsed .overview-aside-collapse-ico { transform: rotate(180deg); }

/* Один язык на все иконки-кнопки в приложении: тот же тонированный кружок
   в покое, что и у статус-иконок (.badge-icon) рядом — не прозрачная кнопка,
   проявляющаяся только на hover, вперемешку с уже видимыми иконками. */
/* Тот же рецепт, что у иконок-кнопок топбара (.btn.btn-icon) — один тип
   элемента, один стиль везде, а не залитый кружок в одном месте и прозрачное
   кольцо в другом. */
.ov-icon-btn {
  display: flex; align-items: center; justify-content: center;
  width: var(--control-h-sm); height: var(--control-h-sm); border: 0; border-radius: var(--radius-control);
  background: transparent; color: var(--text-secondary); cursor: pointer;
  box-shadow: none;
  transition: color var(--dur-glow) var(--ease), background var(--dur-glow) var(--ease), box-shadow var(--dur-glow) var(--ease);
}
.ov-icon-btn:hover { color: var(--text-primary); transition-duration: var(--dur); }

/* === Единое выделение ВСЕХ кнопок: градиентная кайма (индиго → циан) ===
   ★ ОДОБРЕНО ПОЛЬЗОВАТЕЛЕМ 2026-08-02 как эталонный стиль панели («идеальное
   выделение, дорогое стекло») — НЕ МЕНЯТЬ механику, градиент и тайминги.
   Новые типы кнопок/вкладок только ПОДКЛЮЧАЮТСЯ к этому блоку.
   Двухслойная механика (та же, что у иконок-вкладок и пилюль): ::before —
   градиент на весь элемент, ::after — непрозрачная крышка интерьера с
   inset 1px; mask-composite НЕ используем (рваный пиксельный край).
   Оба слоя живут всегда с opacity 0 — hover/active включаются плавно.
   Специализированные блоки (chip-group-icon, pills) имеют
   свои ::before/::after с более высокой специфичностью — не конфликтуют. */
.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon {
  position: relative; isolation: isolate;
}
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon)::before {
  content: ""; position: absolute; inset: 0; z-index: -2; border-radius: inherit;
  background: linear-gradient(135deg, var(--primary-hover), var(--primary) 55%, #22d3ee);
  pointer-events: none; opacity: 0; transition: opacity var(--dur-glow) var(--ease);
}
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon)::after {
  content: ""; position: absolute; inset: 1px; z-index: -1; border-radius: inherit;
  background: var(--surface-hover);
  pointer-events: none; opacity: 0; transition: opacity var(--dur-glow) var(--ease);
}
/* Hover: кайма приглушена, интерьер закрыт крышкой (градиент не проступает). */
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon):hover::before { opacity: .55; transition-duration: var(--dur); }
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon):hover::after { opacity: 1; transition-duration: var(--dur); }
/* Active/selected: полная кайма + свечение — одно выделение на всю панель. */
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon).active::before { opacity: 1; transition-duration: var(--dur); }
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon).active::after { opacity: 1; }
/* Меню — исключение из «active держит крышку»: у активного пункта в покое
   только градиентная кайма, без постоянной заливки. Крышка — только :hover. */
.nav-item.active::after { opacity: 0; }
:is(.ov-icon-btn, .view-toggle-btn, .dock-btn, .nav-item, .ov-tile,
.topbar-search, .topbar-aside-col-r .btn-icon).active {
  color: var(--text-primary);
  box-shadow: 0 0 14px rgba(99, 102, 241, .18);
}

/* Шапка правой панели — подпись тем же мелким uppercase-стилем, что и
   заголовки разделов внутри (одна роль — один стиль), значок-глиф в
   тонированной плашке и счётчик «требует внимания». */
.aside-head {
  display: flex; align-items: center; gap: var(--sp-2);
  height: 48px; padding: 0 var(--sp-4); flex-shrink: 0;
}
.aside-head .sp { flex: 1; }
/* Кнопка сворачивания панели — тот же пул .ov-icon-btn (32×32), один размер
   для всех иконок-действий панели. */
.aside-head-ico {
  width: var(--control-h-sm); height: var(--control-h-sm); border-radius: var(--radius-control); flex-shrink: 0; border: 0; cursor: pointer;
  display: grid; place-items: center; background: transparent; color: var(--text-secondary);
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.aside-head-ico:hover { color: var(--text-primary); background: var(--surface-hover); }
.aside-head-ico .icon { width: 16px; height: 16px; }
.aside-title {
  font-size: var(--fs-micro); font-weight: 400; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.aside-badge { background: var(--primary-wash); color: var(--primary); }

/* Правая колонка: активность / быстрые действия / тариф — секции одной
   стеклянной панели (.overview-aside), а не отдельные непрозрачные карточки. */
.aside-card {
  background: transparent; border: 0; border-bottom: 1px solid var(--border);
  border-radius: 0; padding: var(--sp-3) var(--sp-4); flex-shrink: 0;
}
.aside-card:last-child { border-bottom: 0; }
.aside-card h3 {
  font-size: var(--fs-micro); font-weight: 400; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-tertiary); margin: 0 0 var(--sp-2);
}
.activity-item { display: flex; gap: var(--sp-3); padding: 6px 0; }
.activity-icon {
  width: 30px; height: 30px; border-radius: var(--radius-sm); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--primary-wash); color: var(--primary);
}
.activity-icon.tone-green { background: var(--success-wash); color: var(--success); }
.activity-icon.tone-warn { background: var(--warning-wash); color: var(--warning); }
.activity-icon.tone-blue { background: var(--tone-blue-bg); color: var(--tone-blue); }
.activity-icon.tone-danger { background: var(--danger-wash); color: var(--danger); }
.activity-body { min-width: 0; }
.activity-text { font-size: var(--fs-micro); color: var(--text-primary); line-height: 1.35; }
.activity-time { font-size: 10px; color: var(--text-tertiary); margin-top: 2px; }
.aside-link-btn { width: 100%; margin-top: var(--sp-2); }

.quick-action {
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; padding: 6px var(--sp-2); border-radius: var(--radius-sm);
  /* Еле заметная рамка: ряд «Быстрых действий» читается упорядоченным
     списком, а не висящим в воздухе текстом. Hover/active не трогаем. */
  border: 1px solid var(--border);
  background: transparent; color: var(--text-secondary); cursor: pointer;
  /* Тот же шрифт и кегль, что у пунктов меню выше (.nav-item = fs-body),
     но легче: 400 вместо 500 — вспомогательная строка,
     иерархия читается весом, не размером. */
  font-family: inherit; font-size: var(--fs-body); font-weight: 400; text-align: left;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.quick-action:hover { background: var(--surface-hover); color: var(--text-primary); }
.quick-action .icon { width: 20px; height: 20px; color: var(--text-tertiary); } /* было 16px — на глаз слишком мелко рядом с текстом */
.quick-action:hover .icon { color: var(--primary); }
.quick-action .qa-plus { margin-left: auto; font-size: 15px; color: var(--text-tertiary); }

.tariff-title { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.tariff-badge {
  display: inline-block; font-size: 10px; font-weight: 500; letter-spacing: .08em;
  text-transform: none; padding: 3px 10px; border-radius: var(--radius-badge);
  background: linear-gradient(135deg, var(--primary), #a855f7); color: #fff;
}
.tariff-rows { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-3); font-size: var(--fs-caption); }
.tariff-row { display: flex; justify-content: space-between; color: var(--text-secondary); }
.tariff-row b { color: var(--text-primary); font-weight: 400; }

/* ==========================================================================
   Обзор v2.1 — жёсткое правило «без скролла страницы»: секция занимает ровно
   высоту viewport за вычетом топбара и паддингов main, а переполнение
   разрешается только внутри списков (логи, активность) и горизонтальных рядов
   карточек (сайты, SSL). Работает и при 100% масштабе (1080 CSS px), и при
   150% (720 CSS px) — за счёт гибкой duo-зоны и компактных карточек.
   ========================================================================== */

#view-overview {
  display: flex;
  flex-direction: column;
  /* Низ — на одной линии с нижним краем дока (тот же --page-gutter, что и
     отступ дока от низа viewport). Верх: топбар 64 + паддинг main (sp-4). */
  height: calc(100vh - 64px - var(--sp-4) - var(--page-gutter));
  overflow: hidden;
}
#view-overview[hidden] { display: none; } /* id-селектор выше сильнее .view[hidden] — возвращаем скрытие */

.overview-main {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

/* Правая колонка — вписана в высоту, активность скроллится внутри */
.aside-card.activity-card { flex: 1; min-height: 0; display: flex; flex-direction: column; flex-shrink: 1; }
#ovActivity { flex: 1; min-height: 0; overflow-y: auto; }

/* Просмотрщик nginx-логов на странице «Логи» */
.log-viewer {
  margin: 0;
  max-height: 420px;
  overflow: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--sp-3) var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  line-height: 1.55;
  color: var(--text-secondary);
  white-space: pre-wrap; /* нужно <pre>-вьюверам (docker-логи); у .log-line свои правила */
  word-break: break-all;
}
/* Строка лога — две колонки: время слева (фикс., тусклое, tabular-nums),
   содержание справа. Время — главная ось чтения лога, оно должно стоять
   первым, а не тонуть в середине raw-строки. */
.log-line { display: grid; grid-template-columns: 118px 1fr; gap: var(--sp-3); }
.log-line + .log-line { margin-top: 1px; }
.log-time { color: var(--text-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; user-select: none; }
.log-rest { white-space: pre-wrap; word-break: break-all; min-width: 0; }

/* Журнал: nginx-логи и аудит действий — две колонки с перетаскиваемым
   разделителем. Ширина левой колонки — --audit-left (ставит initAuditSplitter
   в app.js), по умолчанию 50/50. В сплите карточкам не нужен нижний margin —
   он остался от времён вертикальной стопки. */
.audit-split {
  display: grid;
  grid-template-columns: var(--audit-left, 1fr) 10px minmax(0, 1fr);
  align-items: stretch;
}
.audit-split > .card { margin-bottom: 0; min-width: 0; }
.audit-split.resizing { user-select: none; cursor: col-resize; }
/* Разделитель — тонкая риска по центру своей 10px дорожки, подсвечивается
   акцентом на hover и во время перетаскивания. */
.audit-splitter { cursor: col-resize; position: relative; }
.audit-splitter::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 2px; height: 48px; border-radius: 99px;
  background: var(--border-hover);
  transition: background var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.audit-splitter:hover::before,
.audit-split.resizing .audit-splitter::before { background: var(--primary); box-shadow: var(--glow-soft); }
@media (max-width: 900px) {
  .audit-split { grid-template-columns: 1fr; }
  .audit-splitter { display: none; }
  .audit-split > .card { margin-bottom: var(--sp-5); }
}

/* ==========================================================================
   Пачка 3: глобальный поиск (Ctrl+K) и подсветка строки сайта
   ========================================================================== */

.palette-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  background: var(--backdrop);
  backdrop-filter: blur(2px);
  display: flex;
  justify-content: center;
  padding-top: 12vh;
}
.palette-overlay[hidden] { display: none; }
.palette {
  width: 520px;
  max-width: 92vw;
  height: fit-content;
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  animation: dialog-in var(--dur) var(--ease);
}
.palette input {
  width: 100%;
  height: 48px;
  padding: 0 var(--sp-4);
  border: none;
  border-bottom: 1px solid var(--border);
  background: transparent;
  color: var(--text-primary);
  font-size: var(--fs-body);
  font-family: inherit;
  outline: none;
}
.palette input::placeholder { color: var(--text-tertiary); }
.palette-list { max-height: 46vh; overflow-y: auto; padding: var(--sp-2); }
.palette-group {
  padding: var(--sp-2) var(--sp-3) 4px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.palette-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 8px var(--sp-3);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--fs-caption);
  color: var(--text-primary);
}
.palette-item .icon { color: var(--text-tertiary); flex-shrink: 0; }
.palette-item.active, .palette-item:hover { background: var(--primary-wash); }
.palette-item.active .icon, .palette-item:hover .icon { color: var(--primary); }
.palette-item-sub { margin-left: auto; font-size: 10px; color: var(--text-tertiary); }
.palette-empty { padding: var(--sp-5); text-align: center; color: var(--text-tertiary); font-size: var(--fs-caption); }

/* Подсветка сайта после перехода из поиска/уведомления */
@keyframes row-flash {
  0%, 60% { background: var(--primary-wash); }
  100% { background: transparent; }
}
.row-flash { animation: row-flash 1.6s var(--ease); }

/* Разделитель и подзаголовок в дропдауне уведомлений */
.notif-sep { height: 1px; background: var(--border); margin: var(--sp-2) 0; }
.notif-sub {
  padding: 4px var(--sp-2);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.notif-item-time { margin-left: auto; font-size: 10px; color: var(--text-tertiary); white-space: nowrap; }
/* Непрочитанное событие — тонкая акцентная кромка слева, не заливка. */
.notif-item-unread { box-shadow: inset 2px 0 0 var(--primary); }
.notif-item-unread > .icon { color: var(--primary); }

/* ==========================================================================
   Мобильная вёрстка (брейкпоинты 900px и 640px)
   Блок в самом конце файла намеренно: перебивает базовые правила и более
   поздние секции (collapsed-сайдбар, #view-overview) без !important.
   На десктопе (> 900px) ничего из этого не применяется.
   ========================================================================== */

@media (max-width: 900px) {
  /* Сайдбар — off-canvas drawer: по умолчанию уехал за левый край,
     открывается классом .nav-open на .app-shell (тоггл в app.js). */
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    height: 100vh;
    width: min(300px, 85vw);
    z-index: var(--z-drawer);
    transform: translateX(-100%);
    transition: transform var(--dur) var(--ease);
  }
  .app-shell.nav-open .sidebar { transform: translateX(0); box-shadow: var(--shadow-md); }

  .nav-backdrop {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-drawer) - 5);
    background: var(--backdrop);
    backdrop-filter: blur(2px);
  }
  .nav-backdrop[hidden] { display: none; }

  /* Collapsed-режим (только иконки) на мобильном отключаем — drawer всегда полный */
  .app-shell.collapsed .sidebar { width: min(300px, 85vw); padding: var(--sp-5) var(--sp-3); }
  .app-shell.collapsed .nav-item span { display: block; }
  .app-shell.collapsed .sidebar-footer { display: flex; }
  .app-shell.collapsed .nav-item { justify-content: flex-start; padding: 0 var(--sp-3); }
  .app-shell.collapsed .sidebar-brand { justify-content: flex-start; padding: 0 var(--sp-2) var(--sp-6); }

  /* Топбар ужимаем: без меты юзера (только аватар), меньше паддинги */
  .app-topbar { padding: 0 var(--sp-3); gap: var(--sp-3); }
  .topbar-actions { gap: var(--sp-3); }
  .topbar-user-meta { display: none; }

  main { padding: var(--sp-4); }

  /* «Обзор»: на десктопе высота жёстко под viewport — на мобильном страница скроллится */
  #view-overview { height: auto; overflow: visible; }
  .overview-main { overflow: visible; }
  .overview-aside { height: auto; overflow: visible; }
  .aside-card.activity-card { flex: none; }

  /* Таблицы не разъезжаются — скроллятся горизонтально внутри .data-grid-wrap */
  .data-grid-wrap .sites-table { min-width: 640px; }

  /* Выпадашки и тосты не вылезают за вьюпорт */
  .notif-panel { max-width: calc(100vw - 32px); }
  .scan-toast { max-width: calc(100vw - 32px); }
}

@media (max-width: 640px) {
  /* Топбар: текст переключателя кэша скрыт */
  .switch { font-size: 0; gap: 0; }
  .topbar-actions { gap: var(--sp-2); }

  main { padding: var(--sp-3); }

  /* Все гриды — в одну колонку */
  .card-grid { grid-template-columns: 1fr; }
  .limits-grid { grid-template-columns: 1fr; }
  .settings-extra-grid { grid-template-columns: 1fr; }

  .panel, .card { padding: var(--sp-4); }

  /* Диалоги — почти во весь экран, кнопки переносятся и растягиваются */
  dialog, dialog.wide { width: calc(100vw - 16px); max-width: calc(100vw - 16px); }
  dialog form, dialog .dialog-body { padding: var(--sp-4); }
  dialog.wide .dialog-body, dialog.wide form { padding: var(--sp-4); }
  .dialog-actions { flex-wrap: wrap; }
  .dialog-actions .btn { flex: 1 1 auto; }

  /* Шапки страниц с действиями — в столбик */
  .page-header.with-action { flex-direction: column; align-items: stretch; gap: var(--sp-3); }
  .page-header.with-action .header-actions { justify-content: flex-start; flex-wrap: wrap; }
}

/* ==========================================================================
   Дашборд: мелочи таблицы сайтов (статус-точки, подписи)
   ========================================================================== */

.dot.down { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-wash); }

.site-subline { font-size: var(--fs-caption); margin-top: 2px; }

/* ==========================================================================
   Skeleton-загрузка — шиммер-плейсхолдеры, пока данные не пришли.
   .skeleton — сам элемент-заглушка (блочный), .skel-line — строка текста.
   Цвета — через --skeleton-base/--skeleton-shine (обе темы).
   ========================================================================== */

.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s linear infinite;
  color: transparent !important; /* текст-заглушку не показываем — только форму */
  pointer-events: none;
  user-select: none;
}
.skeleton * { visibility: hidden; } /* внутри skeleton-карточки контент прячем, оставляем габарит */
@keyframes skeleton-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }

.skel-line { display: block; height: 12px; margin: 8px 0; width: 70%; }
.skel-line.short { width: 40%; }

/* ==========================================================================
   Меню профиля в топбаре (тема / плотность / выход) — то же семейство, что
   .notif-panel: выпадашка справа от кнопки-аватара.
   ========================================================================== */

.user-menu-wrap { position: relative; }
.avatar-btn { border: none; cursor: pointer; font-family: inherit; transition: transform var(--dur) var(--ease); }
.avatar-btn:hover { box-shadow: var(--glow-soft); }
.avatar-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.user-menu {
  position: absolute; top: 46px; right: 0; z-index: var(--z-pop);
  width: 260px; max-width: calc(100vw - 32px);
  background: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-md);
  padding: var(--sp-2);
}
.user-menu[hidden] { display: none; }
.user-menu-head { display: flex; flex-direction: column; gap: 2px; padding: var(--sp-2) var(--sp-3); }
.user-menu-head b { font-size: var(--fs-caption); color: var(--text-primary); }
.user-menu-head span { font-size: 10px; color: var(--text-tertiary); }
.user-menu-item {
  display: flex; align-items: center; gap: var(--sp-3); width: 100%;
  padding: 8px var(--sp-3); border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-secondary); cursor: pointer;
  font-family: inherit; font-size: var(--fs-caption); text-align: left;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.user-menu-item:hover { background: var(--surface-hover); color: var(--text-primary); }
.user-menu-item .icon { color: var(--text-tertiary); flex-shrink: 0; }
.user-menu-item:hover .icon { color: var(--primary); }
.user-menu-item b { color: var(--text-primary); font-weight: 400; }
.user-menu-sep { height: 1px; background: var(--border); margin: var(--sp-2); }
/* Плотность внутри меню: чипы в две равные кнопки без нижнего отступа */
.user-menu-density { padding: 4px var(--sp-3) var(--sp-2); }
.user-menu-density .chip-group { display: flex; width: 100%; margin-bottom: 0; }
.user-menu-density .chip { flex: 1; justify-content: center; height: 30px; padding: 0 var(--sp-2); }
.user-menu-label { font-size: 10px; font-weight: 500; letter-spacing: .07em; text-transform: uppercase; color: var(--text-tertiary); padding: var(--sp-2) var(--sp-3) 0; }

.user-menu-gradient { display: flex; align-items: center; gap: var(--sp-2); padding: 4px var(--sp-3) var(--sp-2); }
.user-menu-gradient input[type="color"] {
  width: var(--control-h-sm); height: var(--control-h-sm); padding: 0; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: none; cursor: pointer; flex-shrink: 0;
}
.user-menu-gradient input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
.user-menu-gradient input[type="color"]::-webkit-color-swatch { border: none; border-radius: 4px; }
.user-menu-gradient input[type="range"] { flex: 1; min-width: 0; accent-color: var(--primary); }
.user-menu-gradient .btn-icon { flex-shrink: 0; }

/* Настройка видимых пунктов меню сайдбара (пер-юзер, localStorage) —
   галочка видимости и drag-n-drop порядка в одной строке. */
.nav-cfg-list { max-height: 240px; overflow-y: auto; }
.nav-cfg-item {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: 6px var(--sp-2); border-radius: var(--radius-sm);
  color: var(--text-secondary); font-size: var(--fs-caption);
}
.nav-cfg-item:not(.on) { color: var(--text-tertiary); }
.nav-cfg-item.dragging { opacity: .4; }
.nav-cfg-grip { cursor: grab; color: var(--text-tertiary); flex-shrink: 0; user-select: none; }
.nav-cfg-grip:active { cursor: grabbing; }
.nav-cfg-label { flex: 1; }
.nav-cfg-check {
  border: none; background: transparent; padding: 0; cursor: pointer;
  width: 26px; height: 26px; display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); flex-shrink: 0;
}
.nav-cfg-check:hover { background: var(--surface-hover); }
/* Галочка стоит на месте ВСЕГДА, в обоих состояниях: кнопка фиксированного
   размера, глиф не стирается — меняются только цвет/прозрачность/свечение.
   Пустой .mark сжимал кнопку до невидимой полоски: выключенный пункт было
   почти невозможно включить обратно, а сам переключатель «прыгал». */
.nav-cfg-item .mark {
  color: var(--text-tertiary); opacity: .45; font-weight: 600;
  transition: color var(--dur) var(--ease), opacity var(--dur) var(--ease), filter var(--dur) var(--ease);
}
.nav-cfg-item.on .mark { color: var(--primary); opacity: 1; filter: drop-shadow(0 0 6px rgba(99, 102, 241, .55)); }

/* ==========================================================================
   Плотность интерфейса (compact / comfortable) — UI-настройка юзера из
   настроек, хранится в localStorage, класс вешается на <body>.
   comfortable — дефолтные значения выше, так что правила нужны только для
   compact. Глобальные --sp-* не переопределяем (они задают ритм всей
   раскладки) — компактность точечно жмёт паддинги таблиц/карточек/списков.
   ========================================================================== */

body.density-compact { --fs-caption: 11px; --fs-micro: 11px; --row-h: 36px; }
body.density-compact .sites-table thead th { padding: var(--sp-2) var(--sp-3); }
body.density-compact .panel, body.density-compact .card { padding: var(--sp-4); margin-bottom: var(--sp-4); }
body.density-compact .site-card { padding: var(--sp-4); gap: var(--sp-2); }
body.density-compact .fm-list-item { padding: var(--sp-1) var(--sp-2); }
body.density-compact .nav-item { height: 34px; }
body.density-compact .aside-card { padding: var(--sp-2) var(--sp-3); }

/* ==========================================================================
   Системное «уменьшить прозрачность» (macOS/iOS): стекло выключаем —
   поверхности становятся непрозрачными, blur убираем. Читаемость важнее вау.
   ========================================================================== */
@media (prefers-reduced-transparency: reduce) {
  :root, html[data-theme="light"] { --surface-glass: var(--surface); }
  .panel, .card, .site-card, .overview-aside, .dock {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* --- Fleet (master): сателлиты + лог команд ------------------------------ */
.fleet-layout { display: flex; gap: 16px; align-items: flex-start; }
.fleet-main { flex: 1; min-width: 0; }
.fleet-aside { width: 340px; flex-shrink: 0; }
@media (max-width: 1100px) { .fleet-layout { flex-direction: column; } .fleet-aside { width: 100%; } }
.fleet-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
.sat-card { padding: 14px; display: flex; flex-direction: column; gap: 10px; }
.sat-head { display: flex; align-items: center; gap: 8px; }
.sat-meta { display: flex; flex-wrap: wrap; gap: 6px; }
.sat-bars { display: flex; flex-direction: column; gap: 6px; }
.sat-bar { height: 6px; border-radius: 4px; background: var(--surface-hover, rgba(255,255,255,.08)); overflow: hidden; }
.sat-bar i { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.sat-bar i.warn { background: var(--warning, #f59e0b); }
.sat-bar i.danger { background: var(--danger, #ef4444); }
.sat-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.sat-reports { display: flex; flex-direction: column; gap: 4px; font-size: 12px; border-top: 1px solid var(--border, rgba(255,255,255,.08)); padding-top: 8px; }
.sat-report { display: flex; gap: 6px; align-items: baseline; overflow: hidden; }
.sat-report code { flex-shrink: 0; }
.sat-report.bad { color: var(--danger, #ef4444); }
.fleet-log { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; max-height: 420px; overflow-y: auto; }
