/*
  Точка входу стилів і єдине місце, де оголошено, що за чим іде.

  Кроку збірки в деплої немає й тут: цей файл читає сам застосунок на старті
  й підставляє замість кожного @import вміст названого файлу, загорнутий у
  його шар (shared/assets.py). Для каскаду це те саме — @import ... layer(X)
  і @layer X { … } рівносильні за специфікацією. Для браузера різниця велика:
  доти він дізнавався про ці тридцять файлів лише розібравши цей, тобто перше
  малювання сторінки чекало два оберти по мережі замість одного.

  Правити тут і в сусідніх файлах — як і раніше; ASSETS_CACHE=false у .env
  перескладає все на кожен запит, тож локально нічого не змінилось.

  Порядок шарів нижче ВАЖЛИВІШИЙ за специфічність: правило з пізнішого
  шару виграє в правила з ранішого, хай яким «сильним» той селектор був.
  Завдяки цьому в проєкті майже немає !important і немає гонки селекторів.

  Хто де: rhythm — вертикальні відступи МІЖ блоками сторінки (мусить стояти
  після components, бо гасить власні margin компонентів), service —
  інлайнові стилі конкретного сервіса, utilities — точкові класи-виправлення.
*/
@layer reset, tokens, base, layout, components, rhythm, service, utilities;

@layer reset {
/* 00-reset.css */
/*
  Мінімальний ресет — лише те, без чого дефолти браузера справді заважають.
  Свідомо не «обнуляємо все»: типографіку й відступи задає 20-base.css.
*/

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Атрибут hidden має вимикати елемент незалежно від того, який display
   йому задав компонентний клас. Без цього правила будь-який .hint чи .badge
   із власним display перебиває стиль браузера й лишається на видноті —
   саме через це підказка про Caps Lock висіла на сторінці входу завжди.

   !important тут не «сильніший селектор», а перемикач порядку шарів: для
   важливих оголошень шари рахуються у зворотному порядку, тож reset —
   найраніший у списку — б'є і components, і utilities. Єдине місце в
   проєкті, де це доречно: правило має діяти скрізь і без винятків. */
[hidden] {
  display: none !important;
}

/* display:block прибирає «зайвий» відступ під картинкою від рядкового
   потоку, max-width не дає їй вилізти за контейнер. */
img,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
}

/* Поля форм не успадковують шрифт від сторінки самі — доводиться просити. */
input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

table {
  border-collapse: collapse;
}

}
@layer tokens {
/* 10-tokens.css */
/*
  Токени дизайн-системи — єдине місце, де задаються кольори, відступи
  й розміри. Далі всі компоненти беруть готові змінні й не рахують нічого самі.

  Дві ідеї, на яких усе тримається:

  1. Кольори виводяться з ВІДТІНКУ (hue), а не задаються поштучно.
     Одне число --h-accent → і вся акцентна гама сторінки перерахувалась.
     Саме тому сервіс отримує свій колір одним рядком у registry.py.

  2. Світла й темна теми живуть в одному оголошенні через light-dark().
     Ніяких дублів на кожен колір і жодного окремого «темного файлу».
     Перемикає їх color-scheme нижче.
*/
:root {
  /* Обидві теми доступні; фактичну обирає data-theme або система. */
  color-scheme: light dark;

  --h-brand: 265;

  /* Відтінки станів. Вибрані так, щоб читались і на світлому, і на темному. */
  --h-ok: 150;
  --h-warn: 75;
  --h-fail: 25;
  --h-info: 230;

  /* Акцент сторінки. Дефолт — брендовий, але base.html перебиває його
     інлайном на <body>, підставляючи hue поточного сервісу. */
  --h-accent: var(--h-brand);

  /* Поверхні: bg — тло сторінки, далі за зростанням «підняття».
     Легка домішка brand-відтінку не дає сірому виглядати брудним. */
  --bg:        light-dark(oklch(.985 .003 var(--h-brand)), oklch(.170 .015 var(--h-brand)));
  --surface:   light-dark(oklch(1    0    0),              oklch(.210 .018 var(--h-brand)));
  --surface-2: light-dark(oklch(.975 .004 var(--h-brand)), oklch(.250 .020 var(--h-brand)));
  --surface-3: light-dark(oklch(.955 .006 var(--h-brand)), oklch(.290 .022 var(--h-brand)));

  /* Текст: ink — основний, muted — другорядний, subtle — ледь помітний.
     У темній темі порядок muted/subtle за світлістю навмисно
     перевернутий: там «тихіший» означає темніший, а не світліший. */
  --ink:        light-dark(oklch(.250 .020 var(--h-brand)), oklch(.940 .008 var(--h-brand)));
  --ink-muted:  light-dark(oklch(.550 .020 var(--h-brand)), oklch(.680 .020 var(--h-brand)));
  --ink-subtle: light-dark(oklch(.680 .015 var(--h-brand)), oklch(.550 .018 var(--h-brand)));

  --border:        light-dark(oklch(.910 .008 var(--h-brand)), oklch(.300 .020 var(--h-brand)));
  --border-strong: light-dark(oklch(.840 .012 var(--h-brand)), oklch(.380 .024 var(--h-brand)));

  --brand:     light-dark(oklch(.550 .20 var(--h-brand)), oklch(.700 .16 var(--h-brand)));
  --brand-ink: light-dark(oklch(1 0 0),                   oklch(.170 .02 var(--h-brand)));

  /* Похідні від --brand через color-mix: підмішування до поверхні (soft,
     line) або до тексту (hover, active). Так відтінки лишаються
     узгодженими самі собою, у будь-якій темі. */
  --brand-soft:   color-mix(in oklch, var(--brand) 12%, var(--surface));
  --brand-line:   color-mix(in oklch, var(--brand) 35%, var(--border));
  --brand-hover:  color-mix(in oklch, var(--brand) 88%, var(--ink));
  --brand-active: color-mix(in oklch, var(--brand) 76%, var(--ink));

  /* Те саме, але від --h-accent. Різниця лише в тому, що акцент
     змінюється від сторінки до сторінки, а бренд — ні. */
  --accent:      light-dark(oklch(.550 .20 var(--h-accent)), oklch(.700 .16 var(--h-accent)));
  --accent-soft: color-mix(in oklch, var(--accent) 12%, var(--surface));
  --accent-line: color-mix(in oklch, var(--accent) 40%, var(--border));

  --focus-ring: 2px solid var(--brand);
  --focus-offset: 2px;

  /* Стани — одна трійка на кожен: ink (текст/іконка), bg (тло), line (рамка).
     Пропорції домішування скрізь однакові, тож алерти, бейджі й рядки
     таблиць виглядають як одна сім'я. */
  --ok-ink:   light-dark(oklch(.450 .13 var(--h-ok)),   oklch(.800 .14 var(--h-ok)));
  --ok-bg:    color-mix(in oklch, var(--ok-ink) 12%, var(--surface));
  --ok-line:  color-mix(in oklch, var(--ok-ink) 32%, var(--border));

  --warn-ink:  light-dark(oklch(.500 .12 var(--h-warn)), oklch(.820 .14 var(--h-warn)));
  --warn-bg:   color-mix(in oklch, var(--warn-ink) 12%, var(--surface));
  --warn-line: color-mix(in oklch, var(--warn-ink) 32%, var(--border));

  --fail-ink:  light-dark(oklch(.500 .18 var(--h-fail)), oklch(.780 .15 var(--h-fail)));
  --fail-bg:   color-mix(in oklch, var(--fail-ink) 12%, var(--surface));
  --fail-line: color-mix(in oklch, var(--fail-ink) 32%, var(--border));

  --info-ink:  light-dark(oklch(.500 .15 var(--h-info)), oklch(.800 .13 var(--h-info)));
  --info-bg:   color-mix(in oklch, var(--info-ink) 12%, var(--surface));
  --info-line: color-mix(in oklch, var(--info-ink) 32%, var(--border));

  /* Один множник на всю сітку відступів: змінивши --density, можна
     згустити чи розрідити інтерфейс цілком, не чіпаючи компоненти. */
  --density: 1;

  /* Півкроку. Потрібен рідко — там, де рядок списку має «дихати», але вже
     не поміщається в повний крок: стрічка кампаній, фішка в клітинці
     календаря. Окремим токеном, а не 2px у стилі сервісу, — інакше він
     перестає стискатись разом із рештою при зміні --density. */
  --s-0: calc(2px  * var(--density));

  --s-1: calc(4px  * var(--density));
  --s-2: calc(8px  * var(--density));
  --s-3: calc(12px * var(--density));
  --s-4: calc(16px * var(--density));
  --s-5: calc(24px * var(--density));
  --s-6: calc(32px * var(--density));
  --s-7: calc(48px * var(--density));
  --s-8: calc(64px * var(--density));

  /* Розміри тексту в rem — щоб масштаб шрифту в налаштуваннях браузера
     працював як має. */
  --t-xs:  0.6875rem;
  --t-sm:  0.75rem;
  --t-cmp: 0.8125rem;
  --t-md:  0.9375rem;
  --t-lg:  1.0625rem;
  --t-xl:  1.3125rem;
  --t-2xl: 1.6875rem;

  /* Системні шрифти: жодного веб-шрифту, тобто нуль зовнішніх запитів
     і нуль стрибків тексту при завантаженні. */
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "Cascadia Mono", Menlo, Consolas, monospace;

  --r-sm:   6px;
  --r-md:   8px;
  --r-lg:   12px;
  --r-full: 999px;

  /* У темній темі тіні помітно щільніші: на темному тлі слабка тінь
     просто не читається. */
  --shadow-1: light-dark(oklch(0 0 0 / .06), oklch(0 0 0 / .40));
  --shadow-2: light-dark(oklch(0 0 0 / .09), oklch(0 0 0 / .50));

  --e-1: 0 1px 2px var(--shadow-1);
  --e-2: 0 4px 12px var(--shadow-2), 0 1px 2px var(--shadow-1);
  --e-3: 0 12px 32px var(--shadow-2), 0 2px 6px var(--shadow-1);
}

/*
  Увесь перемикач теми — оці два правила. data-theme на <html> звужує
  color-scheme до однієї схеми, і всі light-dark() вище перемикаються
  разом. Без атрибута лишається "light dark", тобто рішення за системою.
  Ставить атрибут shared/static/js/theme.js.
*/
:root[data-theme="light"] {
  color-scheme: light;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

}
@layer base {
/* 20-base.css */
/*
  Базова типографіка й вигляд «голих» тегів. Компоненти далі спираються
  на це й перевизначають лише те, що відрізняється.
*/

/*
  Сама сторінка не прокручується — прокрутка живе всередині .main
  (див. 30-layout.css). Резервувати тут жолоб під смугу не можна: він
  лежить ЗЗОВНІ <body>, тож топбар і бічна панель не діставали б до краю
  екрана, а на короткій сторінці там лишалася б порожня смужка фону.
*/

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--t-md);
  line-height: 1.55;
  background: var(--bg);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.011em;
  /* balance — щоб заголовок не лишав одне слово-сироту на другому рядку. */
  text-wrap: balance;
}

h1 { font-size: var(--t-xl); }
h2 { font-size: var(--t-lg); margin: var(--s-6) 0 var(--s-3); }
h3 { font-size: var(--t-md); margin: var(--s-5) 0 var(--s-2); }
h4 { font-size: var(--t-md); margin: var(--s-4) 0 var(--s-2); }

p {
  margin: 0 0 var(--s-3);
  text-wrap: pretty;
}

a {
  color: var(--brand);
  /* Підкреслення напівпрозоре в спокої й суцільне на ховері: лінк
     лишається впізнаваним, але не рябить у щільному тексті. */
  text-decoration-color: color-mix(in oklch, currentColor 35%, transparent);
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}

a:hover {
  text-decoration-color: currentColor;
}

/* Один фокус-ринг на весь застосунок. :focus-visible, а не :focus —
   рамка з'являється при навігації клавіатурою, але не після кліку мишею.
   Прибирати її не можна: без неї з клавіатури працювати неможливо. */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

label {
  display: block;
  margin: var(--s-4) 0 var(--s-1);
  font-weight: 600;
  font-size: var(--t-md);
}

::placeholder {
  color: var(--ink-subtle);
  opacity: 1;
}

code, kbd, samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

code {
  padding: 0.1em 0.35em;
  background: var(--surface-3);
  border-radius: var(--r-sm);
}

pre {
  margin: 0;
  padding: var(--s-4);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--t-cmp);
  line-height: 1.5;
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

pre code {
  padding: 0;
  background: none;
  font-size: inherit;
}

hr {
  height: 1px;
  margin: var(--s-5) 0;
  border: 0;
  background: var(--border);
}

::selection {
  background: var(--brand-soft);
  color: var(--ink);
}

/*
  Повага до системного «менше руху». Не вимикаємо анімації зовсім, а
  стискаємо до 0.01ms: так вони лишаються миттєвими, але події
  transitionend/animationend усе одно спрацьовують, і код, який на них
  розраховує, не зависає.
*/
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

}
@layer layout {
/* 30-layout.css */
/*
  Каркас сторінки: бічна панель + топбар + контент.

  Стан панелі приходить із data-атрибутів на <html> (їх ставить shell.js):
    data-sidebar="collapsed" — вузька смуга з іконок (широкий екран);
    data-drawer="open"       — панель поверх контенту (вузький екран).
  Самі атрибути нічого не роблять — уся поведінка описана тут.
*/

:root {
  --sidebar-w: 248px;
  --sidebar-rail: 60px;  /* ширина у згорнутому стані — лише під іконки */
  --topbar-h: 52px;

  /* Три ширини контенту: форма (щоб рядок не був завдовжки),
     звичайна сторінка і на всю ширину — для таблиць. */
  --w-form: 680px;
  --w-app: 1200px;
  --w-wide: 100%;
}

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  grid-template-rows: var(--topbar-h) 1fr;
  /* Панель займає обидва рядки — тому топбар починається праворуч від неї. */
  grid-template-areas:
    "sidebar topbar"
    "sidebar main";

  /* Рівно на висоту екрана, не min-height: прокручується лише .main, а
     панель із топбаром лишаються на місці. Так смуга прокрутки опиняється
     всередині контенту й не забирає собі праву межу каркаса. */
  height: 100dvh;
}

/* Згортання — це просто інша ширина колонки. Решта підлаштовується сама. */
:root[data-sidebar="collapsed"] .app-shell {
  --sidebar-w: var(--sidebar-rail);
}

.sidebar {
  grid-area: sidebar;

  view-transition-name: sidebar;
  display: flex;
  flex-direction: column;

  /* Висоту дає сітка каркаса; min-height: 0 — щоб вміст не розпирав комірку.

     Панель принципово не прокручується: власна смуга прокрутки поруч зі
     смугою контенту читалася як друга сторінка, а навігація має бути одним
     нерухомим полем, де пункт завжди на тому самому місці. Тому список
     компактний за розмірами (див. .side-link і @media height нижче) і
     вміщається цілком; зайве обрізається, а не гортається. */
  min-height: 0;
  overflow: hidden;
  background: var(--surface);
  border-right: 1px solid var(--border);
}

/* Шапка панелі: бренд і кнопка згортання. Заввишки рівно з топбар — лінія
   під нею продовжує нижню межу топбару в один рівний рядок. */
.sidebar-head {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--s-1);
  height: var(--topbar-h);
  padding: 0 var(--s-2) 0 var(--s-4);
  border-bottom: 1px solid var(--border);
}

.brand {
  display: flex;
  /* Займає весь вільний рядок, кнопка лишається притиснутою до правого краю;
     min-width: 0 — щоб довга назва обрізалась, а не виштовхувала кнопку. */
  flex: 1;
  min-width: 0;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-md);
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}

.brand-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.collapse-toggle {
  flex: none;
}

.brand .ico {
  color: var(--brand);
}

.side-nav {
  flex: 1;
  min-height: 0;
  padding: var(--s-2);
}

/* Підгрупа сервісів: заголовок і пункти під ним (див. shared/base.html).
   Власних відступів не має — їх задає .side-group. Обгортка потрібна заради
   згорнутої панелі: там заголовки ховаються, і без неї групи не було б чим
   розділити (правило нижче, поруч із рештою «згорнутого»). */
.side-group {
  margin: var(--s-3) var(--s-2) 2px;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.side-link {
  /* --h-side ставить base.html інлайном на кожне посилання сервісу.
     Немає його (Дашборд, розділи адмінки) — беремо брендовий відтінок. */
  --side-accent: light-dark(
    oklch(0.55 0.2 var(--h-side, var(--h-brand))),
    oklch(0.7 0.16 var(--h-side, var(--h-brand)))
  );

  display: flex;
  align-items: center;
  gap: var(--s-2);

  min-height: 32px;
  padding: 0 var(--s-2);
  font-size: var(--t-cmp);
  font-weight: 600;
  color: var(--ink-muted);
  text-decoration: none;
  white-space: nowrap;
  border-radius: var(--r-md);

  border-left: 3px solid transparent;
  transition: background-color 0.12s, color 0.12s;
}

.side-link .ico {
  color: var(--ink-subtle);
}

/* Запобіжник під назви сервісів: замість того щоб розпирати панель, задовгий
   підпис обрізається трьома крапками. min-width: 0 обов'язковий — без нього
   флекс-елемент не стискається менше за свій вміст і обрізання не спрацює.
   Повну назву видно при наведенні: base.html кладе її в title посилання. */
.side-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.side-link:hover {
  color: var(--ink);
  background: var(--surface-3);
}

.side-link:hover .ico {
  color: var(--side-accent);
}

.side-link.active {
  color: var(--ink);
  background: var(--surface-3);
  border-left-color: var(--side-accent);
}

.side-link.active .ico {
  color: var(--side-accent);
}

/* Дуже низьке вікно (ноутбук з панеллю задач, відкрита консоль): панель не
   прокручується, тож замість смуги прокрутки список просто стає щільнішим. */
@media (max-height: 760px) {
  .side-link {
    min-height: 28px;
  }

  .side-group {
    margin-top: var(--s-2);
  }
}

/* Бренд у згорнутій панелі ховається цілком: на смузі в 60px він і кнопка
   згортання поруч не стоять, а сама кнопка потрібніша — без неї панель
   не розгорнути. На головну з вузької смуги веде пункт «Дашборд». */
:root[data-sidebar="collapsed"] .brand,
:root[data-sidebar="collapsed"] .side-label,
:root[data-sidebar="collapsed"] .side-group {
  display: none;
}

:root[data-sidebar="collapsed"] .sidebar-head {
  justify-content: center;
  padding-inline: 0;
}

/* Заголовків груп у вузькій панелі немає — лишаються самі іконки, і без
   розділювача вони злилися б в один суцільний стовпчик. Лінія каже те саме,
   що й напис: далі інша група. */
:root[data-sidebar="collapsed"] .side-section + .side-section {
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px solid var(--border);
}

:root[data-sidebar="collapsed"] .side-link {
  justify-content: center;
  padding-inline: 0;
}

/* У згорнутій панелі позначка активного пункту переїжджає ліворуч→праворуч:
   зліва вона зливалася б із краєм екрана, справа — межує з контентом. */
:root[data-sidebar="collapsed"] .side-link {
  border-left-width: 0;
  border-right: 3px solid transparent;
}

:root[data-sidebar="collapsed"] .side-link.active {
  border-right-color: var(--side-accent);
}

.topbar {
  grid-area: topbar;
  view-transition-name: topbar;
  /* На широкому екрані топбар і так нерухомий — це рядок сітки. sticky
     потрібен вузькому екрану, де прокручується сторінка цілком. */
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0 var(--s-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.topbar-spacer {
  flex: 1;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  justify-content: center;

  min-width: 34px;
  min-height: 34px;
  padding: 0 var(--s-2);
  font-size: var(--t-cmp);
  font-weight: 600;
  color: var(--ink-muted);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background-color 0.12s, color 0.12s, border-color 0.12s;
}

.icon-btn:hover {
  color: var(--ink);
  background: var(--surface-3);
  border-color: var(--border);
}

/* Довгий email обрізаємо трьома крапками — інакше він розпихає топбар. */
.user-name {
  max-width: 20ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.main {
  grid-area: main;

  /* min-width: 0 обов'язковий для grid-елемента: без нього широка
     таблиця всередині розпирає всю сітку замість того, щоб скролитись.
     min-height: 0 — те саме по вертикалі, щоб працював overflow нижче. */
  min-width: 0;
  min-height: 0;

  /* position: relative — не для зсуву, а щоб .main став containing block для
     своїх абсолютних нащадків. Без цього ними опікується <html>, і overflow
     нижче їх НЕ обрізає: вони лишаються в області прокрутки документа й
     тягнуть за собою другу смугу — поруч зі смугою .main.
     На кожній сторінці таких нащадків десятки: кожен <label class=
     "visually-hidden"> (position: absolute) сидить на своєму місці в потоці,
     тобто інколи за кілька екранів униз. Саме вони й розтягували <html>. */
  position: relative;

  /* Єдина область, що прокручується. Місце під смугу резервуємо завжди —
     інакше сторінки з довгим і коротким вмістом «стрибають» одна відносно
     одної; тут цей жолоб лежить усередині контенту й нікого не обрізає. */
  overflow-y: auto;
  scrollbar-gutter: stable;

  padding: var(--s-5) var(--s-5) var(--s-8);
}

.container {
  max-width: var(--w-app);
  margin: 0 auto;
}

.container.form {
  max-width: var(--w-form);
}

.container.wide {
  max-width: var(--w-wide);
}

.drawer-toggle {
  display: none;
}

.drawer-overlay {
  display: none;
}

/*
  Вузький екран. Панель перестає бути колонкою й стає шухлядою поверх
  контенту. Межа 900px мусить збігатися з тією, що в shell.js.
*/
@media (max-width: 900px) {
  .app-shell {
    /* Колонки панелі більше немає — вона тепер position: fixed. */
    grid-template-columns: 1fr;
    grid-template-areas:
      "topbar"
      "main";

    /* Прокрутка повертається сторінці: на телефоні смуга накладна, жолоба
       не займає, зате рідний скрол ховає адресний рядок і не ламається
       від динамічної висоти вікна. */
    height: auto;
    min-height: 100dvh;
  }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 40;
    width: 248px;
    /* Панель більше не комірка сітки — висоту треба задати самій. */
    height: 100dvh;
    transform: translateX(-100%);
    box-shadow: var(--e-3);
    transition: transform 0.18s ease;

    /* Тут прокрутка панелі повертається — і це не суперечить рішенню вище.
       Нерухомим полем панель має бути там, де вона стоїть поруч із контентом
       постійно; шухляда ж відкривається на весь екран телефона, де висоти
       менше за список, і обрізаний кінець списку просто був би недоступним. */
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .drawer-toggle {
    display: inline-flex;
  }

  .collapse-toggle {
    display: none;
  }

  /* Скасовуємо ефекти «згорнутості»: тут панель або схована повністю,
     або показана як шухляда — вузької смуги з іконок не буває.
     А сам стан лишається збереженим і повернеться на широкому екрані. */
  :root[data-sidebar="collapsed"] .brand,
  :root[data-sidebar="collapsed"] .side-label,
  :root[data-sidebar="collapsed"] .side-group {
    display: revert;
  }

  /* Кнопки згортання тут немає (правило вище), тож у шапці лишається
     сам бренд — вирівнюємо його по лівому краю, як у розгорнутій панелі. */
  :root[data-sidebar="collapsed"] .sidebar-head {
    justify-content: flex-start;
    padding-inline: var(--s-4) var(--s-2);
  }

  :root[data-sidebar="collapsed"] .side-link {
    justify-content: flex-start;
    padding-inline: var(--s-2);
  }

  :root[data-sidebar="collapsed"] .side-link {
    border-left-width: 3px;
    border-right-width: 0;
  }

  /* Заголовки груп тут видно (правило вище), тож розділювальна лінія між
     секціями стає другим підписом того самого — прибираємо. */
  :root[data-sidebar="collapsed"] .side-section + .side-section {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
  }

  /* Затемнення під шухлядою. Ховаємо через opacity + visibility, а не
     display: none — інакше не буде плавного згасання. visibility при
     цьому прибирає його з фокусу й кліків. */
  .drawer-overlay {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: oklch(0 0 0 / 0.45);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.18s ease, visibility 0.18s;
  }

  :root[data-drawer="open"] .sidebar {
    transform: none;
  }

  :root[data-drawer="open"] .drawer-overlay {
    opacity: 1;
    visibility: visible;
  }

  .main {
    /* Прокручується сторінка, а не ця область — власний жолоб тут зайвий. */
    overflow: visible;
    scrollbar-gutter: auto;
    padding: var(--s-4) var(--s-3) var(--s-7);
  }

  .user-name {
    display: none;
  }
}

/*
  Друк. Каркас висотою в екран із прокруткою всередині надрукував би лише
  перший екран контенту — на папері повертаємо звичайний потік. Панель і
  топбар до аркуша не мають стосунку.
*/
@media print {
  .app-shell {
    display: block;
    height: auto;
  }

  .sidebar,
  .topbar {
    display: none;
  }

  .main {
    overflow: visible;
    padding: 0;
  }
}

/*
  Сторінки входу і 403. Власний каркас: ні панелі, ні топбару там немає.
  На вузькому екрані лишається сама картка, брендова половина ховається.
*/

.auth-body {
  display: grid;
  min-height: 100vh;
}

@media (min-width: 860px) {
  .auth-body {
    grid-template-columns: 1fr 1fr;
  }
}

.auth-brand {
  display: none;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--s-7) var(--s-6);
  background: linear-gradient(
    160deg,
    color-mix(in oklch, var(--brand) 22%, var(--surface)),
    color-mix(in oklch, var(--brand) 6%, var(--bg))
  );
  border-right: 1px solid var(--border);
}

@media (min-width: 860px) {
  .auth-brand {
    display: flex;
  }
}

.auth-mark {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-xl);
  font-weight: 700;
  color: var(--ink);
}

.auth-mark .ico {
  color: var(--brand);
}

.auth-tagline {
  max-width: 34ch;
  margin-top: var(--s-3);
  color: var(--ink-muted);
}

.auth-foot {
  font-size: var(--t-sm);
  color: var(--ink-subtle);
}

.auth-panel {
  display: grid;
  place-items: center;
  padding: var(--s-5) var(--s-4);
}

.auth-card {
  width: 100%;
  max-width: 380px;
}

.auth-card .auth-mark {
  margin-bottom: var(--s-5);
}

/* Логотип у картці потрібен, лише поки брендова половина схована —
   інакше він дублювався б на екрані двічі. */
@media (min-width: 860px) {
  .auth-card .auth-mark {
    display: none;
  }
}

}

@layer components {
/* 40-components/actions.css */
/*
  Ряди дій — єдина форма для всіх кнопкових груп інтерфейсу.

  Досі кожен сервіс описував свій варіант того самого: .cost-actions,
  .pc-actions, .cost-source-actions — усі три означали «кнопки в рядок із
  проміжком». Тепер це один клас, і кнопки під таблицею в «Костах»
  відбиваються так само, як під карткою в «Доменах».

    .actions         — кнопки в рядок (під таблицею, під карткою);
    .actions.stacked — те саме колонкою, коли в комірці таблиці немає ширини;
    .page-actions    — підвал сторінки: переходи в сусідні розділи.
*/
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
}

/*
  Колонкою — коли в комірці таблиці немає ширини на рядок. Розтягуються
  саме форми: усередині них буває поле («новий пароль»), і воно має зайняти
  комірку. Гола кнопка лишається завширшки зі свій підпис — розтягнута на
  всю комірку, вона читалась би як заголовок рядка, а не як дія.
*/
.actions.stacked {
  flex-direction: column;
  align-items: stretch;
}

.actions.stacked > .btn {
  align-self: flex-start;
}

/*
  Дія, що змінює стан, мусить бути POST, тобто жити всередині <form>. Щоб
  ця форма не ламала рядок, вона сама стає флекс-контейнером зі своєю
  однією кнопкою. display: contents тут не годиться: у частині браузерів
  він викидає <form> із дерева доступності.
*/
.actions > form {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
}

/* Кнопка в ряду не «завершує форму», тож автоматичний відступ згори
   (button.css) їй не потрібен. */
.actions > .btn,
.actions > form > .btn {
  margin-top: 0;
}

/*
  Підвал сторінки: переходи, які стосуються розділу цілком, — архів,
  довідники, повернення до списку.

  Раніше на їх місці стояло текстове посилання в <p class="hint">. Воно
  губилось під карткою й нічим не показувало, що це дія, а не примітка до
  форми вище; клікабельна зона в нього була завширшки з самі літери.

  Лінія згори завершує сторінку: під нею вже нічого немає.
*/
.page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}

.page-actions .btn {
  margin-top: 0;
}

}
@layer components {
/* 40-components/alert.css */
/* Банер повідомлення. Без модифікатора — нейтральний;
   .ok / .warn / .error / .info беруть готові трійки токенів стану. */
.alert {
  display: block;
  margin: var(--s-4) 0;
  padding: var(--s-3) var(--s-4);
  font-size: var(--t-md);
  border: 1px solid var(--border);
  border-radius: var(--r-md);

  color: var(--ink);
  background: var(--surface-2);
}

.alert.ok {
  color: var(--ok-ink);
  background: var(--ok-bg);
  border-color: var(--ok-line);
}

.alert.warn {
  color: var(--warn-ink);
  background: var(--warn-bg);
  border-color: var(--warn-line);
}

.alert.error {
  color: var(--fail-ink);
  background: var(--fail-bg);
  border-color: var(--fail-line);
}

.alert.info {
  color: var(--info-ink);
  background: var(--info-bg);
  border-color: var(--info-line);
}

/* Код усередині банера підмішується до його ж кольору (currentColor),
   а не бере власний фон — інакше він випадав би з кольорової плями. */
.alert code {
  background: color-mix(in oklch, currentColor 12%, transparent);
  color: inherit;
}

.alert .ico {
  margin-right: var(--s-1);
}

/*
  Банер, який починається з іконки. Доти він лишався блоком, і будь-який
  БЛОКОВИЙ вміст після іконки (абзац, список, обгортка <div>) починався з
  нового рядка — іконка висіла сама над текстом. Саме так виглядали
  зауваження прогону в «Розподілі костів» і застереження про читання файлу.

  Дві колонки замість flex: у flex-контейнері сусідні блокові діти стали б
  окремими колонками, а тут другий рядок сітки просто йде під текст. Банер
  без іконки правило не зачіпає — :has() його не вибере, і банери з
  <details> усередині лишаються звичайним блоком.
*/
.alert:has(> .ico:first-child) {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--s-2);
}

/* Проміжок дає gap; власний відступ іконки в сітці був би подвійним. */
.alert:has(> .ico:first-child) > .ico {
  margin-right: 0;
  /* Оптичне вирівнювання по першому рядку тексту, а не по його коробці. */
  margin-top: 0.15em;
}

}
@layer components {
/* 40-components/alerts.css */
/*
  Блок «Потребує уваги» на дашборді: стани, які просять дії просто зараз.

  Файл раніше називався toast.css і містив удвічі більше — компонент тоста,
  який сам вигулькував у правому нижньому куті на КОЖНІЙ сторінці CRM. Тоста
  більше немає, і причина не в тому, що він заважав.

  З появою дзвіночка в топбарі він став другим каналом для тієї самої
  інформації, а два канали з однаковим вмістом гірші за один: людина вчиться
  не помічати обидва, бо кожен щоразу виявляється тим, який вона вже бачила.
  Разом із тостом пішли й усі його механіки — липкість, дедуплікація по ключу,
  «прибрати на кілька годин» у localStorage: вони існували рівно для того, щоб
  канал, який приходить сам, не перетворився на шум. Каналу, по який приходять
  самі, вони не потрібні.

  Що лишилось — панель на дашборді. Вона нічого не вимагає й нікуди не
  вигулькує: людина відкриває головну й бачить, що зараз варте уваги.
*/
.alerts-panel {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  gap: var(--s-3);
}

.alerts-item {
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border: 1px solid var(--border);
  /* Стан несе ліва грань, а не заливка: п'ять залитих карток поспіль читаються
     як одна пляма, у якій нічого не виділено. Кольори — наявні токени станів,
     власних компонент не заводить. */
  border-left: 3px solid var(--border-strong);
  border-radius: var(--r-md);
}

.alerts-item.warn  { border-left-color: var(--warn-ink); }
.alerts-item.error { border-left-color: var(--fail-ink); }
.alerts-item.ok    { border-left-color: var(--ok-ink); }
.alerts-item.info  { border-left-color: var(--info-ink); }

.alerts-item .name { font-weight: 600; }
.alerts-item .name a { color: inherit; text-decoration: none; }
.alerts-item .name a:hover { text-decoration: underline; }

.alerts-item .advice {
  margin: var(--s-1) 0 0;
  font-size: var(--t-cmp);
  color: var(--ink-muted);
}

/* Пасивний слід: «Дмитро відкривав картку 2 хв тому». Кнопок тут немає
   навмисно — підказка має коштувати нуль уваги. */
.alerts-item .trace {
  margin: var(--s-1) 0 0;
  font-size: var(--t-sm);
  color: var(--ink-subtle);
}

}
@layer components {
/* 40-components/badge.css */
/* Компактна мітка статусу. Ті самі модифікатори, що й у .alert,
   плюс .running — з живою крапкою, яка пульсує. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 0.15em var(--s-2);
  font-size: var(--t-xs);
  font-weight: 600;
  line-height: 1.6;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--ink-muted);
  background: var(--surface-3);
  border: 1px solid transparent;
  border-radius: var(--r-full);
}

.badge.ok {
  color: var(--ok-ink);
  background: var(--ok-bg);
  border-color: var(--ok-line);
}

.badge.warn {
  color: var(--warn-ink);
  background: var(--warn-bg);
  border-color: var(--warn-line);
}

.badge.error {
  color: var(--fail-ink);
  background: var(--fail-bg);
  border-color: var(--fail-line);
}

.badge.info {
  color: var(--info-ink);
  background: var(--info-bg);
  border-color: var(--info-line);
}

.badge.running {
  color: var(--info-ink);
  background: var(--info-bg);
  border-color: var(--info-line);
}

/* Крапка малюється псевдоелементом — не потребує зайвого тега в розмітці.
   Розмір в em, тож масштабується разом із текстом бейджа. */
.badge.running::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentColor;
  animation: badge-pulse 1.4s ease-in-out infinite;
}

@keyframes badge-pulse {
  50% {
    opacity: 0.25;
  }
}

}
@layer components {
/* 40-components/breadcrumb.css */
/* Хлібні крихти на внутрішніх сторінках сервісів. */
.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-1);
  margin-bottom: var(--s-3);
  font-size: var(--t-sm);
  color: var(--ink-muted);
}

.breadcrumb a {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  color: var(--ink-muted);
  text-decoration: none;
  border-radius: var(--r-sm);
}

.breadcrumb a:hover {
  color: var(--brand);
  text-decoration: underline;
}

/* Роздільник — псевдоелементом, і лише між пунктами: у розмітці його
   немає, тож він не потрапляє ні в копіювання, ні в скрінрідер. */
.breadcrumb li + li::before {
  content: "/";
  margin-right: var(--s-1);
  color: var(--ink-subtle);
}

/* display: contents прибирає сам <ol> із розкладки, лишаючи його
   семантику: пункти стають прямими дітьми флекс-контейнера. */
.breadcrumb ol {
  display: contents;
  margin: 0;
  padding: 0;
  list-style: none;
}

.breadcrumb li {
  display: inline-flex;
  align-items: center;
}

.breadcrumb [aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
}

}
@layer components {
/* 40-components/button.css */
/*
  Кнопка. Один клас .btn — основна дія; варіанти: .secondary, .ghost,
  .ok, .danger, розмір — .small. Клас .loading показує спінер (див. низ файлу).
  Однаково працює і на <button>, і на <a> — звідси text-decoration: none.
*/
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);

  min-height: 38px;
  padding: var(--s-2) var(--s-5);
  font-size: var(--t-md);
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--brand-ink);
  background: var(--brand);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  text-decoration: none;
  transition: background-color 0.12s, border-color 0.12s, color 0.12s;
}

/*
  Кнопка, що завершує форму й стоїть просто в ній, без обгортки, — типовий
  випадок у простих формах сервісів. Відступ від останнього поля той самий,
  що між блоками сторінки (50-rhythm.css), тож форма не випадає з ритму.

  Раніше цей margin-top стояв на самій .btn, і його доводилось гасити
  всюди, де кнопка стоїть у ряд з іншим: у .pager, .form-actions,
  .modal-footer, у комірках таблиць, у формах-рядках кожного сервіса. Саме
  звідти й бралася більшість різнобою у відступах. Тепер відступ
  з'являється рівно там, де він потрібен.

  Групи кнопок (.actions, .form-actions, .form-row) і компактний розмір
  (.btn.small) гасять його одним класом — жодного правила «на місці».
*/
form > .btn {
  margin-top: var(--s-5);
}

.btn:hover {
  background: var(--brand-hover);
}

.btn:active {
  background: var(--brand-active);
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn.loading[disabled] {
  cursor: progress;
}

.btn.secondary {
  color: var(--ink);
  background: var(--surface);
  border-color: var(--border-strong);
}

.btn.secondary:hover {
  background: var(--surface-2);
  border-color: var(--ink-subtle);
}

.btn.ghost {
  color: var(--ink-muted);
  background: none;
  border-color: transparent;
}

.btn.ghost:hover {
  color: var(--ink);
  background: var(--surface-3);
}

/*
  Дія з протилежним знаком до .danger: «видати», «підтвердити», «увімкнути».
  Форма навмисно та сама (тінт, а не суцільна заливка): ці дві кнопки майже
  завжди стоять парою, і заливка поруч із тінтом читалась би як різна вага
  дій, хоча вага в них однакова — обидві незворотні, просто в різні боки.

  Не заміна основній .btn: та лишається кольором акценту сервісу для дії, у
  якої пари немає.
*/
.btn.ok {
  color: var(--ok-ink);
  background: var(--ok-bg);
  border-color: var(--ok-line);
}

.btn.ok:hover {
  background: color-mix(in oklch, var(--ok-ink) 20%, var(--surface));
}

.btn.danger {
  color: var(--fail-ink);
  background: var(--fail-bg);
  border-color: var(--fail-line);
}

.btn.danger:hover {
  background: color-mix(in oklch, var(--fail-ink) 20%, var(--surface));
}

/* Компактний розмір — це кнопка в комірці таблиці або в рядку картки.
   Там вона ніколи не «завершує форму», тож власних відступів не має:
   проміжки задає ряд, у якому вона стоїть (.actions, .form-row). */
.btn.small {
  min-height: 28px;
  padding: var(--s-1) var(--s-3);
  font-size: var(--t-sm);
  margin: 0;
}

/* Спінер лежить у розмітці завжди, але прихований. Показує його клас
   .loading на кнопці — так ширина не стрибає в момент появи.
   currentColor + em: підлаштовується під будь-який варіант кнопки. */
.spinner {
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  /* Прозорий бік і дає ефект «дуги», що крутиться. */
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  display: none;
}

.btn.loading .spinner {
  display: inline-block;
}

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

}
@layer components {
/* 40-components/card.css */
/* Картка — основна «коробка» контенту сторінки. */
.card {
  padding: var(--s-6) var(--s-6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--e-1);
}

/*
  Компактна картка — та, що стоїть не однією на секцію, а рядком чи сіткою:
  плитки підсумків місяця, рядки дошки оплат, записи дня, підказка про
  непорахований місяць. Падінг картки-секції в такому ряду виглядає
  порожнім, тож він на крок менший.

  Чотири сервіси дійшли до цих самих значень незалежно й тримали їх у
  власних стилях — .cost-suggest, .cost-sum-card, .fin-card, .nt-row. Це
  той випадок, коли варіант мав бути в компоненті, а не в кожному з них.
*/
.card.compact {
  padding: var(--s-4) var(--s-5);
}

/*
  Краї картки. Заголовок чи абзац, що стоїть у ній першим або останнім, не
  додає власного відступу — його вже дає падінг самої картки.

  Без цього картка, яка починається з <h2>, з'їжджала вниз на подвійний
  відступ (32px падінгу плюс 32px власного margin заголовка), а картка, що
  починається з тексту, — ні. Дві сусідні картки з однаковим падінгом
  виглядали по-різному, і причина не була видна ні в розмітці, ні в стилях.
*/
.card > :first-child {
  margin-top: 0;
}

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

/* Шапка сторінки. Смужка ліворуч — той самий акцент, що й у навбарі,
   тобто колір поточного сервісу (--accent ← --h-accent з <body>). */
.page-head {
  margin-bottom: var(--s-5);
  padding-left: var(--s-3);
  border-left: 3px solid var(--accent);
}

/* Флекс, а не звичайний рядок: у заголовку поруч із назвою стоїть іконка
   сервісу, а подекуди й бейдж стану («чернетка» в картці місяця). */
.page-head h1 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  font-size: var(--t-xl);
}

/* Іконка заголовка — тим самим кольором, що й смужка ліворуч. Разом вони
   читаються як «це сторінка такого-то сервіса», ще до самої назви. */
.page-head h1 .ico {
  color: var(--accent);
}

.page-head p {
  margin: var(--s-1) 0 0;
  font-size: var(--t-md);
  color: var(--ink-muted);
}

/*
  Заголовок секції з дією праворуч: «Сервіси» з кнопкою налаштувань
  дашборду, «Прогрес» із бейджем стану, «Рядків: N» із кнопкою оновлення.
  Раніше це були два різні класи з однаковим змістом — .section-head і
  .console-head; лишився один.

  Відступи ті самі, що в голого h2 (20-base.css): заголовок у рядку з
  кнопкою не має з'їжджати відносно решти. Дія відтісняється до правого
  краю — тоді ряд заголовків на сторінці читається однією колонкою,
  а дії — другою.
*/
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin: var(--s-6) 0 var(--s-3);
}

.section-head > h2,
.section-head > h3 {
  margin: 0;
}

.section-head .btn {
  margin-top: 0;
}

}
@layer components {
/* 40-components/chips.css */
/*
  Поле-чіпи (списки баєрів, сабів і кампаній у Keitaro). Розмітку будує
  shared/static/js/chips.js за атрибутом [data-chips]; тут — лише вигляд.

  .chips-field — обгортка з розмітки, .chips — коробка, яку створює JS.
  Тому без скрипта видно звичайний input, і форма лишається робочою.
*/
.chips-field {
  margin-top: var(--s-1);
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-2);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  cursor: text;
  transition: border-color 0.12s, box-shadow 0.12s;
}

.chips:hover {
  border-color: var(--ink-subtle);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  max-width: 100%;
  padding: 0.15em var(--s-1) 0.15em var(--s-2);
  font-size: var(--t-cmp);
  font-weight: 500;
  color: var(--ink);
  background: var(--surface-3);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
}

.chip span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chip button {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  width: 20px;
  height: 20px;
  padding: 0;
  color: var(--ink-muted);
  background: none;
  border: 0;
  border-radius: var(--r-full);
  line-height: 1;
  cursor: pointer;
}

.chip button:hover {
  color: var(--fail-ink);
  background: var(--fail-bg);
}

/* Поле вводу тягнеться на вільне місце, але не вужче 8ch — інакше після
   кількох чіпів у ньому не лишалося б місця набирати. */
.chips input.chip-entry {
  flex: 1 1 8ch;
  min-width: 8ch;
  width: auto;
  padding: var(--s-1) var(--s-1);
  color: var(--ink);
  background: none;
  border: 0;
  font: inherit;
  font-size: var(--t-md);
}

/* Фокус із самого input знімаємо — але не втрачаємо: скрипт вішає клас
   .focus на коробку, і рамку малює вже вона (див. form.css). Інакше
   всередині поля світилася б друга рамка. */
.chips input.chip-entry:focus,
.chips input.chip-entry:focus-visible {
  outline: none;
  box-shadow: none;
  border: 0;
}

/*
  Стан чіпа після перевірки в довіднику (data-suggest у chips.js). Колір —
  не єдиний носій сенсу: причина лежить у title чіпа, а повний розбір усе
  одно приходить із прев'ю. Без підказок стану немає, і чіп лишається
  звичайним — саме так виглядає поле в «Створенні TG-груп».
*/
.chip.is-pending {
  opacity: .6;
}

.chip.is-one {
  color: var(--ok-ink);
  background: var(--ok-bg);
  border-color: var(--ok-line);
}

/* Колір тексту тут не прикраса: тло станів домішує колір лише на 12%, і на
   темній темі жовтий «двозначно» без нього не відрізнити від звичайного
   чіпа. Трійка ink + bg + line — та сама, що в .alert і .badge. */
.chip.is-many {
  color: var(--warn-ink);
  background: var(--warn-bg);
  border-color: var(--warn-line);
}

.chip.is-none {
  color: var(--fail-ink);
  background: var(--fail-bg);
  border-color: var(--fail-line);
}

/*
  Випадайка з варіантами. Вигляд навмисно той самий, що в .sel-panel
  (select.css): для людини це один і той самий «список під полем», і два
  різні кути чи тіні читались би як два різні інтерфейси. Класи все ж свої —
  структуру .sel-* будує select.js під власний <select>, і спільний клас
  зв'язав би два компоненти, які не мають нічого спільного всередині.

  У <body>, а не поруч із полем: усередині картки з overflow панель
  обрізалася б. Тому position: fixed і координати рахує скрипт.

  z-index 50 — той самий поверх, що й у календаря та випадайки.
*/
.chips-menu {
  position: fixed;
  z-index: 50;
  max-height: 20rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-1);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--e-3);
}

.chips-option {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 32px;
  padding: var(--s-1) var(--s-2);
  font-size: var(--t-cmp);
  color: var(--ink);
  border-radius: var(--r-sm);
  cursor: pointer;
}

/* Підсвітка одна на мишу й на клавіатуру — як у .sel-option: інакше видимих
   «поточних» пунктів стає два. */
.chips-option.active,
.chips-option:hover {
  background: var(--surface-3);
}

.chips-option-label {
  flex: 1;
  min-width: 0;
}

/* Дрібне праворуч: id, група, пошта. Не переносимо — рядок має лишатись
   одним, інакше список стрибає висотою при наборі. */
.chips-option-hint {
  flex: none;
  max-width: 45%;
  overflow: hidden;
  font-size: var(--t-xs);
  color: var(--ink-muted);
  text-overflow: ellipsis;
  white-space: nowrap;
}

}
@layer components {
/* 40-components/collapse.css */
/*
  Згортання великих блоків.

  <details> уміє головне сам, без жодного рядка JS, — скрипт поруч
  (static/js/collapse.js) лише запам'ятовує стан між сторінками. Тому блок,
  згорнутий один раз, лишається згорнутим і після редіректу з POST, і завтра.

  Велике й ЗАЗДАЛЕГІДЬ ВІДОМЕ велике (сотні рядків) шаблон має віддавати вже
  згорнутим — атрибутом open, якого немає. Розгортати скриптом після
  завантаження не можна: сторінка на чотириста рядків встигала б смикнутись
  людині в очі, і саме цього смикання ми й позбуваємось.

  Клас навішується на <details> і НЕ замінює .card — вони поєднуються:
  <details class="card collapse"> дає згортання всередині коробки.
*/
.collapse > summary {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  cursor: pointer;
  /* Стандартний трикутник прибираємо: замість нього шеврон зі спрайта, той
     самий, яким позначені розкривні блоки в решті інтерфейсу. list-style
     вимикає його у Firefox, ::-webkit-details-marker — у решті. */
  list-style: none;
  user-select: none;
}

.collapse > summary::-webkit-details-marker {
  display: none;
}

.collapse > summary:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-sm);
}

/* Шеврон — єдине, що показує стан, тож повертається саме він. */
.collapse > summary .ico {
  color: var(--ink-muted);
  transition: transform 120ms ease;
}

.collapse[open] > summary .ico {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .collapse > summary .ico {
    transition: none;
  }
}

/* Заголовок займає рядок цілком — клікабельна вся смуга, а не самі слова. */
.collapse-title {
  font-weight: 600;
}

/*
  Справжній заголовок усередині summary. Рівень лишається справжнім <h2>, щоб
  навігація по заголовках у скрінрідері не ламалась через те, що секцію
  зробили розкривною; від flex-рядка йому потрібно лише зняти власні відступи.
*/
.collapse > summary > h2,
.collapse > summary > h3 {
  margin: 0;
  font-size: var(--t-lg);
}

/*
  Лічильник праворуч. Він тут не для краси: згорнутий блок мусить сам казати,
  скільки в ньому всього, — інакше єдиний спосіб це дізнатись — розгорнути,
  тобто рівно те, від чого людина згортала.
*/
.collapse-count {
  margin-left: auto;
  font-size: var(--t-sm);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Відступ з'являється лише коли є від чого відступати. */
.collapse[open] > summary {
  margin-bottom: var(--s-3);
}

}
@layer components {
/* 40-components/console.css */
/*
  .log-panel — панель статичного логу: живий лог в адмінці та деталі
  запуску в історії.

  Тут був ще .console із розфарбуванням рядків за рівнем (.lvl-*) — під
  живий стрім AutoFlow. Стрім лишився й далі веде прогрес-бар, але рядки
  більше не малюються, тож і стилі до них пішли.

  Рядок заголовка над панеллю (.console-head) теж пішов: він робив те саме,
  що .section-head у card.css, — заголовок ліворуч, дія праворуч. Два класи
  на одне й те саме розходились у відступах, тож лишився один.
*/
/*
  Сама коробка й прокрутка — спільні .panel.scroll: до появи того компонента
  ця панель описувала їх у себе, і її 340px були одним із чотирьох різних
  значень висоти в інтерфейсі. Тут лишилось те, що властиве саме логу, —
  моноширинний шрифт і поводження з довгими рядками.

  У розмітці клас іде поруч: class="log-panel panel scroll".
*/
.log-panel {
  font-family: var(--font-mono);
  font-size: var(--t-cmp);
  line-height: 1.5;
  /* pre-wrap зберігає переноси з логу, але не дає довгому URL вилізти
     за межі — його добиває break-word. */
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--ink);
}

}
@layer components {
/* 40-components/datepicker.css */
/*
  Календар для полів дати (shared/static/js/datepicker.js).

  Власний віджет замість <input type="datetime-local">: рідний малюється за
  локаллю браузера, а не за мовою сторінки, і давав американський порядок
  та AM/PM. Тут усе — токенами, тож панель перемикається разом із темою сама.

  Стилюємо ТІЛЬКИ панель. Саме поле лишається звичайним .field input і
  виглядає як усі інші — календар є надбудовою, а не окремим типом контролу.
*/

.dp-wrap {
  position: relative;
  display: block;
}

.dp {
  position: absolute;
  /* 50 — між панеллю (40) і тостом (60): календар має накривати каркас
     сторінки, але не перекривати сповіщення.
     Тут раніше стояло var(--e-3), а це токен ТІНІ, а не число: правило
     виходило недійсним і відкидалося, тобто панель лишалась без z-index
     узагалі. Симетрична плутанина була й у box-shadow нижче. */
  z-index: 50;
  top: calc(100% + var(--s-1));
  left: 0;
  min-width: 17rem;
  padding: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  /* --e-3, а не --shadow-2: другий — це лише КОЛІР тіні, а box-shadow без
     зсувів недійсний, тож тіні в панелі не було зовсім. */
  box-shadow: var(--e-3);
}

/* Поле в кінці сторінки — панель відкривається вгору. */
.dp.dp-up {
  top: auto;
  bottom: calc(100% + var(--s-1));
}

.dp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: var(--s-2);
}

.dp-title {
  font-weight: 600;
  font-size: var(--t-cmp);
}

.dp-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  font-size: var(--t-lg);
  line-height: 1;
  color: var(--ink-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: pointer;
}

.dp-nav:hover {
  color: var(--ink);
  background: var(--surface-2);
}

.dp-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}

/* Понеділок першим — європейський тиждень. Порядок задає JS, тут лише вигляд. */
.dp-week span {
  padding: var(--s-1) 0;
  font-size: var(--t-xs);
  text-align: center;
  color: var(--ink-subtle);
}

.dp-day {
  padding: var(--s-1) 0;
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.dp-day:hover {
  background: var(--surface-2);
}

/* Сьогодні — рамкою, обране — заливкою: два різні стани мають читатись
   одночасно, бо обраним часто і є сьогодні. */
.dp-day.is-today {
  border-color: var(--accent-line);
  color: var(--accent);
}

.dp-day.is-selected {
  color: var(--brand-ink);
  background: var(--brand);
  border-color: var(--brand);
  font-weight: 600;
}

.dp-time {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--border);
}

.dp-time-label {
  font-size: var(--t-sm);
  color: var(--ink-subtle);
}

/* 24 години без AM/PM — список і є всім поясненням формату. */
.dp-time select {
  padding: var(--s-1) var(--s-2);
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

.dp-colon {
  color: var(--ink-subtle);
}

.dp-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  margin-top: var(--s-3);
}

@media (max-width: 480px) {
  .dp {
    min-width: 0;
    width: 100%;
  }
}

}
@layer components {
/* 40-components/empty.css */
/*
  Порожні стани. .placeholder-panel — простий рядок «нічого немає»,
  .empty-state — розгорнутий варіант з іконкою, поясненням і дією.
  Пунктирна рамка навмисно: одразу читається як «тут поки порожньо».
*/
.empty-state,
.placeholder-panel {
  padding: var(--s-6) var(--s-4);
  text-align: center;
  color: var(--ink-muted);
  background: var(--surface-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
}

.placeholder-panel {
  display: block;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
}

.empty-state .ico,
.placeholder-panel .ico {
  color: var(--ink-subtle);
}

.empty-state-title {
  font-weight: 600;
  color: var(--ink);
}

.empty-state-hint {
  max-width: 46ch;
  font-size: var(--t-sm);
}

/* Дія відбивається від пояснення трохи більше, ніж рядки самого блока —
   інакше вона читається як його третій рядок, а не як кнопка. */
.empty-state .btn,
.placeholder-panel .btn {
  margin-top: var(--s-3);
}

}
@layer components {
/* 40-components/filedrop.css */
/*
  Зона вибору файлу — єдиний вигляд файлового поля в CRM (UI.md, §3).
  Рідний <input type="file"> малює кнопку операційної системи з її ж мовою
  («Выберите файл») і не піддається стилям, тож він схований, а клікабельність
  дає <label>. Вибір працює й без скрипта: клік по підпису відкриває діалог
  сам браузер. Поведінку (перетягування, ім'я файлу) додає
  shared/static/js/filedrop.js.

  .dragover — файл тягнуть над зоною, .has-file — файл уже обрано.
  Обидва змінюють рамку з пунктирної на суцільну: «порожньо» → «є».
*/
.filedrop {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-1);
  margin-top: var(--s-1);
  padding: var(--s-5) var(--s-4);
  text-align: center;
  font-weight: 500;
  color: var(--ink-muted);
  background: var(--surface-2);
  border: 2px dashed var(--border-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 0.12s, background-color 0.12s, color 0.12s;
}

/*
  Саме поле ховаємо ЗВІДСИ, а не атрибутом hidden у розмітці: hidden виносить
  контрол із порядку обходу, тобто до вибору файлу неможливо дійти з
  клавіатури — а саме так із формою працює частина людей. Тут поле лишається
  сфокусованим, просто невидимим, і фокус на ньому підсвічує всю зону.
*/
.filedrop input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  border: 0;
}

.filedrop:hover {
  color: var(--ink);
  border-color: var(--brand-line);
}

.filedrop:focus-within {
  color: var(--ink);
  border-color: var(--brand);
  border-style: solid;
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.filedrop.dragover {
  color: var(--brand);
  background: var(--brand-soft);
  border-color: var(--brand);
  border-style: solid;
}

.filedrop.has-file {
  color: var(--ok-ink);
  background: var(--ok-bg);
  border-color: var(--ok-line);
  border-style: solid;
}

}
@layer components {
/* 40-components/form.css */
/*
  Поля форм. Стилізуємо теги напряму, без класів: розмітка сервісів
  лишається простою — <input>, <label>, <textarea> і все.

  Довгий ланцюжок :not() виключає ті типи, що мають власний вигляд:
  чекбокси й радіо (нижче), файлові поля (.filedrop) і кнопки (.btn).

  Виключення загорнуті в :where() НАВМИСНО, і це не косметика. :not() бере
  вагу свого аргументу, тож п'ять [type="..."] давали цьому правилу
  специфічність (0,5,1) — більше, ніж у будь-якого компонента, який захоче
  перевизначити вигляд поля. Саме через це не працював `border: 0` у
  .chips input.chip-entry (0,2,1): поле-чіпи малювало другу рамку всередині
  своєї коробки, попри прямий намір у chips.css.

  :where() не додає ваги взагалі, тож правило важить (0,0,1) — як звичайний
  селектор за тегом, яким воно й задумане. Набір елементів не змінився.
  Сусідні правила цього файлу (:hover, :focus-visible, :disabled) важать
  (0,2,1) і перекривають базове, як і раніше.
*/
input:not(:where([type="checkbox"], [type="radio"], [type="file"], [type="submit"], [type="button"])),
textarea,
select {
  width: 100%;
  padding: var(--s-2) var(--s-3);
  font: inherit;
  font-size: var(--t-md);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  transition: border-color 0.12s, box-shadow 0.12s;
}

textarea {
  /* field-sizing: content — поле росте під вміст (де браузер це вміє).
     min-height у lh — «не менше чотирьох рядків», хай який шрифт. */
  field-sizing: content;
  min-height: 4lh;
  /* Тільки по вертикалі: горизонтальне розтягування ламало б сітку. */
  resize: vertical;
}

select {
  width: auto;
  min-width: 12ch;
}

input:hover:not(:disabled),
textarea:hover:not(:disabled),
select:hover:not(:disabled) {
  border-color: var(--ink-subtle);
}

/* У полів свій фокус замість загального outline: рамка з підсвіткою
   краще лягає на прямокутник поля. .chips.focus підмішаний сюди ж, щоб
   поле-чіпи виглядало так само, як звичайне. */
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
.chips.focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

input:disabled,
textarea:disabled,
select:disabled {
  color: var(--ink-subtle);
  background: var(--surface-2);
  cursor: not-allowed;
}

/* :user-invalid, а не :invalid — підсвічуємо помилку лише після того, як
   людина справді щось ввела. Інакше порожня форма червоніла б одразу. */
input:user-invalid,
textarea:user-invalid,
select:user-invalid {
  border-color: var(--fail-line);
}

/* Поле з кнопкою збоку — наприклад, «показати пароль» на сторінці входу. */
.input-affix {
  display: flex;
  align-items: center;
  gap: var(--s-1);
}

.input-affix > input {
  flex: 1;
  min-width: 0;
}

.input-affix > .icon-btn {
  flex: none;
  min-height: 38px;
  border-color: var(--border);
}

.hint {
  display: block;
  margin-top: var(--s-1);
  font-size: var(--t-sm);
  font-weight: 400;
  color: var(--ink-muted);
}

.field {
  margin-block: var(--s-4);
}

.field > label {
  margin-top: 0;
}

/*
  Перший підпис форми власного відступу згори не має: його вже дав падінг
  картки або заголовок над формою. Інакше форма в картці починалась на
  48px від краю, а така сама форма під заголовком — на 16px.

  Прихованих полів правило не помічає: вони не малюються, але :first-child
  собою займають — звідси другий селектор.
*/
form > label:first-child,
form > input[type="hidden"] + label {
  margin-top: 0;
}

/* auto-fit + minmax: кількість колонок підбирається сама під ширину,
   без жодного медіа-запиту. Поле ніколи не вужче за 240px. */
.form-grid {
  display: grid;
  gap: 0 var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-5);
}

.form-actions .btn {
  margin-top: 0;
}

/*
  Форма в один рядок: поля й кнопка поруч. Вирівнювання по нижньому краю —
  щоб кнопка стала на лінію самих полів, а не підписів над ними.

  Цей клас замінив три однакові описи в сервісах: .fin-log у «Фінансах»,
  .cost-build у «Костах», .pc-search у «Доменах». Усі троє робили те саме,
  але з різними проміжками — і форми, що виглядають однаково, поводились
  по-різному.
*/
.form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s-2);
}

/* Поле в рядку відступів не має — проміжки задає сам ряд. */
.form-row .field {
  margin: 0;
}

.form-row .btn {
  margin-top: 0;
}

/* Поле без обгортки тягнеться на вільне місце. Вужчу ширину задає вже
   сервіс — там, де відомо, що в полі лежить сума або рік. */
.form-row > input,
.form-row > select {
  flex: 1;
  min-width: 0;
}

/* Те саме для поля в обгортці .field: воно забирає вільне місце рядка, а
   решта (кнопки, вузькі поля) лишаються при своєму розмірі. Клас, а не
   інлайновий style="flex: 1; min-width: 240px" у шаблоні — саме так це
   писалося доти, і ширина в кожному місці була своя. */
.form-row > .field.grow {
  flex: 1;
  min-width: 14rem;
}

/*
  Група полів під спільним підписом — набір радіо чи галочок, які читаються
  як одна відповідь («Хто бачить цей запис», «Ролі»).

  Саме <fieldset> з <legend>, а не <label> над блоком: підпис такої групи не
  вказує на жоден окремий контрол, і звичайний label лишав би її без назви
  для тих, хто читає форму з екрана. Браузерні рамка й падінги скинуті —
  вигляд групі дає сама сторінка.

  Цей самий скид тричі лежав у сервісах під власними іменами (двічі в
  «Календарі», з коментарем «дублюємо свідомо», і ще раз у вигляді голого
  <label> в адмінці).
*/
.fieldset {
  /* Без min-width fieldset відмовляється стискатись у flex- і grid-сітці. */
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

/*
  Підпис групи виглядає рівно як підпис поля (див. label у 20-base.css) — це
  той самий рівень форми, і різний вигляд означав би різний зміст. Доти
  «Хто бачить цей запис» був дрібним і сірим, а «Ролі» поруч у сусідній
  формі — звичайним підписом поля.
*/
.fieldset > legend {
  padding: 0;
  margin-bottom: var(--s-1);
  font-weight: 600;
  font-size: var(--t-md);
}

/* Варіанти в рядок — коли їх два-чотири й вони коротші за рядок. Довші
   набори лишаються в .checkbox-grid, яка сама добирає колонки. */
.fieldset.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
}

/* Сітка чекбоксів — списки прав і ролей в адмінці. */
.checkbox-grid {
  display: grid;
  gap: var(--s-1) var(--s-4);
  margin: var(--s-2) 0 var(--s-4);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* .compact — для ролей просто в комірці таблиці, де місця значно менше. */
.checkbox-grid.compact {
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  margin: var(--s-1) 0 var(--s-2);
  font-size: var(--t-cmp);
}

/* Скидаємо жирність і відступи, які label отримує в 20-base.css:
   там він — підпис поля, а тут просто текст біля галочки. */
label.checkbox {
  display: flex;
  /* baseline, а не center: підпис буває у два рядки, і галочка має
     стояти на рівні першого. */
  align-items: baseline;
  gap: var(--s-2);
  margin: 0;
  font-weight: 400;
  cursor: pointer;
}

label.checkbox:hover {
  color: var(--brand);
}

label.checkbox input {
  width: auto;
  margin: 0;

  accent-color: var(--brand);
}

label.checkbox code {
  font-size: var(--t-xs);
  color: var(--ink-muted);
  background: none;
  padding: 0;
}

}
@layer components {
/* 40-components/icon.css */
/*
  Іконки зі спрайта (shared/static/icons.svg), вставляє глобал icon().
  Розмір в em — іконка масштабується разом із текстом поруч.
  Колір не задаємо: SVG використовує currentColor і фарбується від тексту.
*/
.ico {
  /* flex: none — щоб у флекс-рядку іконку не стискало під текст. */
  flex: none;
  width: 1.15em;
  height: 1.15em;

  /* Підганяння під базову лінію: інакше іконка «висить» над текстом. */
  vertical-align: -0.22em;
}

.ico-lg {
  width: 1.6em;
  height: 1.6em;
  vertical-align: -0.35em;
}

}
@layer components {
/* 40-components/menu.css */
/*
  Випадайки в топбарі: меню користувача й стрічка повідомлень. Побудовані на
  нативному popover: відкриття, закриття по кліку повз і по Esc браузер бере
  на себе — свого JS немає.

  Стрічка — це варіант .menu (.menu.feed), а не власне вікно: обидва висять під
  тим самим краєм топбару, і два різні кути, тіні чи анімації читалися б як два
  різні інтерфейси. Відрізняються вони рівно шириною й тим, що вміст стрічки
  прокручується (див. notifications.css).
*/
.menu[popover] {
  position: fixed;
  /* inset: auto скидає дефолтне центрування popover — далі позиціонуємо
     самі, під кнопкою в правому куті топбару. */
  inset: auto;
  top: calc(var(--topbar-h) + var(--s-1));
  right: var(--s-4);
  min-width: 240px;
  margin: 0;
  padding: var(--s-1);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--e-3);
}

/* Стрічка повідомлень ширша за меню й не має власних падінгів: заголовок і
   рядки задають їх самі, а прокрутка має йти по краю вікна, а не всередині
   рамки з відступом. Вужче за 22rem рядок «Дмитро підтвердив оплату
   Cloudflare» ламається на три стрічки й перестає читатись з першого погляду. */
.menu[popover].feed {
  width: min(24rem, calc(100vw - 2 * var(--s-4)));
  padding: 0;
  overflow: hidden;
}

/* Підложка потрібна, щоб ловити клік «повз меню», але затемнювати
   сторінку заради випадайки з двох пунктів — надто. */
.menu[popover]::backdrop {
  background: transparent;
}

/* Поява/зникнення. allow-discrete — щоб анімувалися й display з overlay:
   без нього меню зникало б миттєво, без згасання. */
.menu[popover] {
  opacity: 0;
  translate: 0 -4px;
  transition: opacity 0.12s, translate 0.12s, overlay 0.12s allow-discrete,
    display 0.12s allow-discrete;
}

.menu[popover]:popover-open {
  opacity: 1;
  translate: none;
}

/* Стан, з якого починається поява. Без @starting-style браузеру нема
   від чого анімувати елемент, якого щойно не було в потоці. */
@starting-style {
  .menu[popover]:popover-open {
    opacity: 0;
    translate: 0 -4px;
  }
}

.menu-head {
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--border);
}

/* break-all — щоб довгий email переносився, а не розпирав меню. */
.menu-head strong {
  display: block;
  font-size: var(--t-cmp);
  word-break: break-all;
}

.menu-head .hint {
  margin-top: 0;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  min-height: 34px;
  padding: 0 var(--s-3);
  font-size: var(--t-cmp);
  font-weight: 600;
  color: var(--ink-muted);
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.menu-item:hover {
  color: var(--ink);
  background: var(--surface-3);
}

.menu-sep {
  height: 1px;
  margin: var(--s-1) 0;
  background: var(--border);
}

.menu-item.danger:hover {
  color: var(--fail-ink);
  background: var(--fail-bg);
}

}
@layer components {
/* 40-components/modal.css */
/*
  Модальне вікно. Розмітка — .modal > .modal-overlay + .modal-content,
  показ і приховування — класом .open, який ставить JS.

  Інлайнового display у шаблоні немає навмисно: інакше стан вікна жив би
  у двох місцях одразу — в атрибуті style і в класі — і рано чи пізно
  вони б розійшлися.
*/
.modal {
  position: fixed;
  inset: 0;
  /* Вище за топбар (20) і за бічну шухляду (40) з 30-layout.css. */
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
}

.modal.open {
  display: flex;
}

.modal-overlay {
  position: absolute;
  inset: 0;
  background: oklch(0 0 0 / 0.5);
}

/* Вміст піднімаємо над накладкою через position, а не через від'ємний
   z-index у самої накладки: так вікно лишається зверху в будь-якому
   контексті накладання, а клік по накладці далі закриває його. */
.modal-content {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 520px;
  max-height: min(80vh, 680px);
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  /* Смужка згори — той самий акцент, що й у плиток дашборду. */
  border-top: 3px solid var(--accent);
  border-radius: var(--r-lg);
  box-shadow: var(--e-3);
}

.modal-header {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--border);
}

.modal-header h2 {
  margin: 0;
  font-size: var(--t-lg);
}

.modal-close {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  font-size: var(--t-xl);
  line-height: 1;
  color: var(--ink-muted);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: color 0.12s, background-color 0.12s, border-color 0.12s;
}

.modal-close:hover {
  color: var(--ink);
  background: var(--surface-3);
  border-color: var(--border);
}

.modal-body {
  flex: 1;
  padding: var(--s-5);
  overflow-y: auto;
  /* Прокрутка списку не «протікає» на сторінку під вікном. */
  overscroll-behavior: contain;
}

.modal-intro {
  margin: 0 0 var(--s-4);
  font-size: var(--t-md);
  color: var(--ink-muted);
}

.modal-footer {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-5);
  background: var(--surface-2);
  border-top: 1px solid var(--border);
}

/* Підпис ліворуч від кнопок: скільки сервісів обрано, а в разі збою —
   що саме пішло не так. Список галочок при цьому лишається на місці. */
.modal-note {
  margin-right: auto;
  font-size: var(--t-sm);
  color: var(--ink-muted);
}

.modal-note.error {
  font-weight: 600;
  color: var(--fail-ink);
}

.services-checklist {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

/* Заголовок підгрупи в списку галочок — той самий підпис, що над плитками
   й у сайдбарі. Перший відступу згори не має: він відкриває список. */
.services-group {
  margin: var(--s-3) 0 0;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

.services-group:first-child {
  margin-top: 0;
}

/* Скидаємо жирність і відступи, які label дістає в 20-base.css: там він
   підпис поля, а тут — ціла клікабельна картка сервіса. */
.service-check {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  margin: 0;
  padding: var(--s-3);
  font-weight: 400;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background-color 0.12s, border-color 0.12s;
}

.service-check:hover {
  background: var(--surface-2);
  border-color: var(--border-strong);
}

/* Обране видно, не вчитуючись у галочки: підсвічений увесь рядок. */
.service-check:has(input:checked) {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* Фокус живе на прихованій галочці, тож рамку малюємо навколо рядка —
   інакше з клавіатури не видно, де ти. */
.service-check:has(input:focus-visible) {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.service-check input[type="checkbox"] {
  flex: none;
  width: auto;
  /* Невеликий зсув униз — щоб галочка стала на рівень першого рядка
     назви, а не її верхнього краю. */
  margin: 0.2em 0 0;
  accent-color: var(--brand);
  cursor: pointer;
}

.service-check .service-title {
  display: block;
  font-weight: 600;
  font-size: var(--t-md);
  color: var(--ink);
}

.service-check .service-path {
  display: block;
  margin-top: var(--s-1);
  font-size: var(--t-sm);
  color: var(--ink-muted);
}

/* Стан списку: «завантажуємо», «нічого немає», помилка. */
.services-status {
  margin: 0;
  padding: var(--s-4) 0;
  font-size: var(--t-md);
  text-align: center;
  color: var(--ink-muted);
}

.services-status.error {
  color: var(--fail-ink);
}

/* На вузькому екрані вікно стає «шторкою» знизу: так до кнопок легше
   дотягнутись великим пальцем, ніж коли вони посеред екрана. */
@media (max-width: 640px) {
  .modal {
    align-items: flex-end;
    padding: 0;
  }

  .modal-content {
    max-width: 100%;
    max-height: 92vh;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
  }

  .modal-header,
  .modal-body,
  .modal-footer {
    padding-inline: var(--s-4);
  }
}

}
@layer components {
/* 40-components/notifications.css */
/*
  Дзвіночок у топбарі й стрічка під ним.

  Сама коробка — це .menu.feed (див. menu.css): те саме нативне popover-вікно,
  що й у меню користувача, лише ширше й із прокруткою. Власного вікна тут не
  заводиться навмисно — випадайка в топбарі має бути одна, з однаковим кутом,
  тінню й анімацією.

  Головне правило вигляду: стрічка НЕ кричить. Єдиний насичений колір на весь
  компонент — крапка лічильника, і саме вона й несе всю терміновість. Тост, який
  тут стояв раніше, кричав кольором на кожній сторінці — і саме тому його
  навчились не бачити.
*/

/* ─── Кнопка з лічильником ───────────────────────────────────────── */

.icon-btn.bell {
  /* Лічильник позиціонується від кнопки, тож їй потрібна власна система
     координат. Решта .icon-btn її не має й не потребує. */
  position: relative;
}

.bell-count {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  /* Текст кольором тла сторінки — не «білий». --bg і --fail-ink рухаються по
     світлості в протилежні боки (у темній темі перший темнішає, другий
     світлішає), тож контраст тримається сам, в обох темах і без другого
     токена. */
  color: var(--bg);
  background: var(--fail-ink);
  /* Коло на однозначному числі, «пігулка» на двозначному — з --r-full це
     виходить само. */
  border-radius: var(--r-full);
  /* Обвідка кольором топбару: без неї цифра зливається з іконкою під нею. */
  box-shadow: 0 0 0 2px var(--surface);
  pointer-events: none;
}

/* ─── Заголовок вікна ────────────────────────────────────────────── */

.feed-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.feed-tools {
  display: flex;
  gap: var(--s-1);
}

/*
  Дві дії над стрічкою. Зроблені тихими — текст кольору підказки, без рамки,
  без заливки: це прибирання, а не те, заради чого вікно відкривали. Помітніша
  за них має лишатись сама стрічка.

  Це не .btn: .btn.small усе одно несе рамку й вагу кнопки форми, а поруч із
  заголовком на 240 пікселів дві такі кнопки читались би як головна дія вікна.
*/
.feed-tool {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1) var(--s-2);
  font-size: var(--t-sm);
  color: var(--ink-subtle);
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.feed-tool:hover { color: var(--ink); background: var(--surface-3); }
.feed-tool.danger:hover { color: var(--fail-ink); background: var(--fail-bg); }
.feed-tool:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.feed-tool .ico { width: 1em; height: 1em; }

/* Кнопка, якій зараз нема чого робити (нічого не прочитано / усе прочитано),
   лишається на місці, але не вдає, що працює: зникнення смикало б заголовок,
   а зникла кнопка ще й змушує щоразу перевіряти, чи вона там була. */
.feed-tool[disabled] {
  opacity: 0.4;
  cursor: default;
  pointer-events: none;
}

/* ─── Стрічка ────────────────────────────────────────────────────── */

.feed-list {
  /* Одна висота на всі стани, як у .panel.scroll: вікно не має стрибати,
     коли прочитане прибрали. */
  max-height: min(60vh, 30rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.feed-status {
  margin: 0;
  padding: var(--s-4) var(--s-3);
  text-align: center;
}

/*
  Рядок. Клікабельна вся площа, а не заголовок: у вікні шириною 24rem влучити
  в текстовий рядок мишею дорожче, ніж здається. Клік гасить рядок і веде туди,
  де подія сталася.

  Тег у рядка буває двох видів (див. notifications.js): <a>, коли є куди вести,
  і <button>, коли вести нема куди («ваш запис видалили»), — але виглядають
  вони однаково, бо роблять однакове. Звідси й скидання кнопкових умовностей
  нижче: власного вигляду в <button> тут не лишається взагалі.
*/
.feed-item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-1) var(--s-2);
  width: 100%;
  padding: var(--s-2) var(--s-3);
  font: inherit;
  text-align: left;
  color: inherit;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.feed-item:hover { background: var(--surface-3); }
.feed-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/*
  Іконка сервісу-джерела в його ж кольорі. --h-item ставить скрипт із accent,
  який прийшов у JSON, — тобто рівно того відтінку, яким сервіс підсвічений у
  навбарі. Сервіс без власного акценту (і невідомий слуг) успадковує --h-accent
  сторінки, тож дірки в кольорі не буває ніколи.

  Формула світлості — та сама, що в --accent (10-tokens.css): у темній темі
  колір має світлішати, інакше іконка сервісу тоне у власному тлі.
*/
.feed-item .ico {
  grid-row: span 2;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 2px;
  color: light-dark(
    oklch(0.55 0.2 var(--h-item, var(--h-accent))),
    oklch(0.7 0.16 var(--h-item, var(--h-accent)))
  );
}

.feed-title {
  font-size: var(--t-cmp);
  /* Не 600 і не 700: у стрічці з десяти рядків суцільний напівжирний читається
     гірше за звичайний — око не має за що зачепитись. Виділяє непрочитане
     крапка праворуч, а не вага шрифту. */
  font-weight: 500;
}

.feed-body {
  grid-column: 2;
  margin: 0;
  font-size: var(--t-sm);
  color: var(--ink-muted);
}

.feed-when {
  grid-column: 2;
  font-size: var(--t-sm);
  color: var(--ink-subtle);
}

/*
  Позначка непрочитаного. Крапка, а не заливка рядка: заливка десяти рядків
  поспіль перетворює список на суцільну пляму, у якій нічого не виділено.
*/
.feed-item.unread { position: relative; }

.feed-item.unread::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--s-2);
  width: 6px;
  height: 6px;
  margin-top: -3px;
  background: var(--accent);
  border-radius: 50%;
}

/* Місце під крапку — щоб довгий заголовок не заповзав під неї. */
.feed-item.unread .feed-title,
.feed-item.unread .feed-body { padding-right: var(--s-3); }

/* Роздільник між рядками — волосина, а не відступ: у вікні заввишки 30rem
   відступи з'їли б два рядки зі списку. */
.feed-item + .feed-item {
  border-top: 1px solid var(--border);
  border-radius: 0;
}

/* «Показати ще» — на всю ширину, під останнім рядком. Не .btn: це
   продовження списку, а не дія над ним. */
.feed-more {
  display: block;
  width: 100%;
  padding: var(--s-2);
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--accent);
  background: none;
  border: 0;
  border-top: 1px solid var(--border);
  cursor: pointer;
}

.feed-more:hover { background: var(--surface-3); }

}
@layer components {
/* 40-components/pager.css */
/* Пагінація під таблицями адмінки (шаблон _pager.html). */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin: var(--s-4) 0;
}

.pager-info {
  font-size: var(--t-sm);
  color: var(--ink-muted);

  /* Моноширинні цифри: підпис не смикається при зміні сторінки. */
  font-variant-numeric: tabular-nums;
}

}
@layer components {
/* 40-components/panel.css */
/*
  .panel — врізка: коробка на тлі картки. Список людей у «Відстеженні Sub»,
  список кампаній у «Заміні SS», JSON у «Генераторі анкет», вкладення й
  коментарі в «Календарі», лог запуску в історії — усе це одна й та сама
  річ: заглиблений прямокутник із власною межею всередині картки.

  Досі кожен сервіс описував її сам, і збігалося лише тло. Падінги були
  var(--s-2) var(--s-3), var(--s-3), var(--s-3) var(--s-4) — три різні
  всередині одного інтерфейсу, — а висоти прокрутки 320px, 340px, 420px і
  460px. Причин для цих чисел не було: вони просто писалися незалежно, і
  саме це видно, коли переходиш між сервісами по навбару.

  Тепер значення тут одні на всіх. Сервіс лишає собі лише те, що справді
  його: сітку всередині, шрифт, кольори станів.

  Клас додається ДО власного класу елемента (class="report panel scroll"),
  а не замість нього: за власними класами тримаються і сітка сервісу, і
  його скрипти.
*/
.panel {
  padding: var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

/*
  Врізка з власною прокруткою. Одна висота на всі: 22rem — це приблизно
  та сама смужка, до якої незалежно прийшли лог (340px) і список кампаній
  (320px), і водночас достатньо, щоб у списку людей було видно кілька
  рядків одразу.

  overscroll-behavior — доскроливши врізку до кінця, не тягнемо за собою
  сторінку під нею.

  Сторінка, де врізка займає весь екран (живий лог), перебиває max-height
  своїм класом у @layer service — той шар оголошений після components.
*/
.panel.scroll {
  max-height: 22rem;
  overflow: auto;
  overscroll-behavior: contain;
}

/*
  Врізка-список, у якої падінги мають самі рядки: вони тягнуться від краю
  до краю й розділені лініями, тож зовнішній падінг лишив би ці лінії
  висіти в повітрі, не доходячи до межі.
*/
.panel.flush {
  padding: 0;
}

/*
  Врізка з машинним текстом — JSON, технічний звіт про доступи, вивід
  скрипта. Стилюємо за тегом: <pre> у картці — це за визначенням текст, у
  якому важливі колонки й переноси, а не абзац.

  Рядки навмисно НЕ переносяться: у JSON вкладеність, а у звіті колонки
  тримаються саме позицією символів, і м'який перенос їх розсипає. Прокрутка
  по горизонталі тут не незручність, а умова читабельності.

  Ці ж чотири рядки лежали окремо у «Відстеженні Sub» і в «Генераторі
  анкет» — байт у байт однакові, з двома різними іменами класів.
*/
pre.panel {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  line-height: 1.55;
  white-space: pre;
}

}
@layer components {
/* 40-components/select.css */
/*
  Випадаючий список (shared/static/js/select.js).

  Рідний <select> у розкритому вигляді малює операційна система: на macOS це
  системний сірий список зі своїм шрифтом, який не знає ні про темну тему
  сторінки, ні про її акцент. Стилювати його вміст не дозволяє жоден браузер,
  тож панель тут намальована своя — токенами, як і решта компонентів, і разом
  із темою вона перемикається сама.

  Кнопка навмисно повторює вигляд поля з form.css (та сама рамка, радіус,
  фокус): для людини це те саме поле форми, а не окремий тип контролу.
*/

/*
  Обгортка заступає місце самого <select>, тож і поводитись має так само.
  Рідний список — inline-block, який сам розтягується під НАЙДОВШИЙ свій
  пункт (form.css: width: auto; min-width: 12ch). На цьому мовчки тримається
  ширина колонок у «Розподілі костів» і полів у «Генераторі анкет», тож
  обгортка повторює саме таку поведінку — інакше кнопка або розпирала б
  контейнер на всю ширину, або схлопувалась до підпису.
*/
.sel {
  position: relative;
  display: inline-block;
  max-width: 100%;
  min-width: 12ch;
  vertical-align: middle;
}

/*
  Невидима мірка: у ній лежить найдовший пункт списку, і саме вона задає
  ширину обгортки — так само, як у рідного select. Без неї кнопка мірялася б
  ПОТОЧНИМ значенням, і поле стрибало б туди-сюди на кожен вибір.

  height: 0 — мірка не займає жодного рядка; padding-inline і margin-right
  повторюють падінги кнопки та місце під стрілку, щоб текст найдовшого пункту
  вміщався в неї цілим.
*/
.sel-sizer {
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  white-space: nowrap;
  font-size: var(--t-md);
  padding-inline: var(--s-3);
  margin-right: calc(1em + var(--s-2) + 2px);
}

/* Поле форми розтягується на всю ширину — як і будь-який input у .field. */
.field > .sel {
  display: block;
  width: 100%;
}

/* У рядку форми поле забирає вільне місце (пор. `.form-row > select`). */
.form-row > .sel {
  flex: 1;
  min-width: 0;
}

/* Рідний список лишається в DOM — у ньому значення й воно ж їде у формі.
   Ховаємо його візуально, але не через display:none: прихований так контрол
   у частині браузерів випадає з відправки форми. */
.sel-native {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  border: 0;
}

.sel-btn {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  min-height: 38px;
  padding: var(--s-2) var(--s-3);
  font: inherit;
  font-size: var(--t-md);
  color: var(--ink);
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: border-color 0.12s, box-shadow 0.12s;
}

.sel-btn:hover:not(:disabled) {
  border-color: var(--ink-subtle);
}

.sel-btn:focus-visible {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.sel-btn:disabled {
  color: var(--ink-subtle);
  background: var(--surface-2);
  cursor: not-allowed;
}

/* Розкритий список підсвічуємо так само, як фокус: поле й панель — одне ціле. */
.sel-btn[aria-expanded="true"] {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}

/* Текст вибраного займає весь вільний простір і обрізається трьома крапками:
   назви трафік-сорсів бувають довшими за поле, і без цього кнопка розпирала
   б рядок форми. */
.sel-value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* «— виберіть сорс —» це підказка, а не вибір: показуємо її приглушено, як
   placeholder у звичайному полі. */
.sel-value.muted {
  color: var(--ink-subtle);
}

.sel-caret {
  flex: none;
  color: var(--ink-muted);
  transition: rotate 0.12s;
}

.sel-btn[aria-expanded="true"] .sel-caret {
  rotate: 180deg;
}

/*
  Панель. position: fixed, а координати виставляє скрипт — і це не примха.

  Частина полів живе всередині .table-scroll (overflow-x: auto). Абсолютно
  спозиційована панель обрізалась би таким предком: за специфікацією, коли по
  одній осі стоїть auto, друга з visible теж стає auto, — тобто випадайка
  зникала б рівно там, де таблиця вужча за екран. Fixed-елемент overflow
  предка не обрізає, тож панель однаково працює і в картці, і в комірці
  прокрученої таблиці.

  Плата за це — координати треба рахувати самим, і вони застарівають при
  прокрутці. Тому select.js на будь-яку прокрутку панель закриває.

  z-index 50 — той самий поверх, що й у календаря: накриває каркас сторінки,
  але лишається під тостами (60).
*/
.sel-panel {
  position: fixed;
  z-index: 50;
  display: flex;
  flex-direction: column;
  min-width: 14rem;
  max-height: 20rem;
  padding: var(--s-1);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--e-3);
}

.sel-search-box {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-2);
  border-bottom: 1px solid var(--border);
}

.sel-search-box .ico {
  flex: none;
  color: var(--ink-subtle);
}

/* Поле пошуку всередині панелі — без власної рамки: рамку вже має панель,
   а друга всередині неї виглядає як вкладена коробка. */
.sel-search-box input {
  width: 100%;
  padding: var(--s-1) 0;
  font-size: var(--t-cmp);
  background: none;
  border: 0;
}

.sel-search-box input:focus-visible {
  outline: none;
  border: 0;
  box-shadow: none;
}

.sel-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Прокрутка списку не має «протікати» на сторінку, коли дійшла до краю. */
  overscroll-behavior: contain;
  padding: var(--s-1) 0;
}

.sel-option {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 32px;
  padding: var(--s-1) var(--s-2);
  font-size: var(--t-cmp);
  color: var(--ink);
  border-radius: var(--r-sm);
  cursor: pointer;
}

/* Підсвітка одна на мишу й на клавіатуру: інакше видимих «поточних» пунктів
   стає два — той, під курсором, і той, до якого дійшли стрілками. */
.sel-option.active {
  background: var(--surface-3);
}

.sel-option.selected {
  font-weight: 600;
  color: var(--brand);
}

.sel-option.placeholder {
  color: var(--ink-subtle);
}

.sel-option.disabled {
  color: var(--ink-subtle);
  cursor: not-allowed;
}

.sel-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Галочка тримає своє місце завжди — інакше рядки зсувалися б по горизонталі
   залежно від того, який із них вибраний. */
.sel-check {
  flex: none;
  margin-left: auto;
  visibility: hidden;
  color: var(--brand);
}

.sel-option.selected .sel-check {
  visibility: visible;
}

.sel-empty {
  margin: 0;
  padding: var(--s-3);
  font-size: var(--t-sm);
  color: var(--ink-muted);
  text-align: center;
}

}
@layer components {
/* 40-components/steps.css */
/*
  Дві різні речі про кроки:

  ol.steps      — статичний підсумок на сторінці результату (що вже сталося);
  .steps-tracker — живий трекер у AutoFlow (що відбувається зараз).
  Класи .done / .active / .failed на трекері перемикає скрипт сторінки.
*/

ol.steps {
  margin: 0;
  padding-left: var(--s-5);
}

ol.steps li {
  margin: var(--s-2) 0;
}

.status {
  margin-right: var(--s-1);
  font-weight: 700;
}

.steps-tracker {
  margin: var(--s-2) 0;
  padding: 0;
  list-style: none;
}

.steps-tracker li {
  position: relative;
  margin: var(--s-1) 0;
  padding: var(--s-1) var(--s-3);
  font-size: var(--t-cmp);
  color: var(--ink-muted);
  border-left: 3px solid var(--border-strong);
  transition: color 0.15s, border-color 0.15s;
}

.steps-tracker li.done {
  color: var(--ink);
  border-left-color: var(--ok-ink);
}

/* Позначки — псевдоелементами, щоб скрипту лишалося тільки перемикати
   класи, не чіпаючи вміст рядків. */
.steps-tracker li.done::after {
  content: "✓";
  margin-left: var(--s-2);
  color: var(--ok-ink);
  font-weight: 700;
}

.steps-tracker li.active {
  color: var(--ink);
  font-weight: 600;
  border-left-color: var(--warn-ink);
  background: var(--warn-bg);
}

.steps-tracker li.active::after {
  content: "…";
  margin-left: var(--s-2);
  color: var(--warn-ink);
  animation: steps-blink 1.2s ease-in-out infinite;
}

.steps-tracker li.failed {
  color: var(--fail-ink);
  font-weight: 600;
  border-left-color: var(--fail-ink);
  background: var(--fail-bg);
}

@keyframes steps-blink {
  50% {
    opacity: 0.3;
  }
}

.progress {
  height: 6px;
  margin: var(--s-2) 0;
  overflow: hidden;
  background: var(--surface-3);
  border-radius: var(--r-full);
}

/* Смужку рухає скрипт, задаючи width у відсотках; колір її завершення
   вирішують класи .done / .failed на самому .progress. */
.progress > span {
  display: block;
  width: 0;
  height: 100%;
  background: var(--brand);
  border-radius: inherit;
  transition: width 0.3s ease;
}

.progress.done > span {
  background: var(--ok-ink);
}

.progress.failed > span {
  background: var(--fail-ink);
}

}
@layer components {
/* 40-components/table.css */
/*
  Таблиці. Два класи історично: .table — списки в адмінці,
  .camp-table — таблиці результатів у сервісах. Стилі майже спільні.

  Підсвітка рядків станами: .row-fail, .degraded, .muted.
  Підсвітка комірок: .s-ok / .s-fail / .s-warning.
*/
.table,
.camp-table {
  width: 100%;
  margin: var(--s-3) 0;
  font-size: var(--t-cmp);
  border-collapse: collapse;
}

.table th,
.table td,
.camp-table th,
.camp-table td {
  padding: var(--s-2) var(--s-3);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}

.table thead th,
.camp-table thead th {
  font-size: var(--t-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  border-bottom-color: var(--border-strong);
  white-space: nowrap;
}

/* th у tbody — це «таблиця-картка» виду ключ/значення
   (деталі запуску), а не заголовок колонки. */
.table tbody th {
  width: 30%;
  font-weight: 600;
  color: var(--ink-muted);
}

.table tbody tr:last-child td,
.camp-table tbody tr:last-child td {
  border-bottom: 0;
}

.table tbody tr:hover td {
  background: var(--surface-2);
}

.table tr.muted td {
  color: var(--ink-subtle);
}

.table tr.degraded td {
  background: var(--warn-bg);
}

.camp-table tr.row-fail td,
.table tr.row-fail td {
  background: var(--fail-bg);
}

.s-ok,
.camp-table .s-ok {
  color: var(--ok-ink);
  font-weight: 700;
}

.s-fail,
.s-failed,
.camp-table .s-fail {
  color: var(--fail-ink);
  font-weight: 700;
}

.s-warning {
  color: var(--warn-ink);
  font-weight: 700;
}

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

/*
  Комірка з числом: праворуч і цифрами однакової ширини. Разом це дає
  стовпчик, який читається згори вниз одним поглядом — розряди стоять під
  розрядами, а не «пливуть» від рядка до рядка.

  Один клас на всі таблиці CRM. Доти те саме писалося в кожному сервісі під
  своїм іменем: .cost-num у картці місяця, .num у трьох таблицях довідників,
  ще одне правило у «Оплаті сервісів» — і, наприклад, у картці місяця числа
  вирівнювались праворуч, а в довідниках поруч — ні.

  На інлайновому <span> (число посеред речення) правило безпечне:
  text-align діє на блок, а не на рядковий елемент, тож там працює лише
  моноширинність цифр.
*/
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Комірка з машинним рядком без пробілів — URL постбека, ключ, хеш. Рветься
   в будь-якому місці, бо інакше один такий рядок розпирає таблицю ширше за
   екран. Класом, а не інлайновим стилем у шаблоні: така комірка є не в
   одному сервісі. */
.detail.break,
td.break {
  word-break: break-all;
}

.table td code,
.camp-table td code {
  background: none;
  padding: 0;
  font-size: var(--t-cmp);
}

/* Обгортка для широких таблиць: скролиться сама, а не розпихає сторінку. */
.table-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

}
@layer components {
/* 40-components/theme-toggle.css */
/*
  Кнопка теми. Усі три підписи лежать у розмітці одразу, а видимий
  вибирається селектором за data-theme-state — скрипт лише перемикає
  атрибут і нічого не переписує в DOM.
*/
.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);

  min-height: 34px;
  padding: var(--s-1) var(--s-3);
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-muted);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background-color 0.12s, color 0.12s, border-color 0.12s;
}

.theme-toggle:hover {
  color: var(--ink);
  background: var(--surface-3);
  border-color: var(--border);
}

.theme-toggle [data-mode] {
  display: none;
}

.theme-toggle[data-theme-state="system"] [data-mode="system"],
.theme-toggle[data-theme-state="light"] [data-mode="light"],
.theme-toggle[data-theme-state="dark"] [data-mode="dark"] {
  display: inline;
}

}
@layer components {
/* 40-components/tile.css */
/* Підгрупа плиток на дашборді: заголовок і своя сітка під ним. Відстань
   між секціями задана не тут, а в 50-rhythm.css — разом із рештою
   вертикального ритму сторінки й у шарі, який його перебиває.

   Той самий підпис, що й над групою в сайдбарі: дрібний, розріджений,
   капітеллю. Однакові написи в двох місцях читаються як одна навігація. */
.tile-group-head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0 0 var(--s-3);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-subtle);
}

/* Лінія добігає до правого краю сітки: без неї заголовок висить над
   плитками сам по собі й не читається як межа секції. */
.tile-group-head::after {
  flex: 1;
  height: 1px;
  background: var(--border);
  content: "";
}

/* Лічильник стоїть ПЕРЕД лінією, поруч із назвою — це підпис до неї,
   а не окрема цифра десь у ряду. */
.tile-group-count {
  min-width: 1.5em;
  padding: 0 var(--s-1);
  font-size: var(--t-xs);
  font-weight: 700;
  line-height: 1.6;
  text-align: center;
  color: var(--ink-muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

/* Плитки сервісів на дашборді й розділів в адмінці.
   Кількість колонок підбирається сама, без медіа-запитів. */
.tiles {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.tile {
  /* --h-tile ставить дашборд інлайном на кожну плитку сервісу — рівно так
     само, як --h-side на пункт навбару. Немає його (розділи адмінки) —
     береться акцент сторінки, тобто колір поточного сервісу.

     Похідний колір оголошений тут, на самій плитці, а не серед токенів:
     var() у значенні змінної підставляється там, де змінну ОГОЛОШЕНО, тож
     --accent із :root на інлайновий hue плитки вже не відреагував би. */
  --tile-accent: light-dark(
    oklch(0.55 0.2 var(--h-tile, var(--h-accent))),
    oklch(0.7 0.16 var(--h-tile, var(--h-accent)))
  );

  display: block;
  padding: var(--s-4);
  color: var(--ink);
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--border);

  /* Смужка згори — колір сервісу. Доки дашборд був однією суцільною сіткою,
     усі плитки були однакові й колір лишався сторінці сервісу; з підгрупами
     саме він відрізняє сусідні плитки в ряду, як і в навбарі. */
  border-top: 3px solid var(--tile-accent);
  border-radius: var(--r-lg);
  box-shadow: var(--e-1);
  transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
}

.tile:hover {
  transform: translateY(-2px);
  border-color: var(--border-strong);
  border-top-color: var(--tile-accent);
  box-shadow: var(--e-2);
}

.tile .tile-icon {
  color: var(--tile-accent);
}

/* Запасний варіант, коли іконки немає: icon() повертає порожньо для
   невідомого імені, і тоді в плитці лишається текст або емодзі —
   збільшуємо його, щоб він виглядав як іконка. */
.tile .tile-icon:not(:has(svg)) {
  font-size: var(--t-2xl);
  line-height: 1;
}

.tile .tile-title {
  margin-top: var(--s-2);
  font-weight: 600;
  font-size: var(--t-md);
}

.tile .tile-path {
  margin-top: var(--s-1);
  font-size: var(--t-sm);
  color: var(--ink-muted);
}

/* Сервіс не піднявся (див. app.state.degraded). Плитка лишається
   клікабельною — просто попереджає. */
.tile.degraded {
  border-color: var(--fail-line);
  border-top-color: var(--fail-ink);
  background: var(--surface-2);
}

.tile .tile-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  margin-top: var(--s-2);
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--fail-ink);
}

.tiles-empty {
  color: var(--ink-muted);
}

}
@layer components {
/* 40-components/timeline.css */
/*
  Стрічка найближчих семи днів на дашборді.

  Компонент оболонки, а не сервісу, з тієї ж причини, що й .alerts-panel:
  малює її скрипт (static/js/timeline.js), а скрипт живе в shared — сама
  оболонка не знає, ЯКИЙ сервіс віддає стрічку, вона бачить лише контейнер
  із data-timeline-url.

  Сім однакових колонок, а не «скільки влізе»: тиждень має читатись як
  тиждень, і день, який поїхав на другий ряд, ламає саме це. На вузькому
  екрані стрічка від'їжджає горизонтальним скролом усередині себе.
*/
.timeline-panel {
  overflow-x: auto;
  /* Відступ під тінь картки: без нього нижня тінь зрізається скролом. */
  padding-bottom: var(--s-1);
}

.timeline-strip {
  display: grid;
  grid-template-columns: repeat(7, minmax(9rem, 1fr));
  gap: var(--s-2);
}

.timeline-day {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  min-height: 7rem;
  padding: var(--s-2) var(--s-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

/* Сьогодні виділене акцентною рамкою й тлом: стрічка існує, щоб відповісти
   «що зараз», і початок відліку має бути видно, не читаючи підписів. */
.timeline-day.is-today {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.timeline-day.is-weekend .timeline-date {
  color: var(--warn-ink);
}

.timeline-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-1);
  padding-bottom: var(--s-1);
  border-bottom: 1px solid var(--border);
}

.timeline-label {
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.timeline-day.is-today .timeline-label {
  color: var(--accent);
}

.timeline-date {
  font-size: var(--t-md);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.timeline-item {
  display: flex;
  align-items: baseline;
  gap: var(--s-1);
  padding: 2px var(--s-1);
  font-size: var(--t-sm);
  color: var(--ink);
  text-decoration: none;
  background: var(--surface-2);
  border-left: 2px solid var(--accent-line);
  border-radius: var(--r-sm);
}

.timeline-item:hover {
  background: var(--surface-3);
}

/* Запис без часу — інший вигляд, а не інша позиція: він не стоїть у
   розкладі й виглядати як подія о певній годині не повинен. */
.timeline-item.no-time {
  border-left-color: var(--border-strong);
}

.timeline-at {
  font-size: var(--t-xs);
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.timeline-what {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timeline-item .ico {
  width: 12px;
  height: 12px;
  flex: none;
  color: var(--ink-subtle);
}

/* Порожній день — не порожнеча, а відповідь («вільно»), тож підпис є, але
   ледь помітний: він не має конкурувати з днями, де щось є. */
.timeline-free {
  font-size: var(--t-xs);
  color: var(--ink-subtle);
}

.timeline-more {
  font-size: var(--t-xs);
  color: var(--ink-muted);
  text-decoration: none;
}

.timeline-more:hover {
  color: var(--accent);
}

}

@layer rhythm {
/* 50-rhythm.css */
/*
  Вертикальний ритм сторінки — один крок на всі сервіси.

  Досі відступ між блоками задавав кожен шаблон сам: де margin-bottom у
  власному @layer service, де інлайновий style="margin-top: …", а де взагалі
  нічого, і блоки злипались. Через це однакові за змістом сторінки
  відрізнялися відступами — рівно те, що видно, коли перемикаєшся між
  сервісами по навбару.

  Тут це сказано один раз: сусідні блоки верхнього рівня всередині
  .container розділені кроком --s-5. Компоненти власних зовнішніх відступів
  більше не потребують, а ті, що їх мають (.alert, .table), тут
  перекриваються — саме тому шар rhythm оголошений ПІСЛЯ components.
  У самих компонентах ці margin лишились: вони працюють, коли компонент
  вкладений у картку чи в комірку таблиці, а не стоїть блоком на сторінці.
*/

/*
  Заголовки виключені з обох боків правила:

    • сам заголовок зберігає власний, більший відступ згори (20-base.css) —
      він відкриває секцію, а не стоїть у ряд із блоками;
    • блок одразу ПІД заголовком не отримує нічого — інакше підпис
      відірвався б від того, що підписує.

  .section-head — той самий заголовок, лише з дією праворуч, тож поводиться
  так само. .modal у списку з іншої причини: це position: fixed з inset: 0,
  і margin у нього не «відступ», а зсув самого вікна від краю екрана.
*/
.container > *:not(h1, h2, h3, .section-head, .modal)
           + *:not(h1, h2, h3, .section-head, .modal) {
  margin-top: var(--s-5);
}

/*
  Заголовок, що відкриває свій контейнер, власного відступу згори не має:
  його вже дає падінг картки або сама сітка. Без цього дві колонки поруч
  починалися на різній висоті — та, що з заголовка, з'їжджала вниз на
  32px відносно тієї, що з форми (саме так було в AutoFlow і «Доменах»).

  Правило живе тут, а не в 20-base.css: .section-head описаний у шарі
  components, і скидання з ранішого шару його б не перебило.
*/
h2:first-child,
h3:first-child,
h4:first-child,
.section-head:first-child {
  margin-top: 0;
}

/*
  Підгрупи плиток на дашборді розсунуті ширше за звичайний крок: усередині
  групи плитки стоять за 16px одна від одної, і на 24px ритму групи ще
  читалися б як одна суцільна сітка. 32px — та відстань, з якої видно, що
  блоків кілька, і саме вона робить поділ поділом.

  Правило живе тут, а не в tile.css: секції — сусіди верхнього рівня в
  .container, тож правило вище вже дало їм свої 24px, а шар rhythm
  оголошений після components і компонентному margin просто не програє.
  .container у селекторі — з тієї ж причини: без нього специфічність нижча
  за правило вище, і 24px лишились би.
*/
.container > .tile-group + .tile-group {
  margin-top: var(--s-6);
}

/*
  Банер аварійного входу стоїть найпершим у .main, ще над .container, тож
  відступ згори йому не потрібен — його дає падінг самої області. Правило
  живе тут, а не в 30-layout.css: там воно програвало власному margin
  компонента .alert, бо шар components оголошений після layout.
*/
.breakglass {
  margin-top: 0;
}

/*
  Той самий крок усередині картки чи згорнутої секції — там, де блоки
  так само стоять один під одним. Опт-ін класом, а не автоматично: у
  картці частіше лежить текст і поля форми, у яких свій, щільніший ритм.
*/
.stack > * + * {
  margin-top: var(--s-4);
}

}

@layer utilities {
/* 90-utilities.css */
/*
  Утиліти — останній шар, тож перебивають будь-який компонент.
  Саме тому їх мало й вони точкові: це не «атомарний CSS», а виняток
  на випадок, коли компоненту потрібне разове відхилення.
*/

/* Приховати візуально, але лишити для скрінрідера. Не display: none —
   те, чого немає в потоці, скрінрідер теж не бачить. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* «Перейти до вмісту» — перше посилання на сторінці. Схований за верхнім
   краєм і виїжджає лише під фокусом, щоб той, хто ходить із клавіатури,
   міг проскочити навбар. */
.skip-link {
  position: absolute;
  left: var(--s-2);
  top: var(--s-2);
  z-index: 100;
  padding: var(--s-2) var(--s-3);
  color: var(--brand-ink);
  background: var(--brand);
  border-radius: var(--r-md);
  transform: translateY(-200%);
}

.skip-link:focus-visible {
  transform: none;
}

/* Один із двох !important у проєкті (другий — у блоці prefers-reduced-motion).
   Тут він доречний: .hidden ставить скрипт (блок результату в AutoFlow),
   і приховування має спрацювати незалежно від display самого компонента. */
.hidden {
  display: none !important;
}

.mono {
  font-family: var(--font-mono);
}

/*
  Один рядок із трьома крапками замість решти. Ці три властивості працюють
  лише разом: без nowrap текст переноситься й ніколи не «переповнюється»,
  без overflow ellipsis нема на чому спрацювати.

  Саме тому вони й розповзалися по сервісах трійками — логін користувача в
  «Відстеженні Sub», назва кампанії в «Заміні SS», підпис вкладення й запис
  у клітинці календаря. Тепер це одне ім'я.

  Сусіду по флекс-рядку майже завжди потрібен ще й min-width: 0 — інакше
  він відмовляється стискатись, і обрізати стає нічого. Це вже властивість
  розкладки, тож лишається в самого елемента.
*/
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tabular {
  font-variant-numeric: tabular-nums;
}

}

/* Плавний перехід між сторінками там, де браузер це вміє.
   Суто косметика: де не підтримується — просто звичайна навігація. */
@view-transition {
  navigation: auto;
}
