/* ==========================================================================
   Broadcaster v2.0 — базовая таблица стилей.
   Единственная рукописная таблица стилей проекта. Build-шага нет (D-02),
   Tailwind удалён (D-01), тема только тёмная (D-10).

   Части файла (порядок обязателен):
     1. :root — токены, выведенные из частотной таблицы инлайн-стилей макета
     2. Дословный перенос базового блока макета: keyframes и три медиазапроса
     3. Стили шелла на атрибутах data-* из анатомии макета
     4. Библиотека компонентов (План 02) — семантические классы макросов

   Разделы 1-3 принадлежат Плану 01, раздел 4 — Плану 02. Дальше файл только
   потребляется: планы разделов классы не добавляют, а используют.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Токены
   В макете нет ни одного :root, ни одной CSS-переменной и ни одного class=.
   Значения выведены по частотности из 490 инлайн-стилей
   (см. 01-RESEARCH.md §Design Token Extraction).
   -------------------------------------------------------------------------- */
:root {
  /* Поверхности */
  --bg: #08080b;
  --surface-side: #0b0b10;
  --surface: #0e0e14;
  --surface-pill: #0f0f16;
  --surface-input: #12121a;

  /* Границы — от самой слабой к самой сильной */
  --border-hair: #14141b;
  --border-soft: #16161e;
  --border-muted: #17171f;
  --border-side: #18181f;
  --border: #1a1a23;
  --border-pill: #1c1c25;
  --border-input: #1e1e28;
  --border-control: #24242e;
  --border-control-strong: #2a2a34;
  --border-dashed: #2e2e3a;

  /* Текст */
  --text: #eeeef2;
  --text-bright: #a8a8b6;
  --text-secondary: #8a8a98;
  --text-tertiary: #7a7a88;
  --text-muted: #6a6a78;
  --text-faint: #5f5f6e;
  --text-dim: #4e4e5c;
  --text-on-accent: #0a0a0d;

  /* Акцент */
  --accent: oklch(0.72 0.19 310);
  --accent-cta: oklch(0.79 0.14 305);
  --accent-link: oklch(0.78 0.16 310);
  --accent-link-hover: oklch(0.87 0.11 310);
  /* Летопись обвода фокуса: до 2026-09-25 — `rgba(196, 132, 252, .5)`, оценка
     UI-ревью ≈2.6:1 на фоне плашки отказа, ниже 3:1 WCAG 1.4.11; решение Г-2,
     пункт 5 (план 15-19). Непрозрачность стережёт
     `test_the_focus_ring_token_is_opaque_enough_for_non_text_contrast`. */
  --focus-ring: rgb(196, 132, 252);

  /* Статусы */
  --ok: oklch(0.75 0.17 150);
  --warn: oklch(0.82 0.16 85);
  --danger: oklch(0.68 0.19 25);
  --info: oklch(0.72 0.16 250);

  /* Радиусы */
  --r-xs: 6px;
  --r-sm: 8px;
  --r-md: 10px;
  --r-lg: 11px;
  --r-xl: 14px;
  --r-2xl: 16px;
  --r-pill: 99px;
  --r-circle: 50%;

  /* Кегли. Дробные ступени макета (9.5/10.5/11.5/12.5/13.5px) сглажены
     до целых — сознательное допущение A5. */
  --fs-2xs: 10px;
  --fs-xs: 11px;
  --fs-sm: 12px;
  --fs-md: 13px;
  --fs-lg: 14px;
  --fs-xl: 15px;
  --fs-2xl: 16px;
  --fs-h1: 22px;
  --fs-h2: 26px;
  --fs-h3: 32px;

  /* Гарнитуры (D-04, D-17). Основная текстовая — IBM Plex Sans, потому что
     у Space Grotesk в макете объявлены только latin, latin-ext и vietnamese:
     кириллического файла в манифесте физически нет, и весь русский текст
     молча ушёл бы в системный фолбэк. Space Grotesk остаётся только для
     логотипа и латинских акцентов. */
  --font-sans: 'IBM Plex Sans', system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-display: 'Space Grotesk', 'IBM Plex Sans', system-ui, sans-serif;
}

/* --------------------------------------------------------------------------
   1a. Гарнитуры — self-host (D-04). Внешних шрифтовых запросов нет.
   Одно правило на (гарнитура, вес, подмножество) — структура обеих исходных
   таблиц перенесена дословно.
   -------------------------------------------------------------------------- */

/* IBM Plex Sans — Основная текстовая гарнитура (D-17). Кириллические подмножества
   догружены отдельно: в манифесте макета их физически нет. */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* IBM Plex Mono — Mono-метки, счётчики и статусы. Извлечён из манифеста макета. */
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/ibm-plex-mono-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Space Grotesk — Только логотип и латинские акценты (--font-display): в макете
   объявлены лишь latin, latin-ext и vietnamese — кириллицы нет. */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/space-grotesk-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/space-grotesk-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/space-grotesk-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/space-grotesk-vietnamese.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/space-grotesk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   2. Дословный перенос из макета.
   Блок базовых стилей, пять @keyframes и три медиазапроса — самая ценная
   часть макета, переносится без переосмысления.
   -------------------------------------------------------------------------- */
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); }
a { color: var(--accent-link); text-decoration: none; }
a:hover { color: var(--accent-link-hover); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #24242c; border-radius: 8px; border: 3px solid var(--bg); }
::-webkit-scrollbar-track { background: var(--bg); }

@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .3; transform: scale(.8); } }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes orbit { to { transform: rotate(360deg); } }
@keyframes sweep { 0% { transform: translateX(-120%); } 100% { transform: translateX(420%); } }

/* Перенесено из base.html:25-29. Без [x-cloak] свёрнутые Alpine-блоки
   фильтров мигают при загрузке страницы. */
[x-cloak] { display: none !important; }
@keyframes fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fade-in 0.2s ease-out; }

@media (max-width: 1080px) {
  [data-hrow] { grid-template-columns: minmax(0, 1fr) !important;
    grid-template-areas: 'head' 'body' 'meta' 'err' !important; row-gap: 11px !important; }
  /* Единственное отступление от дословного переноса: в макете селектор был
     завязан на подстроку инлайн-стиля дочернего элемента (искал в атрибуте
     style фрагмент с именем области сетки) и молча перестал бы совпадать при
     переезде на классы. Здесь он переведён на атрибут data-area — План 05
     размечает историю по нему. Селекторов по подстроке инлайн-стиля в этом
     файле нет и появиться не должно. */
  [data-hrow] > [data-area="meta"] { border-left: none !important; padding-left: 0 !important;
    padding-top: 11px !important; border-top: 1px solid var(--border-muted) !important;
    justify-content: flex-start !important; flex-wrap: wrap !important; }
}

@media (max-width: 900px) {
  [data-editor] { grid-template-columns: minmax(0, 1fr) !important; }
  [data-editor-side] { position: static !important; }
}

@media (max-width: 860px) {
  [data-shell] { grid-template-columns: minmax(0, 1fr) !important; }
  [data-side] { position: static !important; height: auto !important; flex-direction: row !important;
    align-items: center !important; gap: 14px !important; padding: 12px 14px !important;
    border-right: none !important; border-bottom: 1px solid var(--border-side) !important;
    overflow: visible !important; }
  [data-nav] { display: none !important; }
  /* Мобильной поверхности у виджета доступа НЕТ и не появляется (A-4): правило
     сохранено дословно, сменилось только имя атрибута. Ту же величину на
     телефоне несёт панель раздела подписки, а критерий «человеку сказано
     словами» закрыт редиректом на /billing и плашкой, а не виджетом. */
  [data-access], [data-user] { display: none !important; }
  [data-tabs] { display: flex !important; }
  [data-head] { padding: 14px 16px !important; }
  [data-body] { padding: 18px 16px 90px !important; }
  [data-row] { display: flex !important; flex-wrap: wrap !important; gap: 8px 12px !important; padding: 14px 16px !important; }
  [data-row] > * { flex: none !important; }
  [data-row] > [data-grow] { flex: 1 1 100% !important; }
  [data-hrow] { grid-template-columns: minmax(0, 1fr) !important;
    grid-template-areas: 'head' 'body' 'meta' 'err' !important; row-gap: 11px !important; }
  [data-hrow] > [data-area="meta"] { border-left: none !important; padding-left: 0 !important;
    padding-top: 11px !important; border-top: 1px solid var(--border-muted) !important;
    justify-content: flex-start !important; }
  [data-rowhead] { display: none !important; }
}

/* --------------------------------------------------------------------------
   3. Шелл.
   Атрибуты data-* взяты из анатомии макета (строки 304-381 распакованного
   документа). 58 псевдо-атрибутов наведения и 2 псевдо-атрибута фокуса в
   макете — выдумка конструктора, браузер их игнорирует; здесь они стали
   настоящими :hover / :focus-visible.
   -------------------------------------------------------------------------- */
/* Полоса возврата из имперсонации (D-25, UI-контракт C8).

   ⚠️ ПОЛОСА СТОИТ НАД ШЕЛЛОМ, А НЕ ВНУТРИ НЕГО, и правила её раскладки живут
   ЗДЕСЬ, перед правилами шелла, — ровно в том порядке, в каком элементы стоят
   в разметке. Порядок элементов САМОГО шелла при этом не тронут: отступ низа
   сайдбара объявлен на конкретном элементе, и перестановка оторвала бы блок
   пользователя от низа сайдбара на всех страницах (цена уже была названа в
   прошлой фазе).

   ⚠️ `flex-wrap: wrap` — НЕ УКРАШЕНИЕ. Имя пользователя произвольно, ширина
   экрана тоже; без переноса блок возврата уехал бы за край, то есть путь назад
   стал бы недостижим ровно на узком экране. Усечение подписи стоит вдобавок и
   в обработчике: перенос спасает от края, но не от полосы в пять строк над
   каждым экраном.

   Фон, цвет и граница НЕ ОБЪЯВЛЯЮТСЯ: их даёт `.alert--warning` — те же доли,
   что в макете. Второй набор долей здесь разошёлся бы с первым молча. */
[data-impersonation] {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
  border-radius: 0;
  border-width: 0 0 1px 0;
}

[data-impersonation] .impersonation-text { min-width: 0; overflow-wrap: anywhere; }

/* `margin-left: auto` прижимает возврат вправо, пока он помещается в строку;
   после переноса он встаёт на новую строку слева и ничего не ломает. */
[data-impersonation] .impersonation-back { margin-left: auto; }

[data-shell] {
  display: grid;
  grid-template-columns: 262px minmax(0, 1fr);
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

[data-side] {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 24px;
  padding: 24px 16px;
  background: var(--surface-side);
  border-right: 1px solid var(--border-side);
  overflow: auto;
}

/* Логотип */
.brand { display: flex; align-items: center; gap: 11px; padding: 0 8px; color: var(--text); }
.brand:hover { color: var(--text); }
.brand-mark {
  position: relative; width: 34px; height: 34px; border-radius: var(--r-lg);
  background: var(--accent); display: grid; place-items: center; flex: none;
}
.brand-mark::before {
  content: ""; width: 10px; height: 10px; border-radius: var(--r-circle); background: var(--surface-side);
}
.brand-mark::after {
  content: ""; position: absolute; inset: -5px; border-radius: var(--r-2xl);
  border: 1px solid rgba(196, 132, 252, .4); border-top-color: transparent;
  animation: orbit 4.5s linear infinite;
}
.brand-name { font-family: var(--font-display); font-size: var(--fs-2xl); font-weight: 600; letter-spacing: -.02em; }

/* Навигация */
[data-nav] { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 12px; border-radius: var(--r-lg);
  font-size: var(--fs-lg); font-weight: 500;
  color: var(--text-secondary); background: transparent;
  transition: background .16s ease, color .16s ease;
}
.nav-item:hover { background: #15151d; color: var(--text); }
.nav-item:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.nav-item.is-active { background: #15151d; color: var(--text); }
.nav-dot { width: 6px; height: 6px; border-radius: var(--r-circle); flex: none; background: var(--border-control-strong); }
.nav-item.is-active .nav-dot { background: var(--accent); }
.nav-label { flex: 1; min-width: 0; }
.nav-count { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-faint); }
.nav-tag { font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: .1em; color: var(--text-faint); }
.nav-sep { height: 1px; background: var(--border-side); margin: 10px 12px 8px; }

/* Виджет доступа.
   ⚠️ `margin-top: auto` ОБЪЯВЛЕН ЗДЕСЬ, А НЕ НА [data-user]. Сайдбар — это
   колонка flex, и к низу её прижимает ИМЕННО ЭТОТ элемент, утаскивая за собой
   блок пользователя. Свойство обязано ездить вместе с селектором виджета: не
   уехав, оно оторвало бы блок пользователя от низа сайдбара на всех 26
   страницах, и правка выглядела бы безобидным переименованием (D-J).

   Шкалы у виджета нет (.access-track / .access-fill не существует и не должно
   появиться) — прохибиция P-4 UI-контракта.

   ⚠️ НИ .access-plan, НИ .access-value НЕ ПОЛУЧАЮТ `white-space: nowrap` И
   `flex: none`. Под значение остаётся 147px (262 сайдбар − 16×2 его padding −
   14×2 padding виджета − 2 border − 45 подпись «Доступ» − 8 зазор), а самое
   длинное значение «до 2026-08-20 14:30» — ≈138px. Запас узкий, и безопасным
   его делает именно отсутствие этих двух объявлений: при нехватке ширины
   значение ПЕРЕНЕСЁТСЯ внутри виджета, а не вылезет за его границу. */
[data-access] {
  margin-top: auto; display: flex; flex-direction: column; gap: 11px;
  padding: 14px; border-radius: var(--r-xl);
  background: var(--surface-pill); border: 1px solid var(--border-pill);
}
.access-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.access-plan { font-size: var(--fs-md); font-weight: 600; }
.access-value { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .06em; color: var(--text-tertiary); }
.access-link { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .06em; }

/* Блок пользователя */
[data-user] { display: flex; align-items: center; gap: 10px; padding: 0 4px; }
/* Размер задаётся пользовательским свойством --avatar-size, которое ставит
   макрос components/avatar.html. Шелл вызывает класс без свойства и получает
   макетные 30px из фолбэка. */
.avatar {
  width: var(--avatar-size, 30px); height: var(--avatar-size, 30px);
  border-radius: var(--r-circle);
  background: var(--border-input); display: grid; place-items: center;
  font-size: var(--fs-sm); font-weight: 600; color: #9a9aaa; flex: none;
}
.user-meta { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; }
.user-name { font-size: var(--fs-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.user-tz { font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--text-muted); }
.user-logout { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: .06em; }

/* Нижние табы — мобильная навигация (D-12: бургера и слайд-овера нет) */
[data-tabs] {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  gap: 4px; padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
  background: rgba(11, 11, 16, .94); backdrop-filter: blur(14px);
  border-top: 1px solid var(--border-side); overflow-x: auto;
}
.tab-item {
  flex: none; padding: 9px 13px; border-radius: var(--r-md);
  font-size: var(--fs-sm); font-weight: 500; white-space: nowrap;
  color: var(--text-secondary); background: transparent;
}
.tab-item:hover { color: var(--text); }
.tab-item[aria-current="page"] { background: #15151d; color: var(--text); }

/* Шапка раздела */
[data-main] { display: flex; flex-direction: column; min-width: 0; }
[data-head] {
  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 18px 32px;
  background: rgba(8, 8, 11, .85); backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border-soft);
}
.head-titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.head-title { margin: 0; font-size: var(--fs-h1); font-weight: 600; letter-spacing: -.025em; }
.head-subtitle { margin: 0; font-size: var(--fs-md); color: var(--text-tertiary); }
.head-subtitle:empty { display: none; }
.head-actions { margin-left: auto; display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }

/* Индикатор сессий мессенджеров (D-19). Источник — MessengerAccount.status
   из БД; это состояние сессии, а не состояние Docker-контейнера. */
.session-pill {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 13px; border-radius: var(--r-pill);
  background: var(--surface-pill); border: 1px solid var(--border-pill);
}
.session-dot { width: 7px; height: 7px; border-radius: var(--r-circle); background: var(--text-dim); }
.session-dot.is-online { background: var(--ok); animation: pulse 1.6s ease-in-out infinite; }
.session-label { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-secondary); }

/* Правил перечня воркеров дашборда здесь больше нет (задача 260826-6jq):
   сам блок снят со страницы, а стилей без разметки в проекте не держат.
   Блок пилюли сессий выше — шапка шелла, он живой и к дашборду не привязан. */

/* Контент */
[data-body] { padding: 26px 32px 60px; }

/* Имя заливки шкалы виджета вычеркнуто вместе с самой шкалой (план 05.1-04),
   имя точки перечня воркеров — вместе с карточкой воркеров дашборда
   (задача 260826-6jq): правило, адресованное несуществующему классу, — мусор,
   который следующий читатель примет за живой. Остальные три имени не тронуты. */
@media (prefers-reduced-motion: reduce) {
  .brand-mark::after, .session-dot.is-online, .animate-fade-in { animation: none !important; }
}

/* --------------------------------------------------------------------------
   4. Библиотека компонентов (План 02, D-13/D-14).
   Семантические классы для макросов app/templates/components/*.html.
   Все цвета, радиусы и кегли — только через токены раздела 1: новых
   захардкоженных значений здесь нет. Состояния наведения и фокуса выражены
   настоящими псевдоклассами — псевдо-атрибуты состояний из макета браузер
   игнорирует.
   Начиная с Плана 03 этот файл только потребляется: планы разделов классы не
   добавляют, а используют.
   -------------------------------------------------------------------------- */

/* Кнопки ------------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 16px; border-radius: var(--r-pill); border: 1px solid transparent;
  font-family: var(--font-sans); font-size: var(--fs-md); font-weight: 600;
  line-height: 1; cursor: pointer; white-space: nowrap;
  transition: filter .16s ease, background .16s ease, color .16s ease, border-color .16s ease;
}
.btn__icon { flex: none; }
.btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn--block { width: 100%; }

.btn--primary { background: var(--accent-cta); color: var(--text-on-accent); }
.btn--primary:hover { filter: brightness(1.08); color: var(--text-on-accent); }

.btn--ghost {
  background: var(--surface-pill); border-color: var(--border-control);
  color: var(--text-bright); font-weight: 500;
}
.btn--ghost:hover { background: var(--border-input); color: var(--text); }

.btn--danger {
  background: transparent; border-color: var(--border-control-strong);
  color: var(--danger); font-weight: 500;
}
.btn--danger:hover { background: var(--surface-input); color: var(--danger); }

/* Поля -------------------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: 7px; }
.field__label {
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-muted);
}
.field__input {
  padding: 12px 13px; border-radius: var(--r-lg);
  background: var(--surface-input); border: 1px solid var(--border-input);
  font-family: var(--font-sans); font-size: var(--fs-lg); color: var(--text);
  transition: border-color .16s ease;
}
.field__input::placeholder { color: var(--text-dim); }
.field__input:hover { border-color: var(--border-control); }
.field__input:focus { outline: none; border-color: var(--focus-ring); }
.field__input[disabled] { opacity: .5; cursor: not-allowed; }
.field__input--area { line-height: 1.6; resize: vertical; white-space: pre-wrap; }
.field__input--select { appearance: none; cursor: pointer; }
.field__input--center {
  text-align: center; font-family: var(--font-mono);
  font-size: var(--fs-2xl); letter-spacing: .35em;
}
.field__hint { font-size: var(--fs-sm); color: var(--text-muted); }
.field__error { font-size: var(--fs-sm); color: var(--danger); }
.field--invalid > .field__input { border-color: var(--danger); }

/* Карточка-панель --------------------------------------------------------- */
.card {
  display: block; border-radius: var(--r-2xl);
  background: var(--surface); border: 1px solid var(--border);
  overflow: hidden;
}
.card__head {
  display: flex; flex-direction: column; gap: 3px;
  padding: 15px 20px; border-bottom: 1px solid var(--border-muted);
}
.card__title { margin: 0; font-size: var(--fs-xl); font-weight: 600; letter-spacing: -.015em; }
.card__subtitle { margin: 0; font-size: var(--fs-md); color: var(--text-tertiary); }
.card__body { padding: 18px 20px; }

/* Бейдж-пилюля ------------------------------------------------------------ */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: var(--r-xs);
  font-size: var(--fs-xs); font-weight: 500; line-height: 1.3;
  border: 1px solid transparent; flex: none; white-space: nowrap;
}
.badge--mono { font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: .08em; }
.badge--success { color: var(--ok); background: color-mix(in oklab, var(--ok) 12%, transparent); }
.badge--warning { color: var(--warn); background: color-mix(in oklab, var(--warn) 12%, transparent); }
.badge--danger  { color: var(--danger); background: color-mix(in oklab, var(--danger) 12%, transparent); }
.badge--info    { color: var(--info); background: color-mix(in oklab, var(--info) 12%, transparent); }
.badge--neutral { color: var(--text-secondary); background: var(--surface-pill); border-color: var(--border-pill); }

/* Пустое состояние -------------------------------------------------------- */
.empty {
  grid-column: 1 / -1;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 48px 20px; text-align: center;
  border-radius: var(--r-2xl); background: var(--surface); border: 1px solid var(--border);
}
.empty__title { font-size: var(--fs-lg); color: var(--text-secondary); }
.empty__hint { font-size: var(--fs-md); color: var(--text-muted); }
.empty__action { margin-top: 4px; }

/* Тумблер (knob-паттерн макета) ------------------------------------------- */
.toggle { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; }
.toggle__input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.toggle__track {
  display: block; width: 38px; height: 22px; padding: 3px; flex: none;
  border-radius: var(--r-pill); background: var(--border-control);
  transition: background .2s ease;
}
.toggle__knob {
  display: block; width: 16px; height: 16px; border-radius: var(--r-circle);
  background: var(--surface-side);
  transition: transform .2s cubic-bezier(.2, .8, .2, 1);
  transform: translateX(0);
}
.toggle__input:checked + .toggle__track { background: var(--accent); }
.toggle__input:checked + .toggle__track > .toggle__knob { transform: translateX(16px); }
.toggle__input:focus-visible + .toggle__track { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.toggle__input[disabled] + .toggle__track { opacity: .45; cursor: not-allowed; }
.toggle__label { font-size: var(--fs-md); color: var(--text-secondary); }

/* Прогресс-бар ------------------------------------------------------------ */
.progress { display: flex; flex-direction: column; gap: 7px; }
.progress__label { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-tertiary); }
.progress__track {
  height: 5px; border-radius: var(--r-pill);
  background: var(--border-pill); overflow: hidden;
}
.progress__bar {
  height: 100%; border-radius: var(--r-pill); background: var(--accent);
  transform-origin: left; animation: grow .8s cubic-bezier(.2, .8, .2, 1);
}
.progress--ok .progress__bar { background: var(--ok); }
.progress--warn .progress__bar { background: var(--warn); }
.progress--danger .progress__bar { background: var(--danger); }

/* Mono-метка -------------------------------------------------------------- */
.mono { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .06em; }
.mono--upper { text-transform: uppercase; letter-spacing: .12em; }
.mono--muted { color: var(--text-muted); }
.mono--bright { color: var(--text); }
.mono--accent { color: var(--accent-link); }
.mono--ok { color: var(--ok); }
.mono--warn { color: var(--warn); }
.mono--danger { color: var(--danger); }

/* Сообщения --------------------------------------------------------------- */
.alert {
  padding: 11px 14px; border-radius: var(--r-lg);
  font-size: var(--fs-md); line-height: 1.5;
  border: 1px solid transparent;
}
.alert--error   { color: var(--danger); background: color-mix(in oklab, var(--danger) 10%, transparent); border-color: color-mix(in oklab, var(--danger) 28%, transparent); }
.alert--success { color: var(--ok); background: color-mix(in oklab, var(--ok) 10%, transparent); border-color: color-mix(in oklab, var(--ok) 28%, transparent); }
.alert--warning { color: var(--warn); background: color-mix(in oklab, var(--warn) 10%, transparent); border-color: color-mix(in oklab, var(--warn) 28%, transparent); }
.alert--info    { color: var(--info); background: color-mix(in oklab, var(--info) 10%, transparent); border-color: color-mix(in oklab, var(--info) 28%, transparent); }

/* Адаптивные примитивы строки-таблицы (UI-06) -----------------------------
   Базовые, НЕ медиазапросные правила: поведение на узких экранах уже описано
   медиазапросами раздела 2. Раскладка колонок приходит из --cols, которое
   ставит макрос components/table.html — так у каждого раздела своя сетка без
   новых классов. */
[data-rowhead] {
  display: grid; grid-template-columns: var(--cols, minmax(0, 1fr));
  gap: 14px; padding: 13px 20px;
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-muted);
}
[data-row] {
  display: grid; grid-template-columns: var(--cols, minmax(0, 1fr));
  gap: 14px; align-items: center; padding: 14px 20px;
  border-bottom: 1px solid var(--border-hair);
  transition: background .16s ease;
}
[data-row]:hover { background: var(--surface-input); }
[data-row]:last-child { border-bottom: none; }
[data-grow] { min-width: 0; }
.cell {
  display: flex; align-items: center; gap: 9px; min-width: 0;
  font-size: var(--fs-md); overflow: hidden; text-overflow: ellipsis;
}
.cell--mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
.cell--muted { color: var(--text-tertiary); }

/* Вкладки подразделов админ-панели.
   ⚠️ ИМЯ СВОЁ, А НЕ [data-tabs]: тот атрибут уже занят НИЖНЕЙ мобильной
   навигацией шелла, которая на узких экранах становится фиксированной панелью
   внизу. Переиспользование имени утащило бы вкладки подраздела вниз экрана
   вместе с ней.
   ⚠️ ПЕРЕНОС ПО СТРОКАМ, А НЕ ГОРИЗОНТАЛЬНАЯ ПРОКРУТКА: на 375px шесть
   подписей не помещаются в строку, и прокручиваемая полоса прятала бы
   последние вкладки за краем — достижимыми, но невидимыми. */
[data-subtabs] {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 4px; padding: 3px; margin-bottom: 4px;
  border-radius: var(--r-lg); background: var(--surface-input);
  border: 1px solid var(--border-input);
}
.subtab {
  padding: 8px 13px; border-radius: var(--r-md);
  font-size: var(--fs-sm); white-space: nowrap;
  color: var(--text-secondary); background: transparent;
  transition: background .16s ease, color .16s ease;
}
.subtab:hover { color: var(--text); }
/* Тот же приподнятый фон, что у активного нижнего таба шелла (.tab-item
   [aria-current]) — две полосы вкладок на одной странице обязаны отмечать
   активное одинаково, иначе читаются как разные механизмы. */
.subtab[data-subtab-active] { background: #15151d; color: var(--text); }

@media (prefers-reduced-motion: reduce) {
  .progress__bar, .toggle__knob { animation: none !important; transition: none !important; }
}

/* Сворачиваемый блок фильтров (План 04) -----------------------------------
   Единственный оставшийся сценарий Alpine после D-12, применяемый в трёх
   разделах. Свёрнутость выражена классами и медиазапросом, а НЕ привязкой
   класса к состоянию Alpine: разметка приходит с сервера уже в правильном
   состоянии, поэтому блок не мигает до инициализации скрипта. Alpine
   добавляет только .filters--open.
   Раскладка полей опирается на существующие .field и .btn — новых классов
   под элементы формы не вводится. */
.filters { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
.filters__toggle { display: none; align-self: flex-start; }
.filters__form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 9px; }
.filters__form .field { flex: 1 1 168px; min-width: 0; }
.filters__form .btn { flex: none; }

/* Свёрнут на всех ширинах — для разделов, где фильтры вторичны. */
.filters--collapsed > .filters__toggle { display: inline-flex; }
.filters--collapsed > .filters__form { display: none; }
.filters--collapsed.filters--open > .filters__form { display: flex; }

@media (max-width: 860px) {
  .filters > .filters__toggle { display: inline-flex; }
  .filters > .filters__form { display: none; }
  .filters--open > .filters__form { display: flex; }
}

/* Модальное подтверждение (D-18) ------------------------------------------
   Прецедента в макете нет: единственный position:fixed там — нижние табы.
   Компонент спроектирован с нуля на тех же токенах. Показом управляет Alpine
   через x-show; до его инициализации окно скрыто правилом [x-cloak]. */
.modal {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center; padding: 20px;
}
/* ⚠️ ПОКА ПАНЕЛЬ ОТКРЫТА, ТЕЛО НЕ ПРОКРУЧИВАЕТСЯ (план 09-13, добавка
   `scroll-lock` решением владельца). Правило выше кладёт панель поверх
   документа, но прокрутку под ней не останавливает: список за открытым окном
   продолжал ехать, а вместе с ним срабатывал `hx-trigger="revealed"` сентинела
   бесконечной прокрутки — и порция улетала, пока человек читал вопрос
   «Удалить группу?». Признак поднимает и снимает тот же Alpine, что управляет
   показом панели (components/modal.html).
   ⚠️ ЭТО ЗАКРЫТИЕ ОДНОГО ПУТИ К ОКНУ ЧЕРЕДОВАНИЯ, А НЕ САМОГО ОКНА: запрос
   порции может лететь и с момента ДО открытия панели. Класс отказа CR-01
   закрыт ФОРМОЙ КУРСОРА (`keyset`), а не этим правилом. Побочно правило чинит
   настоящий изъян интерфейса — прокрутку списка за модальным окном.
   ⚠️ СНЯТИЕ ПРИЗНАКА ИДЁТ ТЕПЕРЬ ДВУМЯ ПУТЯМИ — ПО ЧИСЛУ ПУТЕЙ УХОДА УЗЛА
   ПАНЕЛИ ИЗ ДОКУМЕНТА (план 09-17, CR-01 четвёртого круга, ветвь
   `destroy-guard`): человек закрыл — `hide()`, узел унесён внеполосным
   снятием ответа удаления — `destroy()`; строка выше про «поднимает и
   снимает тот же Alpine» верна и называется здесь неполной. */
.is-modal-open,
.is-modal-open body {
  overflow: hidden;
}

/* ⚠️ ПОДЪЁМ ЗАГОТОВОК ПЛАШКИ ОТКАЗА — БЕЗУСЛОВНЫЙ: пока заготовка видима, она
   плавает у верхнего края окна, и ни один признак состояния документа этого не
   решает (гэп G-10-7 обхода walkthrough_3 2026-09-11, план 10-51).
   ⚠️ ПРЕЖНИЙ ЗАГОЛОВОК ЭТОГО БЛОКА ОСТАЁТСЯ ЗАПИСЬЮ СВОЕГО ДНЯ И НЕ СТИРАЕТСЯ
   (идиома D-30/D-32 .planning/STATE.md): «ПОДЪЁМ ЗАГОТОВОК ПЛАШКИ ОТКАЗА НАД
   ПАНЕЛЬЮ — ТОЛЬКО ПОКА ПАНЕЛЬ ОТКРЫТА (расхождение Р-3 обхода 2026-09-09,
   план 10-33)». Чем он перерос себя — записано ниже, рядом с пунктом (в).
   Блок стои́т РЯДОМ с правилом
   блокировки прокрутки нарочно: это два правила ОДНОГО состояния, и разнесённые
   по файлу они читались бы как два независимых механизма. ⚠️ ПОСЛЕ СНЯТИЯ
   УСЛОВИЯ СОСЕДСТВО ЭТО СТАЛО ИСТОРИЧЕСКИМ, А НЕ СМЫСЛОВЫМ, и блок оставлен на
   месте нарочно: переезд оторвал бы его от собственной летописи.

   (а) ЧТО ИЗМЕРЕНО ОБХОДОМ 2026-09-09, шаг 4.4, стенд Chrome, ad_id=51,
   sched-143, режим Offline — шесть величин, а не впечатление. Текст плашки
   обрыва связи в документе ЕСТЬ и верен. Сама плашка: `position: static`,
   `z-index: auto` — обычный поток, своего контекста наложения нет. Её
   прямоугольник: `top: -322`, `bottom: -259` при `scrollY: 418`, то есть
   ЦЕЛИКОМ выше окна. Панель: `position: fixed`, `z-index: 60`, `top: 0`,
   высота 1267 — во всё окно. Верхний узел в точке плашки: `DIV.modal__overlay`
   внутри диалога `sched-del-143`. Прокрутка корня: `overflow: hidden` —
   доскроллить до плашки НЕЛЬЗЯ. Следствие для человека: он нажал «Удалить» без
   связи, панель сознательно осталась открытой (D-12), кнопка снова нажимаема —
   и ни одного объяснения, почему ничего не произошло, ему не показано.

   (б) ПОЧЕМУ ПОДЪЁМ УСЛОВЕН, А НЕ БЕЗУСЛОВЕН, И ЦЕНА БЕЗУСЛОВНОГО НАЗВАНА
   ЧУЖИМ ЗАПИСАННЫМ РЕШЕНИЕМ. Сценарий включения снятую плашку обратно НЕ
   ПРЯЧЕТ: автоматическое сокрытие потребовало бы третьего обработчика, а рамка
   вехи ограничивает число новых сценариев — допущение записано планом Фазы 8 в
   докстринге `includes/htmx_error_banner.html` и возвращается к обсуждению,
   если накопление плашек станет заметно. Безусловный подъём сделал бы это
   накопление заметным НЕМЕДЛЕННО, то есть переоткрыл бы чужое решение боковым
   следствием правки стиля, не назвав его: после одного обрыва связи человек
   получил бы прямоугольник поверх экрана до конца сессии. Условный подъём не
   трогает то решение вовсе — вне открытой панели поведение заготовок
   сегодняшнее.

   ⚠️ ЗАКЛЮЧЕНИЕ ЭТОГО ДОВОДА ОПРОВЕРГНУТО СЕДЬМЫМ КРУГОМ РЕВИЗИИ (находка
   CR-01, план 10-35), А САМ ДОВОД НЕ ВЫЧЕРКНУТ: опровергнутое помечается с
   кругом и замером, а не стирается — действующая идиома проекта, записи
   D-30/D-32 .planning/STATE.md.
   ЧЕМ ОПРОВЕРГНУТО — воспроизводимой последовательностью из трёх шагов,
   целиком в границах живого продукта: (1) сентинель бесконечной прокрутки
   поймал обрыв связи БЕЗ единого действия человека, и заготовка потеряла
   признак скрытости навсегда; (2) связь вернулась, и ничто плашку не спрятало
   — третьего обработчика нет по решению; (3) человек открыл ЛЮБУЮ из 18
   панелей подтверждения.
   ЧТО ИМЕННО НЕВЕРНО — РОВНО заключение «не трогает то решение вовсе». Правка
   не оставила поведение заготовок вне панели сегодняшним, а перенесла ТОТ ЖЕ
   прямоугольник поверх экрана в те моменты, когда открыта панель, — то есть в
   момент наивысшей цены ошибки: над кнопкой необратимого удаления, с
   утверждением о ТЕКУЩЕМ действии, которое ложно.
   ⚠️ ПОСЫЛКА ДОВОДА ВЕРНА И ОСТАЁТСЯ ДЕЙСТВУЮЩЕЙ: цена безусловного подъёма
   названа автором правильно, условность подъёма сохраняется, и снимать верную
   половину вместе с неверной нельзя.
   ⚠️ ЧЕМ ДОВОД ЗАКРЫТ. Снятие ОБЕИХ заготовок при открытии панели живёт теперь
   в методе открытия рычага app/templates/components/modal.html: третьего
   обработчика не заведено, сценарий включения не правлен ни на строку, число
   зарегистрированных обработчиков не сдвинуто.

   ⚠️ ЧЕМ СВОЙСТВО СТЕРЕЖЁТСЯ — ЗАМЕР, А НЕ ОБЕЩАНИЕ, И НАЗВАН ОН ЧЕТЫРЬМЯ
   ПУНКТАМИ, ПОТОМУ ЧТО ОДНОЙ ФРАЗЫ ЗДЕСЬ УЖЕ ОКАЗАЛОСЬ МАЛО (план 10-41).
   (1) ЧТО ИМЕННО ПРИНУЖДАЕТСЯ. Метод открытия рычага ИСПОЛНЯЕТСЯ в
   интерпретаторе JS над отрисованным выражением клиентского состояния, и после
   исполнения спрашивается КАЖДЫЙ узел отдельно: получила ли ЭТА заготовка
   признак скрытости, и НЕ получила ли его площадка приземления фокуса.
   (2) ГДЕ ЭТО ЖИВЁТ, ПОИМЁННО. Правило
   test_the_lever_clears_both_failure_banners_by_execution в модуле
   tests/test_templates/test_components.py, при нём три отрицательных контроля
   на четырёх формах матрицы мутаций девятого круга.
   (3) ЧТО ОСТАЛОСЬ ОТ ПОДСТРОЧНОГО СЛОЯ. Правило _lever_clearing_findings в
   tests/test_pages/test_shell.py НЕ УДАЛЕНО и осталось АНТИВАКУУМНЫМ: оно ловит
   переименование метода, опустевшее тело и снятие, переехавшее в закрытие.
   КОНЪЮНКЦИИ «глагол применён К ЭТОЙ заготовке» оно НЕ УТВЕРЖДАЕТ и прямо
   говорит об этом в своём докстринге.
   (4) ЧТО НЕ ПРИНУЖДАЕТСЯ НИЧЕМ МАШИННЫМ. ОТРИСОВКА браузером. Она остаётся
   шагу 4.4 ручного обхода 10-UAT.md, и объявлять его пройденным по зелени
   правил из пунктов 2 и 3 НЕЛЬЗЯ.

   ⚠️ ПРЕЖНЯЯ ФОРМУЛИРОВКА ЭТОГО АБЗАЦА ПРИВОДИТСЯ ДОСЛОВНО И ПОМЕЧАЕТСЯ
   ОПРОВЕРГНУТОЙ, А НЕ СТИРАЕТСЯ (идиома D-30/D-32 .planning/STATE.md):
   «Свойство стережётся правилом по ИСХОДНИКУ рычага в модуле шелла, а не
   отрисовкой браузером.»
   ЧЕМ ОПРОВЕРГНУТА — восьмым кругом ревизии (находка CR-01) и девятым кругом
   верификации (гэп V9-01), исполнением, а не прочтением. Правило по исходнику
   вело по телу метода ДВА НЕЗАВИСИМЫХ подстрочных поиска и конъюнкции не
   выражало: на копии рычага, где ОБЕ заготовки НАЗВАНЫ, а признак скрытости
   поставлен площадке приземления, оно давало НОЛЬ находок и проходило ЗЕЛЁНЫМ.
   То есть дерево, на котором дефект CR-01 вернулся ЦЕЛИКОМ, проходило
   единственный заведённый под него гейт без единого красного.
   КОГДА — 2026-09-10. ЧЕМ ЗАКРЫТА — планом 10-41.
   ⚠️ ВЕРНАЯ ПОЛОВИНА ПРЕЖНЕЙ ФОРМУЛИРОВКИ ОСТАЁТСЯ ДЕЙСТВУЮЩЕЙ И ВМЕСТЕ С
   НЕВЕРНОЙ НЕ СНИМАЕТСЯ: отрисовка браузером не стереглась тогда и не
   стережётся сейчас — она названа пунктом 4 выше.

   ⚠️ НИ ОДНОГО ОБЪЯВЛЕНИЯ CSS В ТЕЛЕ ЭТОГО КОММЕНТАРИЯ НЕ НАБИРАЕТСЯ, И ЭТО НЕ
   ОПРЯТНОСТЬ, А ЗАМЕРЕННЫЙ ЗАПРЕТ (REVIEW-8/WR-05). Дословный кусок блока
   подъёма, которым доктóрит его отрицательный контроль, начинается НЕ с
   фигурной скобки, а с конца предыдущего блока — то есть ВБИРАЕТ этот
   комментарий целиком (замер: длина куска 7487 символов, первые два символа —
   открывающий комментарий). Контроль сличает число вхождений объявления
   положения в этом куске с ЕДИНИЦЕЙ, и вторая копия, набранная как объявление,
   уронила бы его отказом о прозе вместо отказа о блоке. Поэтому свойство
   положения и его значение вне потока называются здесь СЛОВАМИ — тем же
   приёмом, каким в app/templates/includes/htmx_error_banner.html число слоя
   названо отношением, а не величиной, и где запрет второй копии записан прямо.

   (в) ЦЕНА САМОЙ УСЛОВНОСТИ, НАЗВАННАЯ ПРЯМО И НЕ СМЯГЧЁННАЯ. После закрытия
   панели плашка возвращается в поток и может снова оказаться выше окна. Но там
   прокрутка ЖИВАЯ, и слово замера «недостижима» перестаёт быть верным:
   недостижимой её делала ПАРА «фиксированная панель поверх» + `overflow:
   hidden`, и подъём снимает обе половины пары.

   ⚠️ ВЫВОД ПУНКТА (в) ОПРОВЕРГНУТ ЗАМЕРОМ, А ТЕКСТ ЕГО ОСТАЁТСЯ ДОСЛОВНО
   (идиома D-30/D-32). Запись называет четыре вещи и ни одной не смягчает.
   (1) ЧТО ИМЕННО НЕВЕРНО — ВЫВОД, А НЕ ПОСЫЛКА. Прокрутка после ухода панели
   действительно живая, и плашка действительно достижима прокруткой; неверно
   заключение, что этого довольно. Замер: обход `walkthrough_3` 2026-09-11,
   стенд ad_id=59 / sched-165, шаг 2.8, НАСТОЯЩИЙ обрыв транспорта (событие
   отказа отправки, статус 0), а не подмена транспорта в документе. Плашка
   `htmx-failure-network` поднята верно и текст её верен, но её прямоугольник —
   `top −701`, `left 294`, 944×63 — при прокрутке окна 797, то есть на 701 px
   ВЫШЕ окна; положение у неё обычное для потока, слоя своего нет, и запрос
   верхнего узла в её точке её НЕ возвращает. Глазной вердикт по снимку экрана:
   объяснения на экране НЕТ. Прокруткой вверх она достигается (верх 96) — но
   повода прокручивать человеку никто не дал: на экране устаревший редактор с
   нарисованной карточкой и сводкой «РАСПИСАНИЯ 1» при нуле на сервере.
   (2) ПОЧЕМУ ЭТОГО НЕ УВИДЕЛ ПЕРЕЗАМЕР ТОГО ЖЕ ДНЯ. Он снял шаг 2.8 на стенде
   ad_id=57 и записал прямоугольник В ОКНЕ (верх 96). Та запись ВЕРНА и не
   вычёркивается: там положение прокрутки случилось у верхнего края, и плашка
   попала в окно САМА. Признак зависит от ПРОКРУТКИ, а не от дерева, и потому
   один положительный замер его не закрывает — обход поймал обе стороны этой
   случайности за один день.
   (3) ЧТО СНЯЛО ОСНОВАНИЕ УСЛОВНОСТИ — И БЕЗ ЭТОЙ ПОЛОВИНЫ ПРАВКА БЫЛА БЫ
   РАЗМЕНОМ, А НЕ ПОЧИНКОЙ. До плана 10-49 у ВИДИМОЙ плашки был РОВНО ОДИН
   выход с экрана — открытие панели подтверждения; безусловный подъём отгрузил
   бы полосу, висящую над каждым экраном до ближайшего открытия панели, то есть
   заплатил бы за достижимость записи отказа её несменяемостью. План 10-49 завёл
   ВТОРОЙ и главный выход: гашение обеих заготовок по УСПЕШНОМУ обмену. Страх
   «повиснет до конца сессии», которым условность и оправдывалась (пункт (б)
   выше), перестал иметь предмет. Порядок двух планов — сначала выход, потом
   подъём — обязателен, и обратный отгрузил бы состояние хуже обоих.
   (4) НОВАЯ ЦЕНА, НАЗВАННАЯ ФАКТОМ, А НЕ ЭПИТЕТОМ. Обе заготовки, показанные
   ОДНОВРЕМЕННО, встают в одну точку и накрывают друг друга: величины у них
   общие, и вторая закрывает первую. Окно это было и прежде — при открытой
   панели, — настоящая правка его РАСШИРЯЕТ до «всегда, пока обе видимы».
   Предмета гэпа G-10-7 оно не касается, ПОЧИНКОЙ ЗДЕСЬ НЕ ЗАКРЫВАЕТСЯ и
   передаётся следующему кругу НАЗВАННЫМ, а не умолчанным
   (`T-10-51-04`, `accept`).

   ⚠️ ДВЕ НОВЫЕ ЦЕНЫ, ПРИШЕДШИЕ СО СНЯТИЕМ ЗАГОТОВОК ПРИ ОТКРЫТИИ ПАНЕЛИ (план
   10-35), НАЗВАНЫ ЗДЕСЬ ФАКТОМ, А НЕ ЭПИТЕТОМ.
   (i) НАСТОЯЩИЙ отказ, случившийся ДО открытия панели и человеком не
   прочитанный, при открытии панели СТИРАЕТСЯ с экрана и обратно НЕ
   ВОЗВРАЩАЕТСЯ: снятая заготовка получает признак скрытости и теряет его
   только следующим отказом. Обмен принят сознательно — ложное утверждение о
   ТЕКУЩЕМ действии перед необратимым удалением стоит дороже потерянного
   уведомления о прошлом: второе человек мог и не читать, первое он читает над
   кнопкой.
   (ii) Отказ ФОНОВОГО запроса — тот же сентинель прокрутки, автосохранение, —
   пришедший ПОКА панель уже открыта, по-прежнему поднимается поверх этой
   панели, и говорит она НЕ об этом действии. Окно сужено с «до конца сессии»
   до «пока открыта панель»: на порядки у́же, но НЕ ноль. Наблюдаемо оно только
   рантаймом браузера, поэтому правилами не стережётся и названо здесь, а не
   оставлено следующему кругу.

   (г) СЛОЙ 70 ВЫБРАН ЗАМЕРОМ, А НЕ КРУГЛЫМ СЛОВОМ. Все объявленные слои этой
   таблицы: 60 у `.modal`, 20 у `[data-tabs]`, 5 у `[data-head]`, 1 у
   `.modal__panel` внутри её же контекста. Наибольший — 60; 70 стои́т выше него
   с запасом на одно промежуточное значение. ⚠️ ЧИСЛО НЕ ПОВТОРЯЕТСЯ В ПРАВИЛЕ
   СУИТЫ: правило СЛИЧАЕТ два слоя, прочитанных из этой таблицы, и не знает
   наперёд ни одного из них.
   ЦЕПЬ ПРЕДКОВ ТОЧКИ ВКЛЮЧЕНИЯ ЗАМЕРЕНА ПО ШЕЛЛАМ, а не выведена: основной —
   тело документа → `[data-shell]` (base.html:79) → `[data-main]` (:209) →
   `[data-body]` (:232) → включение (:242); шелл авторизации — тело документа →
   `[data-auth-shell]` (auth_base.html:33) → `.auth-card` (:34) → включение
   (:49). Липкая шапка `[data-head]` со слоем 5 — СОСЕД, а не предок: она
   закрывается на base.html:230, а `[data-body]` открывается на :232. Цепь
   проверена на свойства `transform`, `perspective`, `filter`,
   `backdrop-filter`, `contain`, `will-change`, `isolation`, `mix-blend-mode` и
   `opacity`: сегодня ни одного из них ни один предок не объявляет.
   ⚠️ ПРЕДУПРЕЖДЕНИЕ ПРЯМОЕ: свойство, порождающее содержащий блок, дописанное
   ЛЮБОМУ из этих предков, отнимет у подъёма ОКНО — плашка станет
   позиционироваться внутри предка, а её слой запрётся в его собственном
   контексте наложения, и глазом это неотличимо от работающего подъёма.

   (д) ЧЕМ ЭТО СТЕРЕЖЁТСЯ — ПЯТЬ правил в `tests/test_pages/test_shell.py`, и
   перечень обязан оставаться полным:
   `test_the_failure_banner_is_declared_above_the_panel`
   (порядок слоёв, оба числа читаются из этой таблицы и сличаются между собой;
   ⚠️ ПРЕЖНЕЕ ИМЯ ЭТОГО ПРАВИЛА — `…_above_the_panel_while_it_is_open` — пережило
   свой предмет вместе с условностью и названо здесь, чтобы читатель, пришедший
   со сводкой 10-33, нашёл правило),
   `test_no_ancestor_of_the_failure_banner_traps_the_fixed_layer` (чистота цепи
   предков),
   `test_the_failure_banner_lift_is_unconditional` (БЕЗУСЛОВНОСТЬ подъёма: часть
   селектора равна адресу заготовки, имени ни одного признака состояния правило
   не знает — условность, вернувшаяся под ЛЮБЫМ именем, краснит его; план 10-51),
   `test_the_failure_banner_lift_declares_no_display_mode` (запрет последнего
   абзаца ниже, получивший зубы; план 10-51) и
   `test_the_lever_raises_the_scroll_lock_flag` (рычаг поднимает признак
   блокировки прокрутки ГЛАГОЛОМ — переехало сюда из правила порядка слоёв, где
   держалось условностью подъёма; собственный предмет у него другой и никуда не
   делся: прокрутка списка за открытым окном, план 09-13).
   ⚠️ ГРАНИЦА ДОКАЗАННОГО НАЗВАНА ЗДЕСЬ, А НЕ ОСТАВЛЕНА ЧИТАТЕЛЮ: все пять
   правил утверждают ОБЪЯВЛЕНИЯ этой таблицы и ПОРЯДОК объявленных
   слоёв, а НЕ то, что браузер нарисовал плашку поверх панели или в окне.
   Отрисовка остаётся шагам 4.4 и 2.8 ручного обхода `10-UAT.md`, и пункт
   `missing[1]` записи G-10-7 — «правило, стерегущее, что ПРЯМОУГОЛЬНИК плашки
   лежит в окне при рабочем положении прокрутки» — настоящей правкой НЕ ЗАКРЫТ:
   прямоугольник есть произведение раскладки на положение прокрутки, а ни движка
   раскладки, ни браузерного привода в суите нет ни одного (замер 2026-09-11).

   ⚠️ СПОСОБА ОТОБРАЖЕНИЯ ЭТОТ БЛОК НЕ ОБЪЯВЛЯЕТ НИ В КАКОМ ВИДЕ, И ЭТО НЕСУЩЕЕ
   СВОЙСТВО, А НЕ ПРОПУСК: скрытость обеих заготовок держит атрибут `hidden`
   через таблицу стилей пользовательского агента, и любое объявление `display`
   здесь перебило бы её — на каждом открытии панели человек получал бы две
   пустые подложки вместо ничего. ⚠️ ЦЕНА ЭТОГО ЗАПРЕТА ВЫРОСЛА ВМЕСТЕ С
   БЕЗУСЛОВНОСТЬЮ ПОДЪЁМА (план 10-51): «две пустые подложки на каждом открытии
   панели» стало «две пустые подложки НА КАЖДОМ ЭКРАНЕ ПРОЕКТА, всегда», — и
   ровно поэтому запрет, до сих пор державшийся этой прозой и ничем больше,
   получил собственное правило, названное пунктом (д). */
#htmx-failure-server,
#htmx-failure-network {
  position: fixed;
  top: var(--failure-banner-top, 12px); left: 0; right: 0;
  z-index: 70;
  width: min(560px, calc(100% - 24px));
  margin-left: auto; margin-right: auto;
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: 0 24px 60px rgba(8, 8, 11, .6);
}
/* Стопка двух заготовок (план 10-56, находка UI-2 ревизии 2026-09-12) -------
   ПОВОД ЗАМЕРЕН, А НЕ ПРЕДПОЛОЖЕН. Оба узла делили блок выше с ОДНИМ литералом
   вертикального положения, то есть были объявлены В ОДНОМ прямоугольнике. Отказ
   сервера, за которым последовал обрыв отправки, показывает ОБА, и тот, что
   ниже по порядку узлов, красится поверх первого: человек теряет одну из ДВУХ
   РАЗНЫХ инструкций восстановления — а они разные именно потому, что разные и
   следующие его действия. Окно 76 журнала наложение записало; цена наложения
   после БЕЗУСЛОВНОГО подъёма (план 10-51) названа там прямо — ТЕПЕРЬ ВСЕГДА, а
   не только при открытой панели.

   (а) ПОЧЕМУ СВОЙСТВО-ВЕЛИЧИНА, А НЕ ВТОРОЕ ОБЪЯВЛЕНИЕ ПОЛОЖЕНИЯ ПО КЛАССУ.
   Объявление `top` по КЛАССУ проиграло бы объявлению `top` по ИДЕНТИФИКАТОРУ
   по весу селектора — и проиграло бы МОЛЧА: таблица выглядела бы верной, а
   прямоугольник остался бы один. Свойство-величина разрешается по СВОЕМУ
   каскаду, и объявление по классу в нём побеждает, потому что блок подъёма
   выше этой величины не объявляет ВОВСЕ и лишь читает её.

   (б) ПОЧЕМУ КЛАСС, А НЕ ВТОРОЙ БЛОК С АДРЕСОМ ЗАГОТОВКИ. Пять действующих
   правил подъёма отбирают блоки по ВХОЖДЕНИЮ адреса заготовки в селектор и
   требуют РОВНО ОДНОГО такого блока на заготовку. Второй блок с адресом
   покрасил бы их разом — и не за дефект, а за ФОРМУ правки. Селектор блока
   подъёма поэтому не тронут НИ НА СИМВОЛ, а смещение адресуется классом.

   (в) ПОЧЕМУ НЕТ ЭЛЕМЕНТА-ОБЁРТКИ, КОТОРЫЙ ПРЕДЛАГАЛА UI-РЕВИЗИЯ. Обёртка
   встала бы между заготовкой и окном предком, которого нет в объявленной
   вселенной предков суиты, и правило чистоты цепи продолжало бы ЗЕЛЕНЕТЬ,
   СМОТРЯ НА ЧАСТЬ ЦЕПИ. Это ослабление СЛЕПОТОЙ, и оно дороже открытой
   красноты. По той же причине здесь нет и способа отображения: запрет пункта
   (д) выше эта правка СОБЛЮДАЕТ, а не пересматривает.

   (г) ТРЕТИЙ БЛОК И ЕСТЬ ОТСУТСТВИЕ ДЫРЫ: когда первая заготовка скрыта,
   вторая обязана стоять там, где стояла бы ОДНА, — иначе человек с обрывом
   связи получал бы плашку, отъехавшую от края экрана без причины. Вес третьего
   блока выше веса второго на один простой селектор, поэтому ПОРЯДОК объявлений
   в файле здесь ничего не решает, и переставлять их местами, «починяя», не
   нужно: исход решает вес. Оба веса сличает правило
   `test_a_single_shown_banner_keeps_the_base_offset`.

   (д) ШАГ СТОПКИ — АРИФМЕТИКА, А НЕ ЧИСЛО ИЗ ГОЛОВЫ, и слагаемые названы
   поимённо: наименьшее достаточное смещение есть наибольшая мыслимая высота
   ПЕРВОЙ заготовки плюс зазор. Высота собрана ИЗ ОБЪЯВЛЕНИЙ `.alert` ЭТОЙ ЖЕ
   таблицы — вертикальные отступы с двух сторон (11px × 2 = 22px), толщина рамки
   с двух сторон (1px × 2 = 2px) и межстрочие, умноженное на кегль и на число
   строк (1.5 × 13px × 3 = 58.5px), — итого 82.5px, плюс зазор 8px, итого
   90.5px. Число строк взято С ЗАПАСОМ: текст плашки отказа сервера на самом
   узком ОБЪЯВЛЕННОМ рабочем окне (медиазапрос 400px, ширина плашки 376px, поле
   набора 346px при кегле 13px) укладывается в ДВЕ строки, и третья — запас на
   правку редакции. Объявлено 96px, то есть не меньше посчитанного.
   ⚠️ ПОПРАВКА 2026-09-25 (план 15-19, UI-ревью пункт 6): после компенсации
   `padding-right: 38px` (план 15-07) поле набора 376 − 2 − 14 − 38 = 322px
   (рамка с двух сторон, отступ слева, компенсация справа); текст плашки отказа
   сервера (55 знаков) укладывается в две строки и при нём — 644px поля на две
   строки при ≈ 370–400px набора (оценка по кеглю, не замер отрисовки), —
   третья строка запаса остаётся, объявленные 96px
   достаточны; прежнее число 346px было верно на день записи.
   ⚠️ ВТОРОЙ КОПИИ ВЕЛИЧИН `.alert` ЗДЕСЬ НЕТ И НЕ БУДЕТ: числа выше суть
   ЛЕТОПИСЬ ЗАМЕРА, а достаточность шага ПРОВЕРЯЕТ правило
   `test_the_stack_step_clears_the_tallest_first_banner`, читающее `.alert` из
   этой же таблицы. Увеличили отступ плашки — правило покраснеет, а не смолчит.

   ⚠️ ГРАНИЦА ДОКАЗАННОГО НАЗВАНА ЗДЕСЬ, А НЕ ОСТАВЛЕНА ЧИТАТЕЛЮ: правила
   утверждают ОБЪЯВЛЕНИЯ этой таблицы, а НЕ то, что браузер нарисовал два
   НЕПЕРЕКРЫТЫХ ПРЯМОУГОЛЬНИКА. Прямоугольник есть произведение раскладки на
   положение прокрутки, движка раскладки в суите нет, браузерного привода нет ни
   одного. Отрисовка остаётся шагам 2.8 и 4.4 ручного обхода, и объявлять их
   пройденными по зелени правил НЕЛЬЗЯ — окно 77 журнала записывает, чем такая
   подмена уже обошлась фазе. */
.failure-stack {
  --failure-banner-top: 12px;
  --failure-stack-step: 96px;
}
.failure-stack + .failure-stack {
  --failure-banner-top: calc(12px + var(--failure-stack-step));
}
.failure-stack[hidden] + .failure-stack {
  --failure-banner-top: 12px;
}
/* Выход у плашки отказа — орган снятия (план 10-57, находка UI-1 ревизии
   2026-09-12) -------------------------------------------------------------
   ПОВОД ЗАМЕРЕН, А НЕ ПРЕДПОЛОЖЕН. Третий обработчик включения гасит обе
   заготовки ТОЛЬКО на удавшемся обмене (ранний выход по отсутствию признака
   успеха). Человек, у которого оборвалась связь на экране, не делающем больше
   ни одного удавшегося обмена, получал пришпиленный к верху прямоугольник во
   всю ширину до перезагрузки страницы. Это первый блокер UI-ревизии и главное,
   чем держалась её оценка Pillar 6 (2/4).

   (а) ПОЧЕМУ ОРГАН БЕЗ ОБРАБОТЧИКА. Критерий 3 роадмапа («фаза не добавляет
   новых строк JS») зелен под ИМЕНОВАННЫМИ записями отступления, и НОВАЯ
   регистрация обработчика в файле заготовок прямо объявлена действующей записью
   как требующая НОВОГО решения владельца. Ответ владельца от 2026-09-13 назвал
   ветвь `A` — «снятие без регистрации»: состояние органа переключает БРАУЗЕР,
   скрытие выражено объявлением ниже, и ни одно из пяти чисел гейта критерия 3 не
   сдвинуто. Форма условного селектора по состоянию вложенного элемента
   управления НАСЛЕДУЕТСЯ у `.chip:has(.chip__input:checked)` этой же таблицы, а
   не изобретается.

   (б) ПОЧЕМУ БЛОК АДРЕСУЕТ ЗАГОТОВКУ ПО КЛАССУ СТОПКИ, А НЕ ПО ЕЁ АДРЕСУ.
   `_selector_lifts_banner` отбирает блоки подъёма по ВХОЖДЕНИЮ адреса заготовки
   в селектор и требует РОВНО ОДНОГО такого блока; блок с адресом покрасил бы
   пять действующих правил подъёма за ФОРМУ правки, а не за дефект. По тому же
   основанию таблица не знает и идентификаторов самих органов снятия: селектор
   `#htmx-failure-server-close` нёс бы подстроку `#htmx-failure-server`.

   (в) ПОЧЕМУ ЗДЕСЬ ЕСТЬ СПОСОБ ОТОБРАЖЕНИЯ, А В БЛОКЕ ПОДЪЁМА ЕГО НЕТ, И
   ПОЧЕМУ ЗАПРЕТ ПУНКТА (д) ВЫШЕ ЭТИМ НЕ СНЯТ. Предмет того запрета —
   объявление, ПЕРЕБИВАЮЩЕЕ атрибут скрытия: заготовка, скрытая атрибутом,
   стала бы видимой пустой подложкой. Значение «нет» способно только ДОБАВИТЬ
   скрытости и не способно её отнять — оно перебить атрибут не может НИ В КАКУЮ
   сторону. Различие принуждается собственным правилом
   `test_no_banner_rule_declares_a_display_mode_that_shows`, читающим ВСЕ блоки,
   достигающие узла заготовки, и допускающим ровно одно значение; действующее
   правило блока подъёма остаётся в силе НАРАВНЕ с ним.

   (г) ГРАНИЦА ДОКАЗАННОГО НАЗЫВАЕТСЯ ЗДЕСЬ, А НЕ ОСТАВЛЯЕТСЯ ЧИТАТЕЛЮ. Правила
   утверждают ОБЛАСТЬ СЕЛЕКТОРА и ОБЪЯВЛЕННОЕ ЗНАЧЕНИЕ, а не то, что браузер
   убрал прямоугольник по щелчку мышью или по нажатию пробела. Движка раскладки
   в суите нет, браузерного привода нет ни одного; отрисовка остаётся шагам 2.8
   и 4.4 ручного обхода, и объявлять их пройденными по зелени правил НЕЛЬЗЯ.

   ⚠️ НАЗВАННОЕ СЛЕДСТВИЕ, КОТОРОЕ ЭТА ПРАВКА НЕ ЧИНИТ И НЕ ПРЯЧЕТ: блок
   «отсутствия дыры» выше ключáется на АТРИБУТ скрытия, поэтому снятая органом
   (а не атрибутом) ПЕРВАЯ заготовка оставляет вторую на смещённом месте.
   Починка требует четвёртого блока, объявляющего `--failure-banner-top`, а
   разбор `_stack_blocks` относит его к роли смещения — и правила стопки плана
   10-56 покраснели бы за ФОРМУ правки. Следствие записано окном журнала, а не
   умолчано.

   ⚠️ ПРИНЯТОЕ СЛЕДСТВИЕ ВЕТВИ A: СНЯТИЕ КЛАВИШЕЙ ТЕРЯЕТ ФОКУС. Пробел на органе
   в фокусе отмечает флажок, блок ниже скрывает ВСЮ стопку объявлением
   `display: none`, и элемент в фокусе исчезает вместе с ней: фокус падает на
   `<body>`, то есть в начало страницы (WCAG 2.4.3). Клавиша Enter флажок не
   переключает вовсе — снимает только пробел. Основание — решение владельца
   `chubav` 2026-09-25 (Г-3, «записать следствием»): ветвь `A` остаётся, а
   починка требует нового обработчика или замены органа на `<button>`, то есть
   НОВОГО решения владельца, а не правки плана. Сторож —
   `test_boundary_the_keyboard_focus_and_the_checkbox_role_consequences_are_guarded_not_fixed`
   (`tests/test_templates/test_banner_dismiss.py`): и молчаливая починка, и
   молчаливое снятие этой записи его краснят.

   ⚠️ ПРИНЯТОЕ СЛЕДСТВИЕ ВЕТВИ A: ОРГАН ОБЪЯВЛЯЕТСЯ ФЛАЖКОМ. Вспомогательная
   технология называет его ролью «флажок» с состоянием «не отмечен» —
   переключатель там, где на деле действие; ролевая половина записи D-18.3 НЕ
   починена (имя и место исполнены планом 15-07, вид — нет). Основание — решение
   владельца `chubav` 2026-09-25 (Г-3, «записать следствием»): ветвь `A`
   остаётся, а починка требует замены органа на `<button>` с обработчиком, то
   есть НОВОГО решения владельца, а не правки плана. Сторож тот же. */
.banner-dismiss {
  appearance: none; -webkit-appearance: none;
  position: absolute; top: 6px; right: 6px; z-index: 1;
  width: 24px; height: 24px; margin: 0; padding: 0;
  border: 0; border-radius: var(--r-sm);
  background: transparent; color: var(--text-tertiary);
  font: inherit; line-height: 24px; text-align: center;
  cursor: pointer;
}
.banner-dismiss::before { content: "\00d7"; font-size: var(--fs-md); }
.banner-dismiss:hover { color: var(--text); }
.banner-dismiss:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.failure-stack:has(> .banner-dismiss:checked) { display: none; }
/* Компенсация перекрытия содержимого органом снятия (план 15-07, находка 3
   долга Фазы 10, запись D-18.3) -------------------------------------------
   ПОВОД ЗАМЕРЕН, А НЕ ПРЕДПОЛОЖЕН. Коробка органа снятия (блок выше:
   `position: absolute; top: 6px; right: 6px; width: 24px; height: 24px`)
   лежит поверх правого края содержимого плашки: замер 2026-09-14 — коробка
   885→909 при содержимом `.alert`, кончающемся на 900, перекрытие 15 px, и
   компенсации `padding-right` не было ни в одном правиле стопки.

   ВЕЛИЧИНА ВЫВЕДЕНА, А НЕ ПОДОБРАНА: ширина органа 24px + его отступ справа 6px
   + зазор 8px = 38px (рамка плашки в 1px в сумму не входит и лишь прибавляет
   пиксель к видимому зазору). Правило
   `test_the_clearance_is_derived_from_the_dismiss_box`
   (`tests/test_templates/test_banner_dismiss.py`) читает обе слагаемые из блока
   органа ЭТОЙ ЖЕ таблицы: правка коробки органа без правки этого объявления его
   краснит.

   ⚠️ НАРИСОВАННАЯ ПОЛОВИНА ЗАМЕРЕНА И ОКАЗАЛАСЬ У́ЖЕ ОБЪЯВЛЕННОЙ: видимого
   столкновения текста с крестиком НЕТ (снимок 2026-09-14, окно 1280 px) —
   первая строка сообщения обрывается переносом раньше крестика. Блок объявляет
   компенсацию ПО ЗАМЕРУ КОРОБКИ, а не по наблюдённому столкновению.

   ФОРМА ПРАВКИ ОГРАНИЧЕНА ТРЕМЯ ДЕЙСТВУЮЩИМИ ПРАВИЛАМИ, И ВСЕ ТРИ СОБЛЮДЕНЫ:
   блок адресует содержимое ПО КЛАССУ стопки, а не по адресу заготовки (пункт
   (б) выше — блок подъёма остаётся единственным); способа отображения не
   объявляет (пункт (в) выше); величины вертикального положения заготовки не
   объявляет — названное выше следствие этой правкой НЕ чинится и передано
   дальше записью.

   ⚠️ ГРАНИЦА ДОКАЗАННОГО: правила утверждают ОБЪЯВЛЕНИЕ и его величину, а не то,
   что браузер нарисовал текст левее крестика. Отрисовка остаётся глазам, и
   дословно по абзацу стопки выше — тому, что замыкает пункт (д) шага стопки и
   открывается словами «⚠️ ГРАНИЦА ДОКАЗАННОГО НАЗВАНА ЗДЕСЬ, А НЕ ОСТАВЛЕНА
   ЧИТАТЕЛЮ: правила утверждают ОБЪЯВЛЕНИЯ этой таблицы»: «объявлять их пройденными по зелени правил НЕЛЬЗЯ — окно 77 журнала записывает, чем такая подмена уже обошлась фазе».
   Летопись указателя: прежде здесь стояло «(строки 1255-1258)» — номера строк сдвигает первая же правка выше (ревью IN-04, план 15-19; прецедент `test_the_lever_note_points_by_name_and_not_by_line_number`). */
.failure-stack > .alert { padding-right: 38px; }
.modal__overlay {
  position: absolute; inset: 0;
  background: rgba(8, 8, 11, .72); backdrop-filter: blur(6px);
}
.modal__panel {
  position: relative; z-index: 1;
  width: min(420px, 100%);
  display: flex; flex-direction: column; gap: 14px;
  padding: 22px; border-radius: var(--r-2xl);
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: 0 24px 60px rgba(8, 8, 11, .6);
  animation: rise .2s ease both;
}
.modal__panel:focus { outline: none; }
.modal__title { margin: 0; font-size: var(--fs-xl); font-weight: 600; letter-spacing: -.015em; }
.modal__form { display: flex; flex-direction: column; gap: 14px; }
.modal__text { margin: 0; font-size: var(--fs-md); line-height: 1.55; color: var(--text-secondary); }
.modal__actions { display: flex; justify-content: flex-end; gap: 9px; flex-wrap: wrap; }

@media (prefers-reduced-motion: reduce) {
  .modal__panel { animation: none !important; }
}

/* Auth-шелл (D-08) --------------------------------------------------------
   Компоновка придумана: макет экраны авторизации не покрывает. Визуальный
   язык — те же токены, что и у основного шелла. Сайдбара, навигации и
   нижних табов здесь нет. */
[data-auth-shell] {
  min-height: 100vh;
  display: grid; place-items: center;
  padding: 32px 16px;
  font-family: var(--font-sans); color: var(--text);
}
.auth-card {
  width: min(380px, 100%);
  display: flex; flex-direction: column; gap: 14px;
  padding: 28px 24px 26px;
  border-radius: var(--r-2xl);
  background: var(--surface); border: 1px solid var(--border);
  animation: rise .32s ease both;
}
/* Постоянный якорь шага (Фаза 14, план 14-01, D-06). Две причины правил ниже.
   1. `display: contents` — якорь не становится ОДНИМ флекс-элементом карточки:
      его дети (подзаголовок, плашки, формы, нижние ссылки) остаются элементами
      колонки и сохраняют промежутки `gap: 14px` (прецедент —
      `[data-identity-meta] > #user-block-badge`).
   2. `order` у бренда и подзаголовка — ПОРЯДОК DOM НЕСУЩИЙ: области
      уведомления стоят РАНЬШЕ якоря, иначе первая же подмена стёрла бы код
      исхода (RESEARCH Pitfall 5). Визуальный порядок задаёт CSS: подзаголовок
      сразу под брендом, как до переноса, а пустые области уведомления — ниже
      него; без этого между брендом и подзаголовком встали бы два пустых
      флекс-элемента (около 28px). Правило `.auth-form` не меняется: класс
      колонки стоит теперь на внутреннем узле формы. */
.auth-step { display: contents; }
.auth-brand { display: flex; align-items: center; justify-content: center; gap: 9px; order: -2; }
.auth-card .auth-subtitle { order: -1; }
.auth-brand__mark {
  width: 22px; height: 22px; border-radius: var(--r-sm); flex: none;
  background: var(--accent);
}
.auth-brand__name {
  font-family: var(--font-display); font-size: var(--fs-2xl);
  font-weight: 700; letter-spacing: -.02em;
}
.auth-subtitle {
  margin: 0; text-align: center;
  font-size: var(--fs-md); color: var(--text-tertiary);
}
.auth-subtitle:empty { display: none; }
.auth-note {
  margin: 0; font-size: var(--fs-md); line-height: 1.55; color: var(--text-secondary);
}
.auth-note strong { color: var(--text); font-weight: 600; }
.auth-form { display: flex; flex-direction: column; gap: 14px; margin: 4px 0 0; }
.auth-form--secondary { margin-top: 0; }
.auth-form__row { display: flex; justify-content: flex-end; font-size: var(--fs-md); }
.auth-foot {
  margin: 6px 0 0; text-align: center;
  font-size: var(--fs-md); color: var(--text-tertiary);
}

/* --------------------------------------------------------------------------
   5. Разделы «История», «Дашборд» и «Профиль» (План 05).

   Дополнение файла санкционировано планом: медиазапросы 1080px и 860px из
   раздела 2 уже перестраивают [data-hrow] и его блок [data-area="meta"], но
   БАЗОВЫХ правил этого примитива в файле не было — перестраивать было нечего.
   Все значения выражены токенами раздела 1; ни одного нового захардкоженного
   цвета, радиуса или кегля.
   -------------------------------------------------------------------------- */

/* Колонка записей истории. Сентинел бесконечной прокрутки — последний элемент
   ЭТОГО контейнера. */
[data-hlist] { display: flex; flex-direction: column; gap: 10px; }

/* Запись истории — собственный адаптивный примитив раздела: он схлопывается в
   одну колонку раньше строк-таблиц, на 1080px. */
[data-hrow] {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  grid-template-areas: 'head meta' 'body meta' 'err err';
  column-gap: 22px; row-gap: 10px;
  padding: 14px 18px;
  border-radius: var(--r-2xl);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 2px solid var(--border-control);
  transition: border-color .2s ease, transform .2s ease;
}
[data-hrow]:hover { transform: translateY(-2px); }

/* Цветной левый акцент по статусу отправки. В макете он приходил инлайн-стилем
   с подставленным цветом; здесь — атрибут, потому что селекторов по подстроке
   инлайн-стиля в этом файле не осталось и появиться не должно. */
[data-hrow][data-status="ok"] { border-left-color: var(--ok); }
[data-hrow][data-status="fail"] { border-left-color: var(--danger); }
[data-hrow][data-status="account_disconnected"] { border-left-color: var(--warn); }

[data-hrow] > [data-area="head"] {
  grid-area: head; display: flex; align-items: center; gap: 9px;
  flex-wrap: wrap; min-width: 0;
}
[data-hrow] > [data-area="body"] {
  grid-area: body; display: flex; align-items: baseline; gap: 10px;
  min-width: 0; font-size: var(--fs-lg);
}
[data-hrow] > [data-area="err"] {
  grid-area: err; display: flex; flex-direction: column; gap: 6px;
  padding: 11px 12px; border-radius: var(--r-lg); background: var(--surface-input);
}

/* Ограничение по высоте блока ошибки — МОДИФИКАТОР поверх примитива длинного
   текста (D-32, план 04-07). Примитив ниже НЕ тронут: он обслуживает ещё
   страницу записи, админскую историю и снапшот текста объявления, и правка
   распространила бы усечение на все четыре места разом.

   Ограничение включает САМО ПРИСУТСТВИЕ элемента раскрытия внутри блока —
   поэтому страница записи, которая его не рисует, ограничения не получает, и
   отдельного «сними ограничение» ей не требуется. Состояние переключает пара
   «сводка + раскрытие»: обработчика здесь нет, иначе при выключенном Alpine
   длинный текст ошибки стал бы недоступен.

   Текст при этом лежит в разметке ЦЕЛИКОМ: ограничена видимая высота, а не
   содержимое. Скрытое переполнением остаётся в дереве доступности и читается
   экранным диктором без раскрытия. */
[data-area="err"]:has(> [data-clamp]) > [data-longtext] {
  max-height: 4.8em; overflow: hidden;
}
[data-area="err"]:has(> [data-clamp][open]) > [data-longtext] { max-height: none; }

[data-clamp] > summary {
  display: inline-block; list-style: none; cursor: pointer;
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-muted);
}
[data-clamp] > summary::-webkit-details-marker { display: none; }
[data-clamp] > summary:hover { color: var(--text-bright); }
[data-clamp] > summary:focus-visible {
  outline: 2px solid var(--focus-ring); outline-offset: 2px;
}
[data-clamp] [data-clamp-less] { display: none; }
[data-clamp][open] [data-clamp-more] { display: none; }
[data-clamp][open] [data-clamp-less] { display: inline; }

/* Выделение значения ОДНИМ действием (макет 862, строка `user-select:all`).
   Это базовый путь копирования: он работает при выключенном JavaScript, когда
   кнопки копирования в разметке нет вовсе. */
[data-selectall] {
  -webkit-user-select: all; user-select: all; cursor: text;
}

/* Кнопка копирования диагностического блока (макет 863) — мелкая
   моношрифтовая таблетка в метаколонке. Это МОДИФИКАТОР существующей кнопки,
   а не своя кнопка: собственная разошлась бы с библиотечной в правилах фокуса
   и отключённого состояния, а рядом с ней в той же метаколонке стоит переход
   «Подробнее», собранный библиотечным вариантом. */
[data-copybtn] {
  padding: 5px 10px; border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  font-weight: 500; letter-spacing: .06em; text-transform: uppercase;
}

/* Страница записи истории (D-24) переиспользует примитив записи целиком:
   области, сетка и медиазапросы 1080px/860px достаются ей даром. Модификатор
   правит ровно то, чем страница отличается от строки списка, и ни правилом
   больше. */
[data-hrow][data-hrec]:hover { transform: none; }
/* Тело страницы — колонка: в него приходит снапшот текста и изображений,
   которых в строке списка нет и для которых строка не рассчитана. */
[data-hrow][data-hrec] > [data-area="body"] {
  flex-direction: column; align-items: stretch; gap: 10px;
}
/* В строке списка название и группа усечены многоточием — там им отведена одна
   строка. Страница записи и есть то место, где показывают всё: усекать здесь
   нечего и незачем. */
[data-hrow][data-hrec] > [data-area="body"] > [data-grow],
[data-hrow][data-hrec] > [data-area="body"] > [data-secondary] {
  white-space: normal; overflow: visible; text-overflow: clip; max-width: none;
}
[data-hrow][data-hrec] > [data-area="meta"] { align-items: flex-start; }
/* Опора исправленного медиазапроса 1080px: на узкой ширине левая граница здесь
   сменяется верхней. Правило-приёмник лежит в разделе 2 и правится только
   вместе с этим. */
[data-hrow] > [data-area="meta"] {
  grid-area: meta; display: flex; align-items: center; gap: 9px;
  justify-content: flex-end; padding-left: 22px;
  border-left: 1px solid var(--border-muted);
}

/* Иконка мессенджера приходит из includes/messenger_icon.html без собственных
   размеров: раньше их задавал класс удалённого фреймворка. Задаём здесь, чтобы
   не возвращать такой класс в разметку. */
[data-hrow] [data-area="head"] svg { width: 13px; height: 13px; flex: none; }
[data-hrow] [data-area="body"] > [data-grow] {
  font-weight: 600; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
[data-hrow] [data-area="body"] > [data-secondary] {
  flex: none; max-width: 45%;
  font-size: var(--fs-sm); color: var(--text-secondary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Текст, который обязан читаться ЦЕЛИКОМ: сообщение об ошибке отправки и
   снимок текста объявления. Ни усечения, ни многоточия, ни скрытия за
   раскрытием — по тексту ошибки пользователь понимает, почему его реклама не
   ушла. Длинные строки переносятся, а не обрезаются. */
[data-longtext] {
  display: block; margin: 0;
  font-size: var(--fs-md); line-height: 1.6; color: var(--text-secondary);
  white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;
}
[data-longtext="mono"] {
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--text-bright);
}

/* Миниатюры изображений объявления на странице детали отправки */
[data-thumbs] {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px; margin-top: 12px;
}
[data-thumbs] img {
  display: block; width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: var(--r-md); background: var(--surface-input);
}

/* Плитки дашборда: крупное число и дельта к предыдущему окну на одной базовой
   линии (макет 387-392). Спарклайн макета (393-397) не реализуется — D-03.
   Живая лента приходит планом 04-05. */
[data-metrics] {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 13px;
}
[data-metric-line] {
  display: flex; align-items: baseline; gap: 8px; margin-top: 10px;
}
[data-metric-value] {
  font-size: var(--fs-h3); font-weight: 600; letter-spacing: -.03em;
  /* Цифры одной ширины: без этого число дёргает дельту вбок при каждом
     обновлении страницы. */
  font-variant-numeric: tabular-nums;
}
/* Дельта. Тон приходит атрибутом от вызывающего, а не выводится из знака в
   CSS: для плитки ошибок знак читается наоборот, и правило инверсии
   принадлежит смыслу метрики. Нейтральный тон (дельта равна нулю) —
   приглушённый, чтобы ноль не спорил за внимание с самим числом. */
[data-metric-delta] {
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-muted);
}
[data-metric-delta][data-tone="success"] { color: var(--ok); }
[data-metric-delta][data-tone="danger"] { color: var(--danger); }

/* Блок правил карточек тарифов ВЫРЕЗАН ЦЕЛИКОМ планом 05.1-05 вместе с самими
   карточками: тарифов Free/Basic/Pro не существует (D-F), витрины нет, и сетка
   карточек, рамка текущего тарифа, имя тарифа и две строки лимитов остались без
   единого потребителя в проекте. Селекторы вырезанного блока в комментарии не
   называются: гейт отсутствия проверяет файл ПО ТЕКСТУ, и объяснение, набранное
   их именами, роняло бы собственный запрет.
   ⚠️ ПРАВИЛО НАЖИМАЕМОЙ ВЫСОТЫ НИЖЕ ЛЕЖАЛО ВПЛОТНУЮ К ВЫРЕЗАННОМУ БЛОКУ и
   носит то же префиксное имя. Оно ВЫЖИВАЕТ: это единственное правило нажимаемой
   высоты кнопки оплаты в проекте и закрытая находка фазы 5. Правила плиток
   дашборда выше не тронуты — их держат дашборд Фазы 4 и карточка пользователя
   в админке, и удалять их вместе с соседями было бы самой дорогой ошибкой
   фазы вычитания. */

/* Нажимаемая высота кнопок ОПЛАТЫ (M3/C1). Вычисленная высота .btn — 33px
   (13px --fs-md при line-height:1 + 9px×2 padding + 1px×2 border). Эти 33px
   ПРОХОДЯТ порог WCAG 2.5.8 AA (24px) и не проходят только СОБСТВЕННЫЙ порог
   проекта 44px — токен `touch` из `## Spacing Scale`. Заявить здесь нарушение
   WCAG значило бы обосновать правильную правку неправдой.
   Адресуется [data-plan-cta] .btn, а НЕ голый .btn: общая кнопка стоит на 26
   страницах, и поднятие её высоты глобально — перебазирование ритма, работа
   milestone, а не раздела. Свойство именно min-height: у .btn объявлен
   inline-flex с align-items: center, поэтому подпись центрируется сама, без
   правки унаследованного padding 9px. */
[data-plan-cta] .btn { min-height: 44px; }

/* Нажимаемая высота действия ВНУТРИ СТРОКИ ОЧЕРЕДИ (M3, план 06-07). Кнопка
   «Снять» стоит в ряду ячеек, а не отдельным блоком: на 375px ряд складывается,
   и действие оказывается прижатым к соседним значениям. Прецедент проекта для
   действия внутри строки — [data-group-row] .btn { min-height: 36px } — и здесь
   применяется РОВНО он же, тем же числом, чтобы два списка одного приложения не
   расходились на ощупь.
   Адресуется [data-queue-row] .btn, а НЕ голый .btn: общая кнопка стоит на 26
   страницах, и поднятие её высоты глобально — перебазирование ритма, работа
   milestone, а не подраздела. Атрибут, а не класс: своих классов подраздел не
   заводит, и правило садится на разметку, которая уже есть. */
[data-queue-row] .btn { min-height: 36px; }

/* Строка журнала подраздела «Логи» (S4, C3, план 06-08) ---------------------- */
/* ⚠️ ЭТО ЕДИНСТВЕННАЯ СТРОКА ФАЗЫ ВНЕ ПРИМИТИВА ТАБЛИЦЫ, И ПРИЧИНА ЗАПИСАНА
   ЗДЕСЬ, А НЕ ПОДРАЗУМЕВАЕТСЯ. Ячейка таблицы (.cell выше) объявлена с
   overflow: hidden и text-overflow: ellipsis — то есть усекает содержимое
   многоточием. Для колонки состояния это верно, для тела строки лога —
   разрушительно: усечётся ровно та часть, ради которой журнал открыли (текст
   ошибки, стектрейс). Поэтому шапка строки — обычный ряд mono-меток, а тело
   уходит в [data-longtext="mono"], заведённый Фазой 4 под текст, который обязан
   читаться ЦЕЛИКОМ.
   Правила два, оба — атрибутные: своих классов подраздел не заводит, а
   утилитарные классы в проекте запрещены сплошным обходом шаблонов.
   Разделитель тот же, что у строки таблицы, чтобы две ленты одного приложения
   не расходились на глаз. */
[data-logline] {
  padding: 12px 0;
  border-bottom: 1px solid var(--border-hair);
}
[data-logline]:last-child { border-bottom: none; }
/* Перенос по строкам, а не горизонтальная прокрутка: на 375px время, уровень и
   источник в одну строку не помещаются, а прокручиваемая полоса спрятала бы
   источник за краем — достижимым, но невидимым. */
[data-logline-head] {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 9px; margin-bottom: 5px;
}

/* Строка блока инцидентов «Обзора» (S6, C5, план 06-10) --------------------- */
/* ⚠️ ПРАВИЛА АТРИБУТНЫЕ, СВОИХ КЛАССОВ БЛОК НЕ ЗАВОДИТ: утилитарные классы в
   проекте запрещены сплошным обходом шаблонов, а второй класс на ту же роль
   разошёлся бы с существующими лентами на глаз.
   Раскладка — flex по базовой линии, дословно из макета: точка, время, подпись
   вида и текст стоят на одной линии шрифта, а не по центру коробок разной
   высоты. Перенос обязателен: на 375px четыре слота в строку не помещаются, и
   горизонтальная прокрутка увела бы переход «Чинить →» за край — достижимым, но
   невидимым.
   Разделитель тот же, что у ленты журнала, чтобы два перечня одного приложения
   не расходились на глаз. */
[data-incident-row] {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 9px; padding: 12px 0;
  border-bottom: 1px solid var(--border-hair);
}
[data-incident-row]:last-child { border-bottom: none; }
/* Точка тона. Не эмодзи и не иконка: цвет здесь — ВТОРОЙ носитель того же
   смысла, что подпись вида рядом, а не единственный. Различающий цвета плохо
   читает строку по подписи и тексту без потерь. */
[data-incident-dot] {
  flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%;
  align-self: center; background: var(--text-muted);
}
[data-incident-dot][data-tone="danger"] { background: var(--danger); }
[data-incident-dot][data-tone="warn"] { background: var(--warn); }
/* Текст занимает остаток строки и переносится ЦЕЛИКОМ: он несёт имя и число,
   ради которых строку читают, и усечение многоточием отняло бы у неё ответ. */
[data-incident-text] {
  flex: 1 1 200px; min-width: 0;
  font-size: var(--fs-md); color: var(--text);
  overflow-wrap: anywhere;
}
/* Переход прижат к правому краю там, где строка помещается, и уходит на
   следующую строку там, где нет. Цвет наследуется от ссылки — пятого носителя
   акцента фаза не заводит. */
[data-incident-fix] { flex: 0 0 auto; margin-left: auto; font-size: var(--fs-md); }

/* Строка ближайшей отправки (DASH-02, макет 402-426) ------------------------ */
/* Строка — ССЫЛКА (D-16), поэтому цвет и подчёркивание сбрасываются здесь, а не
   отменяются в разметке: подчёркнутая строка целиком читалась бы как ошибка
   вёрстки, а не как переход. Фокус остаётся видимым — обвести ссылку нечем,
   кроме контура. */
[data-uprow] {
  display: flex; align-items: center; gap: 13px; min-width: 0;
  padding: 11px 10px; border-radius: var(--r-lg);
  color: inherit; text-decoration: none;
  transition: background .16s ease;
}
[data-uprow]:hover { background: var(--surface-input); }
[data-uprow]:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Колонка времени ФИКСИРОВАННОЙ ширины: без неё названия разной длины сдвигают
   время по вертикали, и столбец перестаёт читаться сверху вниз. */
[data-uprow] > .mono { flex: none; width: 84px; font-variant-numeric: tabular-nums; }
[data-upstripe] {
  flex: none; width: 3px; height: 26px; border-radius: 2px;
  background: var(--border-control);
}
[data-upstripe][data-channel="tg_user"] { background: var(--info); }
[data-upstripe][data-channel="wa"] { background: var(--ok); }
[data-upstripe][data-channel="max"] { background: var(--accent); }
/* min-width: 0 обязателен: название приходит от пользователя и не ограничено
   ничем — без него оно вытолкнуло бы бейдж канала за пределы строки. */
[data-upmain] { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
[data-uptitle] {
  min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-md); font-weight: 500;
}
[data-upmeta] {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0;
  font-size: var(--fs-xs); color: var(--text-tertiary);
}
/* Бейдж канала — КОМПАКТНАЯ ПИЛЮЛЯ макета: mono-подпись 10px, отступы
   4px 8px 4px 6px, радиус 6px, иконка 11px.

   РАЗМЕР ИКОНКИ ЗАДАЁТСЯ ЗДЕСЬ И ЭТО ОБЯЗАТЕЛЬНО. Макрос messenger_icon
   получает size='' именно с уговором, что размер придёт от раздела (см. его
   докстринг). Без этого правила svg остаётся с одним viewBox и без width/height
   — заменяемый элемент растягивается до размера контейнера: строка вырастала в
   несколько раз по высоте, а подпись канала уезжала за правый край. Раздел
   истории свою половину уговора выполняет строкой 1063; дашборд — не выполнял.

   Тон приходит АТРИБУТОМ канала, тем же, что у полоски строки: у MAX иконка
   залита градиентом, а не currentColor, поэтому окрасить бейдж через цвет
   глифа нельзя — источником тона обязан быть контейнер. */
[data-upbadge] { flex: none; }
[data-upbadge] .msg {
  gap: 5px; padding: 4px 8px 4px 6px; border-radius: var(--r-xs);
  background: var(--surface-input);
}
[data-upbadge] .msg__glyph { width: 11px; height: 11px; }
[data-upbadge] .msg__label {
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .08em; color: inherit;
}
[data-upbadge][data-channel="tg_user"] .msg {
  color: var(--info); background: color-mix(in oklab, var(--info) 12%, transparent);
}
[data-upbadge][data-channel="wa"] .msg {
  color: var(--ok); background: color-mix(in oklab, var(--ok) 12%, transparent);
}
[data-upbadge][data-channel="max"] .msg {
  color: var(--accent); background: color-mix(in oklab, var(--accent) 12%, transparent);
}

@media (max-width: 400px) {
  /* Перестроение идёт СРЕДСТВАМИ CSS, переключателя вида на JS нет. Первой
     уходит подпись канала: иконка несёт тот же смысл, а её aria-label остаётся
     на месте, поэтому программе чтения с экрана канал по-прежнему называется
     словом. */
  [data-uprow] { gap: 9px; }
  [data-uprow] > .mono { width: 72px; }
  [data-upbadge] .msg__label { display: none; }
  /* Без подписи асимметричные отступы пилюли (6px слева под иконку, 8px справа
     под текст) превращают её в скошенный прямоугольник — выравниваем. */
  [data-upbadge] .msg { gap: 0; padding: 5px; }
}

/* Шапка блока дашборда ------------------------------------------------------ */
/* ОДНА шапка на все три блока: заголовок слева, mono-подпись прижата вправо
   на ТОЙ ЖЕ строке. Своя, а не card_open(title=...), по двум причинам сразу:
   парный макрос карточки ставит подпись ПОД заголовком, а макет — справа на
   строке заголовка; и он же рисует под шапкой разделительную линию
   (.card__head border-bottom), которой в макете нет ни у одной карточки
   дашборда. До этого правила «Ближайшие отправки» шли через card_open(title=)
   и получали линию, а лента и график — нет: две карточки одной пары выглядели
   по-разному.

   Правило ОДНО на три атрибута, а не три одинаковых: до консолидации их было
   два с побайтово совпадающими телами, и третий блок завёл бы третью копию.
   .card__head с разделителем остаётся как есть для всех прочих разделов
   проекта — здесь снят не примитив, а его применение на этой странице. */
[data-blockhead] { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
[data-blockhead] .mono { margin-left: auto; }

/* Живая лента (DASH-03, макет 428-444) -------------------------------------- */
[data-feedlive] { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; }
/* Пульсирующая точка «идёт прямо сейчас». Псевдоэлемент, а не отдельный узел
   разметки: точка — чистое украшение подписи, и программе чтения с экрана в
   потоке она не нужна. Анимация — СУЩЕСТВУЮЩИЙ @keyframes pulse раздела 2
   (тот же, что у индикатора сессий в шапке шелла): второй пульсации в проекте
   не заводится. */
[data-feedlive]::before {
  content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ok); animation: pulse 1.4s ease-in-out infinite;
}
/* Контейнер подмены. Он же носитель опроса, поэтому в разметке у него есть
   идентификатор: подмена приходит ВНУТРЬ него, и сам он DOM не покидает. */
[data-feed] { display: flex; flex-direction: column; gap: 2px; }
/* Строка — ССЫЛКА в запись истории (D-08): цвет и подчёркивание сбрасываются
   здесь, а не отменяются в разметке. Фокус остаётся видимым — обвести ссылку
   нечем, кроме контура. */
[data-feedrow] {
  display: flex; align-items: center; gap: 11px; min-width: 0;
  padding: 9px 10px; border-radius: var(--r-lg);
  color: inherit; text-decoration: none;
  transition: background .16s ease;
}
[data-feedrow]:hover { background: var(--surface-input); }
[data-feedrow]:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Точка статуса. Цвет приходит ПО ЗНАЧЕНИЮ статуса, а не инлайн-стилем макета:
   инлайн-стилей в проекте нет, а неизвестный статус обязан остаться ВИДИМЫМ —
   нейтральная заливка по умолчанию честнее исчезнувшей точки, которая сдвинула
   бы текст события относительно соседних строк. */
[data-feeddot] {
  flex: none; width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-muted);
}
[data-feeddot][data-status="ok"] { background: var(--ok); }
[data-feeddot][data-status="fail"] { background: var(--danger); }
[data-feeddot][data-status="account_disconnected"] { background: var(--warn); }
/* min-width: 0 обязателен: название объявления и имя группы приходят от
   пользователя и не ограничены ничем — без него они вытолкнули бы подпись
   времени за пределы строки. */
[data-feedtext] {
  flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-md);
}
/* Подпись времени прижата вправо и не сжимается: «2 часа назад» и «только что»
   разной длины, и без фиксации край строки прыгал бы от строки к строке. */
[data-feedrow] > .mono { flex: none; font-variant-numeric: tabular-nums; }

/* ПРАВИЛ БАР-ЧАРТА НЕДЕЛЬНОЙ АКТИВНОСТИ ДАШБОРДА ЗДЕСЬ БОЛЬШЕ НЕТ
   (задача 260826-9vv). Раздел описывал столбцы, их колонки, подписи суток и
   отдельное правило пониженной анимации для столбца; вместе с самим блоком у
   всех них не осталось ни одного элемента на экране, а стили без разметки —
   мусор, который следующий читатель принимает за живое. Возврат правил
   запрещает `test_the_dashboard_activity_chart_left_no_css_behind`: он же
   держит запрет на возврат прежней сетки семь на двадцать четыре, чьи правила
   были сняты ещё при замене её столбцами.

   Правило пониженной анимации выше по файлу НЕ ТРОГАЛОСЬ: имени столбца в нём
   никогда не было, а `test_the_reduced_motion_rule_names_only_living_classes`
   требует в первом таком правиле ровно трёх живых имён. */

/* Вертикальная стопка панелей страницы (дашборд, профиль) */
[data-stack] { display: flex; flex-direction: column; gap: 22px; }
/* ПАРА «Ближайшие отправки» / «Живая лента» — ДВЕ КОЛОНКИ по макету. Оба
   шаблона называют себя «левой» и «правой половиной пары», но до этого правила
   лежали в вертикальной стопке страницы, то есть каждая занимала полную ширину:
   пара существовала в комментариях и не существовала на экране.

   Схлопывание в одну колонку делает сама сетка (`auto-fit` + `minmax`), когда
   на две колонки по 330px перестаёт хватать ширины — отдельный медиазапрос для
   этого не нужен и завёл бы вторую точку правды о том же переломе.

   min-width: 0 на потомках обязателен: внутри обеих половин лежат строки с
   обрезкой по многоточию, а элемент сетки по умолчанию не сжимается меньше
   своего содержимого — без него длинное название вытолкнуло бы колонку. */
[data-dashpair] {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
  gap: 13px; min-width: 0;
}
[data-dashpair] > * { min-width: 0; }
/* Вертикальная стопка полей формы */
[data-form] { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
[data-form] > .field { align-self: stretch; }

/* QR-код мастера подключения: ответ опроса /accounts/connect/{wa,max}/status.
   Разметка приехала из app/pages/accounts.py, где собиралась конкатенацией
   строк с utility-классами удалённого фреймворка. */
[data-qr] { display: flex; flex-direction: column; align-items: center; gap: 12px; }
[data-qr] img {
  width: 100%; max-width: 256px; height: auto;
  padding: 14px; background: #fff;
  border: 1px solid var(--border-input); border-radius: var(--r-xl);
}

/* Иконка мессенджера (includes/messenger_icon.html). Последний носитель
   utility-классов удалённого фреймворка в проекте: до Плана 08 его ветка else
   отдавала text-gray-500, а сами svg — text-blue-500 / text-emerald-600.
   Цвета взяты токенами статусов, новых захардкоженных значений нет. */
.msg { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.msg__glyph { flex: none; }
.msg__glyph--tg { color: var(--info); }
.msg__glyph--wa { color: var(--ok); }
.msg__label { font-size: var(--fs-sm); color: var(--text-secondary); }
.msg--plain {
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .08em; color: var(--text-muted);
}

/* Горизонтальный ряд действий: несколько независимых форм в одной строке.
   Общий примитив, НЕ привязанный к разделу: каждое действие остаётся
   отдельной формой со своим маршрутом и методом. */
[data-actions] { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }

/* Блок пользователя на странице профиля */
[data-identity] { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
[data-identity-meta] { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* Постоянная обёртка бейджа блокировки на карточке пользователя в админке
   (Фаза 11, план 11-12): цель внеполосной подмены ответа тумблера. Обёртка
   существует всегда, а колонка личности — флекс с зазором, поэтому пустая
   обёртка добавила бы зазор под адресом у каждого незаблокированного.
   `display: contents` делает флекс-элементом сам бейдж, а не обёртку. */
[data-identity-meta] > #user-block-badge { display: contents; }
[data-identity-name] { font-size: var(--fs-xl); font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  [data-hrow] { transition: none !important; }
  [data-hrow]:hover { transform: none !important; }
}

/* --------------------------------------------------------------------------
   6. Мастера подключения каналов (План 06).

   Дополнение файла санкционировано планом: шаг мастера и блок QR-кода — это
   единственные элементы раздела, для которых в библиотеке компонентов макроса
   нет. Всё остальное на экранах подключения собрано из card / field / button /
   alert / badge. Значения — токены раздела 1; новых цветов, радиусов и кеглей
   не вводится.
   -------------------------------------------------------------------------- */

/* Шаг мастера. Мастера пошаговые, и на экране Telegram шаги переключаются
   скриптом через hidden — поэтому шаг обязан быть отдельным блоком, а не
   строкой внутри карточки. */
.connect-step { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
.connect-step[hidden] { display: none; }
.connect-step--center { align-items: center; text-align: center; }
.connect-step > .field, .connect-step > form { align-self: stretch; }
.connect-step__text { margin: 0; font-size: var(--fs-md); color: var(--text-secondary); }
.connect-step__actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Центрированный шаг центрирует свои flex-предметы, но форма шага растянута
   правилом `.connect-step > form` выше, поэтому ряд действий внутри формы
   начинался у левого края (UI-2, шаг «код истёк»). Правило центрирует ряд
   внутри растянутой формы и так же защищает любой будущий центрированный шаг. */
.connect-step--center .connect-step__actions { justify-content: center; }
.connect-step__form { display: flex; flex-direction: column; gap: 14px; align-items: stretch; }
/* Подпись «Загрузка...» шагов старта и обновления QR (UI-3).
   Сигнал показа — атрибут `disabled`, который htmx ставит на цель блокировки
   обёртки (по умолчанию — кнопка отправки) на всё время запроса и снимает по
   ответу, по сбою сети и по таймауту. Вне запроса эти кнопки не отключены.
   Почему не класс запроса на форме: обёртка всегда объявляет свой индикатор, и
   рантайм вешает класс запроса на узел точки, а не на форму (раздел
   «Индикатор htmx-формы» ниже) — такое правило не сработало бы никогда.
   Почему не `:has()` по узлу точки: гейт неподвижности индикатора панели
   подтверждения краснит любой селектор с классом индикатора вне объявленного
   множества (решение владельца от 2026-09-17).
   Почему `display`, а не прозрачность или видимость: скрытая подпись не должна
   занимать место — иначе она сдвигала бы с центра пару кнопок шага «код истёк»
   и давала бы пустую строку на узкой ширине. `display` не задерживается,
   поэтому порога в 300 мс здесь нет: запрос старта длится секунды.
   Подпись стоит последующим соседом кнопки в ряду — комбинатор `~` находит
   только более поздних соседей. */
.connect-step__busy { display: none; font-size: var(--fs-md); color: var(--text-secondary); }
.connect-step__actions > button[type="submit"][disabled] ~ .connect-step__busy { display: inline; }

/* Блок QR-кода. Ширина ограничена самим изображением: QR обязан остаться
   квадратным и достаточно крупным для сканирования телефоном. */
.connect-qr {
  align-self: center; margin: 0 auto; width: max-content; max-width: 100%;
  padding: 12px; border-radius: var(--r-xl);
  background: var(--surface-input); border: 1px solid var(--border);
}
.connect-qr img { display: block; width: 256px; max-width: 100%; height: auto; border-radius: var(--r-md); }

/* Колонка мастера: узкая центральная страница вместо сетки раздела. */
.connect-shell { display: flex; flex-direction: column; gap: 14px; max-width: 460px; margin: 0 auto; }

/* --------------------------------------------------------------------------
   7. Подпись колонки внутри ячейки и панель постраничной навигации (План 07).

   Дополнение файла санкционировано планом: в Плане 07 из проекта исчезает
   ЕДИНСТВЕННЫЙ элемент таблицы, и его данные переезжают на общие примитивы
   строки. У табличных данных, в отличие от списков, число само по себе ничего
   не значит — его смысл несёт шапка колонок, а она на 860px скрывается
   ([data-rowhead] { display: none }). Отсюда общий, НЕ привязанный к разделу
   примитив подписи. Значения — токены раздела 1.
   -------------------------------------------------------------------------- */

/* Подпись колонки, живущая внутри ячейки: на широком экране её роль играет
   шапка колонок, поэтому она скрыта; на узком — шапки нет, и подпись
   появляется рядом со значением. Ни одного класса раздела: Фазы 5 и 6
   размечают свои табличные данные этим же атрибутом. */
[data-cell-label] { display: none; }

/* Панель постраничной навигации: счётчик слева, кнопки справа. Общий
   примитив — страниц с offset-навигацией в проекте несколько. */
[data-pager] {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-top: 16px;
}
[data-pager-actions] { display: flex; align-items: center; gap: 9px; }

@media (max-width: 860px) {
  [data-cell-label] {
    display: inline; margin-right: 7px;
    font-family: var(--font-mono); font-size: var(--fs-2xs);
    letter-spacing: .12em; text-transform: uppercase; color: var(--text-muted);
  }
}

/* --------------------------------------------------------------------------
   8. Редактор объявления и сводный список расписаний (Фаза 2).

   Дополнение файла санкционировано планом: редактор объявления — единственный
   экран проекта с двухколоночной сеткой, полосой вложений, серверным
   предпросмотром и блоком «ключ — значение»; макросов под них в библиотеке
   раздела 4 нет. Всё остальное на экране собрано из card / field / button /
   badge / modal / empty_state и переиспользуется БЕЗ изменений.

   Первая половина раздела относится к РЕДАКТОРУ (План 02-04), вторая — к
   КАРТОЧКЕ РАСПИСАНИЯ в редакторе (План 02-05). Сводный список расписаний
   приходит планом 02-07.

   Значения — токены раздела 1: ни одного нового цвета, радиуса или кегля.
   Фиксированные размеры кратны четырём: плитка вложения 88, миниатюра
   предпросмотра 120, ячейка дня 44×36 (40×36 на ≤400px), таблетка времени 36,
   чипс аккаунта 44, строка выбора группы 40.
   -------------------------------------------------------------------------- */

/* Сетка редактора. Переопределения на ≤900px уже лежат в разделе 2 со времён
   Плана 01-01 — перестраивать было что, а базового правила в файле не было.
   Здесь появляется только оно; медиазапрос НЕ дублируется. */
[data-editor] {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(300px, .85fr);
  gap: 16px;
  align-items: start;
  animation: rise .28s cubic-bezier(.2, .8, .2, 1);
}

/* Правая колонка: липкая, пока хватает ширины. `position: static` на ≤900px
   тоже уже лежит в разделе 2. */
[data-editor-side] {
  position: sticky; top: 96px;
  display: flex; flex-direction: column; gap: 16px;
  min-width: 0;
}

/* Левая колонка — общий примитив стопки, но с шагом сетки редактора (16), а не
   с шагом стопки страницы (22): между колонками и внутри колонок расстояние
   обязано совпадать, иначе сетка визуально «съезжает». */
[data-editor] > [data-stack] { gap: 16px; }

/* Шапка карточки внутри редактора — СТРОКА: подпись раздела слева, индикатор
   автосохранения у правого края. Макрос card не форкается и не
   перестилизуется — правило действует только внутри сетки редактора и только
   на раскладку шапки. На узкой ширине индикатор переносится на вторую строку
   (flex-wrap), а не выталкивает подпись раздела и не обрезается. */
[data-editor] .card__head {
  flex-direction: row; align-items: center; justify-content: space-between;
  gap: 8px; flex-wrap: wrap;
}

/* Полоса вложений ---------------------------------------------------------- */
[data-media] { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }

.media-tile {
  position: relative; width: 88px; height: 88px; flex: none;
  border-radius: var(--r-lg); overflow: hidden;
  background: var(--surface-input); border: 1px solid var(--border-input);
}
.media-tile img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Имя файла на плитке НЕ рендерится — оно живёт в подсказке. Имя подконтрольно
   отправителю: вынесенное в раскладку, длинное или враждебное имя ломало бы
   полосу вложений. */
.media-tile__remove {
  position: absolute; top: 0; right: 0;
  width: 36px; height: 36px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-bottom-left-radius: var(--r-lg);
  background: color-mix(in oklab, var(--bg) 72%, transparent);
  color: var(--text-secondary);
  font-family: var(--font-mono); font-size: var(--fs-lg); line-height: 1;
  cursor: pointer;
}
/* 36×36, а не «крестик по размеру глифа»: минимальная цель касания раздела —
   36px по меньшей стороне, и исключение из неё в проекте ровно одно
   (унаследованный тумблер 22px, обёрнутый в строку ≥40px). */
.media-tile__remove:hover { color: var(--danger); }
.media-tile__remove:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }

.media-tile--add {
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px dashed var(--border-dashed);
  color: var(--text-muted);
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .08em; cursor: pointer;
}
.media-tile--add:hover { color: var(--text); border-color: var(--border-control-strong); }
.media-tile--add:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* На лимите плитка добавления скрывается, а не удаляется. Правило нужно явно:
   у признака hidden правило браузера слабее классового, и без этой строки
   `display: flex` выше пересилил бы его. */
.media-tile--add[hidden] { display: none; }

/* Счётчик длины текста ----------------------------------------------------- */
/* Приглушённый в нормальном диапазоне — счётчик не акцентный элемент. */
.counter {
  align-self: flex-end;
  font-family: var(--font-mono); font-size: var(--fs-xs);
  letter-spacing: .06em; color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.counter--warn { color: var(--warn); }
.counter--over { color: var(--danger); }

/* Индикатор htmx-формы ------------------------------------------------------ */
/* ОДИН класс на сорок семь форм вехи (FORM-09, QUAL-02, D-15). Узел рисует сам
   макрос-обёртки app/templates/components/form_wrapper.html; ни один вызывающий
   селектора индикатора не передаёт, поэтому разъехаться сорока семи индикаторам
   не на чем.

   ⚠️ СЕЛЕКТОР СОСТАВНОЙ, А НЕ ПОТОМКОВЫЙ, И ФОРМУ У СОСЕДНЕГО ПРАВИЛА БРАТЬ
   НЕЛЬЗЯ. Класс `htmx-request` рантайм вешает на САМ узел, найденный
   `hx-indicator`, а не на предка. Отгруженный прецедент ниже
   (`#ad-form.htmx-request .autosave`) потомковый ровно потому, что у формы
   редактора объявлений `hx-indicator` нет вовсе и класс садится на саму форму.
   Скопированный сюда, он дал бы правило, которое не сработает НИКОГДА.

   ⚠️ ПЕРЕКЛЮЧАЮТСЯ `opacity` И `visibility` — обе, и обе обязательны. `display`
   ЗДЕСЬ НЕ ПЕРЕКЛЮЧАЕТСЯ И ПЕРЕКЛЮЧАТЬСЯ НЕ МОЖЕТ: он не анимируется без
   `transition-behavior: allow-discrete`, и задержка к нему не применяется вовсе —
   индикатор появлялся бы мгновенно на каждое нажатие. Объявлен он ниже
   СТАТИЧЕСКИ и по совсем другому поводу — см. шестой абзац.
   Одна `opacity` оставляет узел НА МЕСТЕ ДЛЯ УКАЗАТЕЛЯ И ДЛЯ ПОИСКА
   ПО СТРАНИЦЕ: невидимая точка продолжала бы ловить наведение и
   находиться поиском там, где глазами ничего нет.
   ⚠️ ДЕРЕВО ДОСТУПНОСТИ ЗДЕСЬ НИ ПРИ ЧЁМ, и прежнее обоснование
   называло эффект, который на этом узле произойти НЕ МОЖЕТ (IN-02
   ревизии Фазы 9). Узел несёт `aria-hidden="true"` и текста не имеет вовсе —
   скринридеру читать нечего НИ В ОДНОМ состоянии, и переключение
   видимости на это не влияет никак. Само переключение остаётся — меняется
   только причина, по которой оно записано.
   Одна `visibility` переключается скачком в конце задержки и читается как
   рывок.

   ⚠️ ЗАДЕРЖКА ВИСИТ НА СОСТОЯНИИ «ЗАПРОС ИДЁТ», А НЕ НА ПОКОЕ. Записанная в
   базовом правиле, она задержала бы и обратный ход: индикатор висел бы ещё
   300 мс ПОСЛЕ ответа, то есть врал бы о состоянии запроса.

   ЦЕНА ОБЕИХ ГРАНИЦ ПОРОГА НАЗВАНА, А НЕ ВЫБРАНА КРАСИВО (D-15). 150 мс дают
   мигание на каждое нажатие: ответ тумблера укладывается быстрее, и человек
   видел бы вспышку на действии, которое и так мгновенно. 500 мс — полсекунды
   без обратной связи, которые читаются как «кнопка не сработала» и провоцируют
   ровно то повторное нажатие, от которого стоит QUAL-01.

   ⚠️ ИМЯ КЛАССА НЕ `htmx-indicator`, И ЭТО ВЫНУЖДЕННО. Рантайм дописывает
   собственный стиль для этого имени в конец <head>, то есть ПОСЛЕ этого файла,
   и при равной специфичности выигрывает — порог пропал бы молча, без единого
   признака отказа. Заглушить его седьмым ключом блока конфигурации нельзя:
   состав ключей утверждён точным равенством множеств в
   tests/test_pages/test_shell.py (Фаза 7).

   ⚠️ КОРОБКА У УЗЛА СОБСТВЕННАЯ, А ДО ЭТОЙ ПРАВКИ ЕЁ НЕ БЫЛО (WARN-5
   ревизии Фазы 9). Узел индикатора — `<span>`, то есть строчный элемент, на
   котором ширина, высота и скругление не действуют вовсе.
   ДО ЭТОЙ ПРАВКИ ВИДИМОСТЬ ДЕРЖАЛАСЬ НА СОВПАДЕНИИ: обе сегодняшние
   формы оказались контейнерами с выкладкой — `.modal__form` (раздел модалок) и
   форма тумблера строки группы (раздел экрана групп), — и обе блокировали
   строчный узел как свой предмет выкладки.
   ЦЕНА СОВПАДЕНИЯ НАЗВАНА: третий…сорок седьмой вызывающий вехи, чья
   форма контейнером с выкладкой НЕ является, получил бы точку НУЛЕВОГО
   размера — то есть никакого признака идущего запроса вовсе.
   И ОТКАЗ БЫЛ БЫ МОЛЧАЛИВЫМ: правило гейта проверяло ТОЛЬКО задержки
   перехода, и невидимый индикатор оставил бы его зелёным — ни в консоли,
   ни в прогоне признака отказа не было бы ни одного.
   Поэтому `display: inline-block` объявлен в базовом правиле САМ, а `flex: none`
   оставлен: внутри форм-контейнеров он продолжает работать, вне их не мешает.
   Охраняется test_the_indicator_class_is_self_sufficient.

   ⚠️ СОБСТВЕННАЯ КОРОБКА ИМЕЕТ ЦЕНУ, И ФАЗА 11 ЕЁ ЗАПЛАТИЛА (гэп G-11-6, план
   11-21). Узел спрятан прозрачностью и видимостью, а обе они коробку
   СОХРАНЯЮТ, и до этой правки ничто не выводило его из потока. Один механизм
   дал три исхода, и все три ЗАМЕРЕНЫ, а не выведены чтением: под колонкой
   блочной формы открывался строчный бокс высотой в строку (21 px у настроек
   профиля, 18,5 px у шага мастера MAX); в форме-колонке правки расписания узел
   становился предметом выкладки и добавлял свои 8 px плюс промежуток 16 —
   ровно 24 px; в рядах действий каждая обёрнутая форма оказывалась примерно на
   12 px шире своего органа, отчего промежутки рядом с голыми формами читались
   неровными.

   ⚠️ ОБЛАСТЬ ИСПРАВЛЕНИЯ ЗАДАЁТ КЛАСС ФОРМЫ ОБЁРТКИ, А НЕ КЛАСС ИНДИКАТОРА И НЕ
   ЕГО СЕЛЕКТОР ПОИСКА, И ЭТО РЕШЕНИЕ ВЛАДЕЛЬЦА ОТ 2026-09-17. Компонент панели
   подтверждения печатает ДОСЛОВНО тот же узел с тем же селектором поиска в
   своей колонке; его высота ПРИНЯТА обходом Фазы 10 (отметка 3.5). Правило,
   заданное по индикатору, сдвинуло бы разом все восемнадцать мест
   подтверждения. Поэтому базовое правило ниже объявления позиции НЕ ПОЛУЧАЕТ
   вовсе, а вне потока узел выводится ТОЛЬКО внутри формы, несущей класс
   обёртки. Неподвижность панели доказывает отдельное правило гейта разметки с
   тремя подстановками.

   ⚠️ СМЕЩЕНИЯ НУЛЕВЫЕ, И ЭТО РЕШЕНИЕ, А НЕ УМОЛЧАНИЕ. Точка обязана остаться
   ВНУТРИ коробки своей формы: карточка раздела 3 несёт `overflow: hidden` и
   срезала бы выступ у своего края, а в рядах с промежутком 8–9 px вынесенная
   наружу точка легла бы на соседний орган. Правый нижний угол выбран один на
   все обёрнутые формы: у высоких форм кнопка отправки последняя, и точка стои́т
   на её строке, у форм из одного органа — у нижнего правого угла самого органа.

   ⚠️ ВЕЛИЧИНА ПРОЗРАЧНОСТИ ДЛЯ УКАЗАТЕЛЯ (`pointer-events`) — НЕ УКРАШЕНИЕ.
   Выведенная из потока точка ложится ПОВЕРХ угла органа, и без неё она
   перехватывала бы нажатие по нему. У тумблеров расписания цель блокировки
   пустая, то есть перехваченное нажатие пропало бы молча.

   ⚠️ БАЗОВОЕ ПРАВИЛО СОХРАНЯЕТ СТРОЧНО-БЛОЧНУЮ КОРОБКУ, И ДВА ПОТРЕБИТЕЛЯ ЕЮ
   ЖИВУТ: форма панели подтверждения и форма тумблера строки группы. Второй
   объявлен ИСКЛЮЧЕНИЕМ раздела экрана групп ниже — его точка остаётся в потоке
   рядом с тумблером (наблюдение 6.1.2 обхода Фазы 9, решение владельца от
   2026-09-17), и это принятое поведение, а не место, до которого не дошли руки.

   Новой CSS для видимости БЛОКИРОВКИ здесь нет и не нужно: правило
   `.btn[disabled]` раздела 4 отгружено до вехи. */
.form-busy {
  flex: none; display: inline-block; width: 8px; height: 8px; border-radius: var(--r-pill);
  background: var(--text-muted);
  opacity: 0; visibility: hidden;
  /* покой наступает СРАЗУ: гаснем 120 мс, прячемся по окончании гашения */
  transition: opacity .12s linear 0s, visibility 0s linear .12s;
}
.form-busy.htmx-request {
  opacity: 1; visibility: visible;
  /* появление — ТОЛЬКО после порога 300 мс */
  transition: opacity .12s linear .3s, visibility 0s linear .3s;
}
/* Форма обёртки — сама себе система координат: иначе точка отсчитывалась бы от
   ближайшего предка выше по дереву и уехала бы из коробки своей формы. */
.form-wrapper { position: relative; }
.form-wrapper > .form-busy {
  position: absolute; right: 0; bottom: 0; pointer-events: none;
}

/* Индикатор автосохранения ------------------------------------------------- */
/* Состояние «идёт запрос» выражено классом, который htmx вешает на инициатора
   запроса САМ (.htmx-request на форме объявления): собственного таймера и
   собственного состояния в JavaScript нет (D-05), рассинхрон невозможен. */
.autosave {
  min-width: 0; text-align: right;
  font-family: var(--font-mono); font-size: var(--fs-xs);
  letter-spacing: .06em; color: var(--text-muted);
  overflow-wrap: anywhere;
}
.autosave--busy, #ad-form.htmx-request .autosave { color: var(--text-secondary); }
.autosave--error { color: var(--danger); }

.autosave__busy, .autosave__error { display: none; }
#ad-form.htmx-request .autosave__idle { display: none; }
#ad-form.htmx-request .autosave__busy { display: inline; }
.autosave--error .autosave__idle { display: none; }
.autosave--error .autosave__error { display: inline; }

/* Предпросмотр ------------------------------------------------------------- */
.preview { display: flex; flex-direction: column; gap: 12px; min-width: 0; }

/* Заголовок объявления — подпись ПАНЕЛИ, а не часть сообщения: он стоит вне
   рамки и в тело предпросмотра не входит (см. ads/includes/preview.html). */
.preview__title {
  margin: 0; min-width: 0;
  font-size: var(--fs-xl); font-weight: 600; letter-spacing: -.015em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.preview__frame {
  display: flex; flex-direction: column; gap: 12px; min-width: 0;
  padding: 12px; border-radius: var(--r-xl);
  background: var(--surface-input); border: 1px solid var(--border-input);
}

/* Переносящийся ряд миниатюр в порядке отправки — порядок списка ключей
   объявления. Ряд, а не одна картинка со счётчиком: ADS-06 требует показать
   ВСЁ, что уйдёт. */
.preview__media { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
.preview__media img {
  display: block; height: 120px; width: auto; max-width: 100%;
  object-fit: cover; border-radius: var(--r-md); background: var(--surface-pill);
}

/* Кегль и межстрочный совпадают с .field__input--area ДОСЛОВНО: предпросмотр
   обязан переносить строки так же, как поле ввода, иначе он показывает не то,
   что уйдёт (ADS-06). Переносы — pre-wrap + anywhere: неразрывная ссылка иначе
   расширила бы липкую колонку. */
.preview__text {
  margin: 0; min-width: 0;
  font-size: var(--fs-lg); line-height: 1.6; color: var(--text-secondary);
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
}
.preview__time { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-muted); }

/* Карточка расписания в редакторе (План 02-05) ----------------------------- */
/* Полосы прогресса внутри карточки расписания НЕТ и быть не может (D-17): в
   макете она показывает выдуманный процент, источника данных для которого в
   схеме не существует. Селектора под неё в этом разделе не появляется. */
[data-sched-list] { display: flex; flex-direction: column; gap: 16px; min-width: 0; }

[data-sched-card] {
  display: flex; flex-direction: column; min-width: 0;
  border-radius: var(--r-xl);
  background: var(--surface-pill); border: 1px solid var(--border-input);
}

/* Шапка — СТРОКА, которая не переносится на вторую строку: сводка обрезается
   многоточием, а тумблер и элемент разворачивания сохраняют свои позиции
   (UI-SPEC E7 overflow). Высота строки ≥40px — это и есть компенсация
   унаследованной дорожки тумблера высотой 22px: областью нажатия является вся
   подпись, а не дорожка. */
.sched-card__head {
  display: flex; align-items: center; gap: 12px;
  min-width: 0; min-height: 40px; padding: 12px 16px;
}
/* Сводка забирает всё свободное место и обрезается ПЕРВОЙ. min-width: 0 —
   обязателен: без него flex-элемент не сжимается ниже своего содержимого и
   выталкивает тумблер из шапки. */
.sched-card__sum {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 8px;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-lg); color: var(--text-secondary);
}
.sched-card__sum > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Иконка мессенджера — ИСКЛЮЧЕНИЕ из правила выше, и вот почему. Правило `> *`
   писалось под ТЕКСТ сводки, а первым её ребёнком стоит ГРАФИКА: вызов
   includes/messenger_icon.html отдаёт круглый глиф фиксированных 30px. Обоими
   своими свойствами общее правило вредит именно ей: min-width: 0 разрешает
   flex-элементу сжаться ниже собственного содержимого, а overflow: hidden
   превращает не влезший кружок не в переполнение, а в ОБРЕЗКУ по краям
   (issue #44). Обрезаться первым обязан текст, а не графика (UI-SPEC E7
   overflow), поэтому общее правило остаётся как есть — снять его значило бы
   обменять этот дефект на вытолкнутый из шапки тумблер.
   Медиазапроса дефект не получает: сжатие происходит на ЛЮБОЙ ширине, где
   сводка перестаёт помещаться, а ширина из issue — та, на которой её заметил
   сообщивший, а не порог правила.
   Тот же приём по той же причине уже стоит в двух местах:
   `.sched-item__head > .msg` и `[data-group-row] > .avatar`. */
.sched-card__sum > .msg { flex: none; min-width: auto; overflow: visible; }
.sched-card__head > .toggle,
.sched-card__head > form,
.sched-card__head > .badge,
.sched-card__head > .sched-card__expand { flex: none; }
/* Строка тумблера в шапке — цель касания высотой не меньше 40px. */
.sched-card__head .toggle { min-height: 40px; }

.sched-card__expand {
  display: inline-flex; align-items: center; min-height: 36px; padding: 0 8px;
  border-radius: var(--r-sm);
  font-family: var(--font-mono); font-size: var(--fs-xs);
  letter-spacing: .08em; color: var(--text-muted); text-decoration: none;
}
.sched-card__expand:hover { color: var(--text); }
.sched-card__expand:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Тело развёрнутой карточки: внутренний отступ 24, промежутки между
   подблоками 16 (UI-SPEC §Spacing Scale). */
.sched-card__body {
  display: flex; flex-direction: column; gap: 16px;
  min-width: 0; padding: 24px; border-top: 1px solid var(--border-hair);
}

/* Обёртка-форма ПРОДОЛЖАЕТ ритм тела, а не гасит его (issue #45).
   Промежуток flex-контейнера действует между ЕГО ДЕТЬМИ, а прямые дети тела —
   это подсказка и ДВЕ формы: сохранения и удаления. Все три .sched-card__block
   и завершающий .sched-card__actions лежат ВНУТРИ формы сохранения, и потому
   16px родителя до них не доходят вовсе: подпись «ГРУППЫ» вставала вплотную к
   полосе чипсов, «ДНИ И ВРЕМЯ» — к кнопкам пресета, а «Время по …» уходило под
   залитую кнопку сохранения, которая срезала нижние выносные элементы глифов.
   Диагноз подтверждает контрольное наблюдение: зазор между «СОХРАНИТЬ» и
   «УДАЛИТЬ» нормальный — эти кнопки лежат в РАЗНЫХ формах, то есть прямыми
   детьми тела.
   Правится СТИЛЬ, а не разметка: вынести подблоки из формы значило бы перестать
   их отправлять, а имена полей — жёсткий контракт с app/pages/schedules.py
   (докстринг sched_card.html:13-19).
   Селектор обязан нести :has(.sched-card__block) — он различает две формы тела.
   Без него правило захватило бы и форму удаления, чей единственный видимый
   ребёнок — кнопка «УДАЛИТЬ РАСПИСАНИЕ»: раскладка stretch растянула бы опасное
   действие во всю ширину карточки, обменяв один дефект на другой. Механизм не
   новый — этим же :has() в этом разделе держится состояние выбранного чипса.
   Значение contents для display рассмотрено и отвергнуто: тот же зрительный
   результат, но форма исторически выпадает из дерева доступности в части
   браузеров, и собственного бокса у неё не остаётся — min-width объявлять
   стало бы не на чем.
   Величина ВЗЯТА У РОДИТЕЛЯ (.sched-card__body выше), а не подобрана: нового
   токена расстояния дефект не заводит. Медиазапроса — тоже: обёртка гасит
   промежуток на ЛЮБОЙ ширине, а названная в issue 1434px — та, на которой
   дефект заметил сообщивший, а не порог правила.
   Приём в этой таблице не изобретается, а повторяется: [data-form],
   .modal__form, .auth-form, .connect-step__form — те же формы-колонки.
   .sched-item обходится без него только потому, что обёртки между его
   подблоками нет вовсе.
   align-items НЕ объявляется: раскладка по умолчанию (stretch) — ровно та, что
   нужна, подблоки занимают ширину карточки. flex-start, взятый у [data-form],
   сжал бы их по содержимому; там он уместен из-за парного правила
   [data-form] > .field { align-self: stretch; }. */
.sched-card__body > form:has(.sched-card__block) {
  display: flex; flex-direction: column; gap: 16px; min-width: 0;
}
.sched-card__block { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.sched-card__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.sched-card__tz { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-muted); }
.sched-card__hint { font-size: var(--fs-lg); color: var(--warn); }

/* Один примитив в трёх размерах ------------------------------------------- */
/* .chip обслуживает чипсы аккаунтов, ячейки дней и таблетки времени. Отдельных
   классов под каждое не заводится: три набора правил, отличающихся только
   размером, разъехались бы при первой же правке состояния «выбрано».
   Полоса ПЕРЕНОСИТСЯ на любой ширине — горизонтального скролла и обрезки не
   появляется нигде (UI-SPEC §Responsive Contract). */
.chip-set { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }

.chip {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  min-width: 0; max-width: 100%; height: 44px; padding: 0 12px;
  border-radius: var(--r-lg);
  background: var(--surface-input); border: 1px solid var(--border-input);
  color: var(--text-secondary); font-size: var(--fs-lg);
  cursor: pointer;
}
.chip:hover { color: var(--text); border-color: var(--border-control-strong); }
/* Настоящий input, визуально скрытый ЗА подписью: состояние читается формой и
   программой чтения с экрана, работает с клавиатуры и уходит без JavaScript.
   Не display:none — скрытый так элемент выпадает из обхода по Tab. */
.chip__input {
  position: absolute; width: 1px; height: 1px; margin: 0;
  opacity: 0; pointer-events: none;
}
.chip__label {
  min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.chip:has(.chip__input:checked), .chip--on {
  color: var(--text); border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 14%, transparent);
}
.chip:has(.chip__input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.chip:has(.chip__input:disabled) { opacity: .45; cursor: not-allowed; }

/* Чипс-ссылкой: фильтры истории (План 04-06) ------------------------------- */
/* Примитив выше рассчитан на поле ввода ВНУТРИ чипса, поэтому оба состояния —
   выбранность и фокус — выражены через :has(.chip__input:…). У ссылки поля
   ввода нет: состояния берутся с самого элемента.

   Три правила ниже — не украшение, а перебивание базового `a`. Селектор
   `a:hover` (0-1-1) сильнее `.chip--on` (0-1-0), и без них наведение на
   ВЫБРАННЫЙ чипс перекрашивало бы его в цвет ссылки — то есть выбранный чипс
   переставал бы выглядеть выбранным ровно в тот момент, когда на него
   смотрят. Собственных цветов здесь не заводится: значения те же, что у
   примитива. */
a.chip { text-decoration: none; color: var(--text-secondary); }
a.chip:hover { color: var(--text); border-color: var(--border-control-strong); }
a.chip.chip--on, a.chip.chip--on:hover { color: var(--text); }
a.chip:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Полоса групп чипсов. Три группы плюс сброс переносятся на узкой ширине —
   горизонтальной прокрутки и обрезки не возникает (UI-SPEC §Responsive
   Contract). Разделителя между группами нет: группы разделены зазором вдвое
   большим, чем чипсы внутри группы, и на этом читаются как группы. */
.chip-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px;
  min-width: 0; margin: 0 0 12px; padding: 12px 14px;
  border-radius: var(--r-xl);
  background: var(--surface); border: 1px solid var(--border-input);
}
/* Сброс прижимается вправо только там, где для этого есть место: на узкой
   ширине margin-left:auto прижал бы его к правому краю СВОЕЙ строки переноса,
   оставив под группами одинокую надпись у чужого края. */
.chip-bar__reset { text-decoration: none; }
@media (min-width: 861px) {
  .chip-bar__reset { margin-left: auto; }
}

/* Сетка дней: семь ячеек 44×36, переносящихся на узкой ширине. */
.day-grid { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
.day-grid .chip {
  width: 44px; height: 36px; padding: 0;
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .04em;
}

/* Таблетка времени: поле ввода времени плюс кнопка убирания. */
.time-set { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
.time-pill {
  display: inline-flex; align-items: center; gap: 4px;
  height: 36px; padding: 0 4px 0 8px;
  border-radius: var(--r-lg);
  background: var(--surface-input); border: 1px solid var(--border-input);
}
.time-pill__input {
  height: 28px; padding: 0; border: 0; background: transparent;
  color: var(--text); font-family: var(--font-mono); font-size: var(--fs-xs);
}
.time-pill__input:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* 28×28 — минимальная сторона внутри таблетки высотой 36; сама таблетка
   остаётся целью касания. */
.time-pill__remove {
  width: 28px; height: 28px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-sm); background: transparent;
  color: var(--text-muted);
  font-family: var(--font-mono); font-size: var(--fs-lg); line-height: 1;
  cursor: pointer;
}
.time-pill__remove:hover { color: var(--danger); }
.time-pill__remove:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }

/* Выбор групп: строки-флажки высотой 40 ------------------------------------ */
/* Внутреннего скролла и кнопки «показать ещё» здесь НЕТ (UI-SPEC E5 overflow):
   спрятанная группа не была бы выбрана, и рассылка тихо ушла бы не туда. */
.group-pick { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.group-pick__row {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  min-height: 40px; min-width: 0; padding: 0 8px;
  border-radius: var(--r-md);
  background: var(--surface-input); border: 1px solid var(--border-input);
  cursor: pointer;
}
.group-pick__row:hover { border-color: var(--border-control-strong); }
.group-pick__box {
  flex: none; width: 18px; height: 18px; margin: 0;
  accent-color: var(--accent); cursor: pointer;
}
.group-pick__box:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* Имя группы приходит из внешнего мессенджера и не ограничено ничем: без
   min-width: 0 оно вытолкнуло бы счётчик за пределы строки. */
.group-pick__name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-lg); color: var(--text-secondary);
}
.group-pick__count {
  flex: none;
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-muted);
}

@media (max-width: 400px) {
  /* Ячейка дня 44 → 40: семь ячеек по 44 с промежутками 8 не помещаются в
     320px, и сетка переносилась бы на две строки уже на первом дне. */
  .day-grid .chip { width: 40px; }
}

/* Сводка «ключ — значение» ------------------------------------------------- */
/* .kv — СТРОКА. Направляющая черта забирает свободное место, поэтому длинное
   значение переносится на следующую строку под своей подписью, а не
   выталкивает подпись из строки. */
.kv { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; min-width: 0; }
.kv + .kv { margin-top: 8px; }
.kv__k {
  flex: none; font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-muted);
}
.kv__rule {
  flex: 1 1 24px; min-width: 24px; height: 1px;
  border-bottom: 1px dashed var(--border-control);
}
.kv__v {
  flex: none; max-width: 100%; min-width: 0;
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-bright);
  overflow-wrap: anywhere;
}
/* Цвет состояния приходит существующими модификаторами mono-метки: новых имён
   классов под то же самое не заводится. Двойной селектор — из-за порядка
   файла: одиночный .mono--warn проиграл бы .kv__v, объявленному ниже. */
.kv__v.mono--warn { color: var(--warn); }
.kv__v.mono--ok { color: var(--ok); }

/* Карточка сводного списка расписаний (План 02-07) ------------------------- */
/* Контейнер [data-sched-list] объявлен ВЫШЕ и переиспользуется как есть:
   промежуток между карточками 16 одинаков в редакторе и в сводном списке, и
   второе объявление того же имени разъехалось бы с первым.

   Полосы прогресса отправок здесь нет и быть не может (D-17) — ровно по той же
   причине, что и в карточке редактора: процента в данных не существует. */

/* Вводный абзац сводного списка и счётчик найденных. Ритм 24 между абзацем,
   полосой фильтров и списком — шаг xl из UI-SPEC §Spacing Scale. */
.sched-lead {
  margin: 0 0 24px; min-width: 0;
  font-size: var(--fs-lg); line-height: 1.6; color: var(--text-secondary);
}
.sched-count { margin: 0 0 16px; min-width: 0; }

.sched-item {
  display: flex; flex-direction: column; gap: 12px; min-width: 0;
  padding: 16px; border-radius: var(--r-xl);
  background: var(--surface); border: 1px solid var(--border-input);
}

/* Шапка карточки — СТРОКА, которая не переносится: заголовок обрезается
   первым, а тумблер и переход в редактор сохраняют свои позиции при любой
   длине названия объявления (UI-SPEC E13 long-text). Бейджи вынесены строкой
   ниже именно поэтому: «Объявление в черновике» длиннее, чем остаток строки на
   320px, и в шапке он вытолкнул бы органы управления.
   Высота ≥40px — компенсация унаследованной дорожки тумблера в 22px: областью
   нажатия является вся подпись, а не дорожка. */
.sched-item__head {
  display: flex; align-items: center; gap: 12px;
  min-width: 0; min-height: 40px;
}
.sched-item__head > .msg,
.sched-item__head > form,
.sched-item__head > .btn { flex: none; }
.sched-item__head .toggle { min-height: 40px; }

/* min-width: 0 обязателен: без него flex-элемент не сжимается ниже своего
   содержимого, и длинное название вытолкнуло бы тумблер из шапки вместо того,
   чтобы обрезаться. Полное значение живёт в подсказке. */
.sched-item__title {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-xl); font-weight: 600; letter-spacing: -.015em;
}

.sched-item__tags { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }

/* Ячейка дня сводного списка 40×32 — тот же примитив .chip, что в редакторе,
   только меньше и БЕЗ курсора указателя: здесь день показывается, а не
   выбирается. */
.sched-item__days { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
.sched-item__days .chip {
  width: 40px; height: 32px; padding: 0; cursor: default;
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .04em;
}

/* Строки «ключ — значение» приходят существующим примитивом .kv: собственные
   правила под то же самое не заводятся, а вертикальный ритм задаёт .kv + .kv. */
.sched-item__meta { display: flex; flex-direction: column; min-width: 0; }

@media (prefers-reduced-motion: reduce) {
  [data-editor] { animation: none !important; }
}

/* --------------------------------------------------------------------------
   9. Экран групп аккаунта (Фаза 3, план 03-05).

   Дополнение файла санкционировано планом: это единственный списочный экран
   проекта, который остаётся КАРТОЧНЫМ на всех ширинах (UI-SPEC §Responsive
   Contract), поэтому строка-таблица data-row с её подписями ячеек ему не
   подходит, а прецедента карточной строки со своими органами управления в
   разделах 4–8 нет. Ближайший стилевой сосед — «Выбор групп» раздела 8
   (строки-флажки 40px), но у него нет ни аватара, ни двух подписей, ни
   тумблера в строке.

   Всё остальное на экране собрано из отгруженных макросов БЕЗ изменений:
   card / field / filters / empty_state / modal / badge / mono / avatar /
   toggle / button. Новых цветов, радиусов и кеглей не вводится — только токены
   раздела 1. Utility-классов не вводится вовсе (запрет закреплён тестами).

   Фиксированные размеры кратны четырём: плитка канала 48, аватар группы 32
   (задаётся макросом), высота строки 64 = 16 + 32 + 16, минимальная сторона
   органа управления 36, кнопки шапки 40. Шаг 12 — только вертикальный ритм
   между блоками экрана (документированное исключение 4 UI-SPEC §Spacing).
   -------------------------------------------------------------------------- */

/* Шапка аккаунта ----------------------------------------------------------- */
/* Переносящаяся строка: на узкой ширине действия уходят на собственную строку
   сами, без второго медиазапроса. Обрезается ПЕРВЫМ имя аккаунта, а не бейдж
   статуса и не действия — они несут состояние и выход с экрана. */
[data-acct-head] {
  display: flex; align-items: center; flex-wrap: wrap; gap: 16px;
  min-width: 0; margin: 0 0 12px; padding: 16px;
  border-radius: var(--r-xl);
  background: var(--surface); border: 1px solid var(--border-input);
}
.acct-head__plate {
  flex: none; width: 48px; height: 48px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-lg);
  background: var(--surface-pill); border: 1px solid var(--border-input);
}
/* min-width: 0 обязателен: без него flex-элемент не сжимается ниже своего
   содержимого, и длинная строка идентичности вытолкнула бы действия из
   карточки вместо того, чтобы обрезаться. */
.acct-head__id {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.acct-head__name {
  margin: 0; min-width: 0;
  font-size: var(--fs-2xl); font-weight: 600; line-height: 1.2;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.acct-head__id > .mono {
  min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
[data-acct-head] > .badge { flex: none; }
/* Блок статуса — прямой ребёнок шапки на месте бейджа: он заменяется опросом
   ЦЕЛИКОМ, поэтому раскладку обязан держать он сам, а не его содержимое.
   Собственный перенос нужен ветке выполнения: там внутри бейдж И подпись. */
[data-acct-sync] {
  flex: none; min-width: 0;
  display: inline-flex; align-items: center; flex-wrap: wrap; gap: 8px;
}
.acct-head__actions {
  flex: none; margin-left: auto;
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
[data-acct-head] .btn { min-height: 40px; }

/* Значок главного действия вращается ровно пока идёт синхронизация. Признак
   несёт ФОРМА (data-syncing), а не свой класс кнопки: состояние принадлежит
   действию, а не оформлению, и правило переживает смену варианта кнопки.
   Анимация повторяет @keyframes orbit раздела 2 — второго вращения не
   заводится.
   ⚠️ ПОКОЛЕНИЕ (Фаза 11, план 11-17): признак переехал с тега формы на её
   ОБЁРТКУ — тег печатает макрос-обёртка htmx-формы, и атрибутов вызывающего он
   не принимает. Селектор поэтому адресуется носителем признака без имени тега;
   состояние по-прежнему принадлежит действию, а не кнопке. */
[data-acct-head] [data-syncing] .btn__icon {
  animation: orbit 1.4s linear infinite;
}

/* Плашка результата синхронизации ------------------------------------------ */
/* Обёртка нужна ровно за одним: у .alert собственных внешних отступов нет, а
   вертикальный ритм экрана держится на 12px. Сам alert не трогается — он
   отгружен и переиспользуется семью экранами. Плашка занимает полную ширину и
   переносит свой текст, поэтому длинная строка ошибки воркера не расширяет
   страницу (UI-SPEC E2 overflow). */
[data-sync-plashka] { margin: 0 0 12px; min-width: 0; }
[data-sync-plashka] .alert { overflow-wrap: anywhere; }

/* Линейка счётчика --------------------------------------------------------- */
/* Заполняющая черта забирает свободное место, поэтому на узкой ширине правая
   надпись переносится ПОД число, а черта схлопывается до своей минимальной
   ширины — обрезки не возникает (UI-SPEC E3 overflow). Приём — тот же, что у
   .kv__rule раздела 8. */
.count-rule {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  min-width: 0; margin: 0 0 12px;
}
.count-rule__line {
  flex: 1 1 24px; min-width: 24px; height: 1px;
  background: var(--border-soft);
}

/* Список и строка группы --------------------------------------------------- */
[data-group-list] { display: flex; flex-direction: column; gap: 8px; min-width: 0; }

[data-group-row] {
  display: flex; align-items: center; flex-wrap: wrap; gap: 16px;
  min-width: 0; min-height: 64px; padding: 16px;
  border-radius: var(--r-xl);
  background: var(--surface); border: 1px solid var(--border-input);
}
[data-group-row] > .avatar,
[data-group-row] > .group-row__sub,
[data-group-row] > .group-row__mark,
[data-group-row] > form { flex: none; }

/* Имя приходит из внешнего мессенджера и не ограничено ничем: без min-width: 0
   оно вытолкнуло бы органы управления за пределы строки. Полное значение живёт
   в подсказке (title). */
.group-row__name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-lg);
}
.group-row__sub { display: inline-flex; align-items: center; }

/* Выключенная строка приглушена, но состояние несёт НЕ прозрачность: положение
   тумблера и его подсказка остаются главным носителем, а области нажатия не
   уменьшаются (UI-SPEC §Color). */
.group-row--off { opacity: .55; }

/* Пометка «не найдена при синке» (D-11). Точка декоративна и скрыта от
   вспомогательных технологий — состояние несёт текст пометки. */
.group-row__mark { display: inline-flex; align-items: center; gap: 4px; }
.group-row__dot {
  flex: none; width: 8px; height: 8px;
  border-radius: var(--r-circle); background: var(--warn);
}

/* Цель касания: 36 по меньшей стороне у каждого органа управления строки.
   Дорожка тумблера унаследованно 22px — компенсируется высотой его подписи,
   как в разделе 8. */
[data-group-row] .toggle { min-height: 36px; }
[data-group-row] .btn { min-height: 36px; }
/* Форма тумблера несёт ВТОРОЙ орган — резервную кнопку отправки для случая,
   когда Alpine не поднялся (её при живом Alpine снимает x-init). Без строчной
   раскладки этот случай ставил бы кнопку под тумблер и ломал высоту строки;
   при живом Alpine правило не проявляется вовсе — отправлять нечего. */
[data-group-row] form[action$="/toggle"] { display: inline-flex; align-items: center; gap: 6px; }
/* Исключение: точка этой формы ОСТАЁТСЯ предметом строчной выкладки рядом с
   тумблером — наблюдённое и принятое поведение (отметка 6.1.2 обхода Фазы 9),
   подтверждённое отдельным решением владельца от 2026-09-17. Специфичность
   (0,3,1) выше правила области (0,2,0) при любом порядке в файле. */
[data-group-row] form[action$="/toggle"] > .form-busy { position: static; }
/* Разрушительное наведение адресовано ФОРМЕ УДАЛЕНИЯ, а не любой ghost-кнопке
   строки: собственного класса под это не заводится, а маршрут в селекторе
   делает правило точным и переживает появление соседних кнопок. */
[data-group-row] form[action$="/delete"] .btn:hover { color: var(--danger); }

@media (max-width: 400px) {
  /* Строка переносится в две: аватар с именем на первой, подписи и органы
     управления на второй. Базис имени — вся ширина минус аватар и промежуток
     (32 + 8), поэтому перенос происходит ровно после имени, а не в случайном
     месте. Горизонтальной прокрутки и обрезки не появляется. */
  [data-group-row] { gap: 8px; }
  .group-row__name { flex: 1 1 calc(100% - 40px); }
  /* Оба органа управления прижимаются вправо на второй строке: auto стоит на
     ПЕРВОЙ форме, поэтому пара «тумблер + удаление» едет вправо целиком. */
  [data-group-row] > form:first-of-type { margin-left: auto; }
}

/* Вращение значка — единственная анимация раздела, и она декоративна:
   выполнение синхронизации несут подпись кнопки, строка идентичности и блок
   статуса. Отключение движения не отнимает ни одного носителя состояния. */
@media (prefers-reduced-motion: reduce) {
  [data-acct-head] [data-syncing] .btn__icon { animation: none !important; }
}

/* --------------------------------------------------------------------------
   10. Списочная страница объявлений (задача 260825-m0b).

   Раздел `isAds` макета (design/new_broadcaster_design.unpacked.html:466-518) —
   КАРТОЧНАЯ СЕТКА на всех ширинах. Строку-таблицу раздел получил Планом 01-03,
   когда одна форма списочной страницы была применена ко всем пяти разделам
   сразу; Фаза 2 своим объёмом списка объявлений не касалась.

   Дополнение файла санкционировано задачей. Все величины — существующие
   переменные :root раздела 1: новых цветовых констант здесь не появляется ни
   одной, и инлайновых hex в разметке раздела тоже нет.

   Тонов пилюли канала здесь НЕТ И БЫТЬ НЕ МОЖЕТ: их единственное объявление —
   [data-upbadge][data-channel] раздела 5, заведённое дашбордом. Карточка берёт
   тот же признак, а не заводит второй; ниже — только раскладка пилюли внутри
   подвала.
   -------------------------------------------------------------------------- */

/* Ширина трека и зазор перенесены из макета (unpacked.html:479): 290px —
   ширина, ниже которой название и подпись состояния перестают помещаться в одну
   строку шапки. auto-fill, а не auto-fit: одна карточка на широком экране
   обязана остаться КАРТОЧКОЙ в 290px, а не растянуться на всю строку. Раздел
   перестраивается этим правилом САМ — медиазапросов у него нет ни одного, и
   правила строки-таблицы на 860px к нему больше не относятся. */
[data-ads-grid] {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: 16px; min-width: 0;
}
/* Сентинел бесконечной прокрутки — ячейка сетки, а не карточка: без растяжки
   на всю строку он встал бы шестым столбцом рядом с пятой карточкой, и подпись
   «Загрузка...» читалась бы как ещё одно объявление. */
[data-ads-grid] > .empty__hint { grid-column: 1 / -1; }

/* Панель поиска раздела (unpacked.html:468-477). Собственный класс, а не макрос
   свёрнутых фильтров: у раздела ОДНА ось отбора, и прятать одно поле за кнопку
   «Фильтры» значило бы прятать всю панель целиком. Переносится по строкам на
   узкой ширине — поле держит базис, кнопки и счётчик уезжают на вторую строку.
   Ритм 24 между панелью и сеткой — шаг xl из UI-SPEC §Spacing Scale. */
.ads-search {
  display: flex; align-items: flex-end; gap: 9px 12px; flex-wrap: wrap;
  min-width: 0; margin: 0 0 24px;
  padding: 13px 14px; border-radius: var(--r-xl);
  background: var(--surface); border: 1px solid var(--border);
}
/* Поле забирает свободное место панели, но не сжимается ниже 220px — ровно
   тот же базис, что у макета (unpacked.html:469): ниже него заполнитель про
   название и текст перестаёт читаться целиком. */
.ads-search .field { flex: 1 1 220px; min-width: 0; }
/* Счётчик прижат вправо ТЕМ ЖЕ приёмом, что у макета (unpacked.html:476), и
   не сжимается: обрезаться в панели имеет право поле, а не число найденного. */
.ads-search__count { flex: none; margin-left: auto; }

.ad-card {
  display: flex; flex-direction: column; min-width: 0; overflow: hidden;
  border-radius: var(--r-2xl);
  background: var(--surface); border: 1px solid var(--border);
  transition: border-color .2s ease, transform .2s ease;
}
.ad-card:hover { border-color: var(--border-dashed); transform: translateY(-3px); }

/* Обложка ФИКСИРОВАННОЙ высоты: она не зависит от наличия вложения, иначе
   соседние карточки сетки разъезжались бы по высоте. Штриховка собрана на
   существующих переменных поверхностей — она же остаётся заполнителем у
   объявления без вложений, и надписи внутри у неё нет. */
.ad-card__cover {
  height: 124px; flex: none; overflow: hidden;
  background: repeating-linear-gradient(135deg,
    var(--surface-input) 0 8px, var(--surface-pill) 8px 16px);
}
.ad-card__img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* flex: 1 обязателен: без него подвал короткой карточки не прижимается к низу,
   и в сетке из карточек разной длины текста подвалы вставали бы на разной
   высоте. */
.ad-card__body {
  display: flex; flex-direction: column; gap: 10px; flex: 1;
  min-width: 0; padding: 16px;
}

.ad-card__head { display: flex; align-items: center; gap: 8px; min-width: 0; }
/* Точка состояния — ВТОРОЙ носитель того же смысла, что и подпись рядом, а не
   единственный: цветом здесь ничего не сказано такого, чего не сказано словом. */
.ad-card__dot {
  flex: none; width: 7px; height: 7px; border-radius: var(--r-circle);
}
.ad-card__dot--ok { background: var(--ok); }
.ad-card__dot--warn { background: var(--warn); }
/* min-width: 0 обязателен: без него flex-элемент не сжимается ниже своего
   содержимого, и длинное название выдавило бы подпись состояния из шапки
   вместо того, чтобы обрезаться. Полное значение живёт в подсказке. */
.ad-card__title {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-lg); font-weight: 600; letter-spacing: -.01em;
}
/* Подпись состояния прижата вправо ТЕМ ЖЕ приёмом, что у макета
   (unpacked.html:490, margin-left:auto), и flex: none не даёт ей сжаться —
   иначе обрезалось бы слово состояния, а не название. */
.ad-card__head > .mono { flex: none; margin-left: auto; }

/* Свободное место карточки забирает ВЫДЕРЖКА ТЕКСТА, и подвал этим прижимается
   к низу — в сетке из карточек разной длины текста подвалы встают на одной
   высоте. Макет прижимает подвал сам (unpacked.html:493, margin-top:auto), и
   ЗДЕСЬ ЭТОТ ПРИЁМ НЕПРИМЕНИМ: `margin-top: auto` во всём файле объявлен ровно
   один раз — на виджете доступа сайдбара, который утаскивает за собой к низу
   блок пользователя, — и единственность объявления закреплена проверкой
   test_the_bottom_pinning_rides_with_the_widget_selector. Второе объявление
   покрасило бы её, а ослаблять чужой ограничитель ради подвала карточки нельзя:
   он ловит настоящую поломку сайдбара на всех 26 страницах. Исход тот же,
   средство другое. */
.ad-card__text {
  margin: 0; min-width: 0; flex: 1 1 auto;
  font-size: var(--fs-sm); line-height: 1.55; color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* Подвал отбит линией, как у макета (unpacked.html:493). Переносится по
   строкам: на узкой карточке пилюли, метаданные и действия не помещаются в
   одну. */
.ad-card__foot {
  display: flex; align-items: center; gap: 8px 12px;
  flex-wrap: wrap; min-width: 0; flex: none;
  padding-top: 12px; border-top: 1px solid var(--border-muted);
}

/* Раскладка пилюль — ТОЛЬКО раскладка: тон каждой приходит существующими
   правилами [data-upbadge][data-channel] раздела 5. */
.ad-card__chans {
  display: inline-flex; align-items: center; gap: 4px; flex: none;
  flex-wrap: wrap;
}
/* Число расписаний канала стоит РЯДОМ с пилюлей, а не внутри неё: внутрь его
   можно было бы положить только собственным фоном и собственным тоном, то есть
   второй копией правил пилюли канала. Копии не заводится — примитив
   переиспользуется как есть, а число называет себя подсказкой пилюли. */
.ad-card__chan-count {
  flex: none; font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: .06em; color: var(--text-muted);
}

/* Метаданные — существующий примитив .kv, поставленный В СТРОКУ: вертикальный
   ритм .kv + .kv здесь снимается, потому что в подвале карточки три пары
   «ключ — значение» стоят рядом, а не столбцом. Направляющая черта .kv__rule в
   подвал не ставится: она забирает свободное место, которого здесь нет. */
.ad-card__meta {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  min-width: 0; flex: 1 1 auto;
}
.ad-card__meta .kv + .kv { margin-top: 0; }

/* Действия прижаты вправо: «Изменить» стоит там же, где у макета
   (unpacked.html:505). Удаление рядом с ним — названное отступление от макета
   (WR-04): без него на странице не осталось бы ни одного пути удалить
   объявление. */
.ad-card__actions {
  display: flex; align-items: center; gap: 4px; flex: none; margin-left: auto;
}
/* Разрушительное наведение адресовано ФОРМЕ УДАЛЕНИЯ, а не любой ghost-кнопке
   карточки: тот же приём, что у строки группы в разделе 9. */
.ad-card__actions form[action$="/delete"] .btn:hover { color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
  /* Подъём карточки на наведении — единственное движение раздела, и оно
     декоративно: состояние, каналы и метаданные несут слова, а не движение. */
  .ad-card { transition: none !important; }
  .ad-card:hover { transform: none !important; }
}

/* --------------------------------------------------------------------------
   11. Списочная страница аккаунтов (задача 260825-of5).

   Раздел `isAccounts` макета (design/new_broadcaster_design.unpacked.html:877-903)
   — КАРТОЧНАЯ СЕТКА на всех ширинах. Строку-таблицу раздел получил Планом 01-03,
   когда одна форма списочной страницы была применена ко всем пяти разделам
   сразу; Фаза 6 своим объёмом брала подключение аккаунтов и опрос статуса, а
   формой списка не занималась.

   Дополнение файла санкционировано задачей. Все величины — существующие
   переменные :root раздела 1: новых цветовых констант здесь не появляется ни
   одной, и инлайновых hex в разметке раздела тоже нет.

   Тонов плашки канала здесь НЕТ И БЫТЬ НЕ МОЖЕТ: их единственное объявление —
   [data-upbadge][data-channel] раздела 5, заведённое дашбордом. Карточка берёт
   ТОТ ЖЕ признак, а не заводит второй; ниже — только ГЕОМЕТРИЯ плашки.
   Кадрового набора пульсации здесь тоже нет: точка состояния берёт
   существующий @keyframes pulse раздела 2, второго не заводится.
   -------------------------------------------------------------------------- */

/* Ширина трека и зазор перенесены из макета (unpacked.html:878): 300px — ширина,
   ниже которой подпись канала и точка состояния перестают помещаться в одну
   строку шапки. auto-fill, а не auto-fit: один аккаунт на широком экране обязан
   остаться КАРТОЧКОЙ в 300px, а не растянуться на всю строку. Раздел
   перестраивается этим правилом САМ — медиазапросов у него нет ни одного, и
   правила строки-таблицы на 860px к нему больше не относятся. */
[data-accounts-grid] {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 16px; min-width: 0;
}
/* Сентинел бесконечной прокрутки — ячейка сетки, а не карточка: без растяжки
   на всю строку он встал бы очередной колонкой рядом с последней карточкой, и
   подпись «Загрузка...» читалась бы как ещё один аккаунт. */
[data-accounts-grid] > .empty__hint { grid-column: 1 / -1; }

/* Отступ, радиус и зазор колонки — из макета (unpacked.html:880), величины взяты
   существующими переменными. */
.acct-card {
  display: flex; flex-direction: column; gap: 15px; min-width: 0;
  padding: 20px; border-radius: var(--r-2xl);
  background: var(--surface); border: 1px solid var(--border);
  transition: border-color .2s ease, transform .2s ease;
}
.acct-card:hover { border-color: var(--border-dashed); transform: translateY(-3px); }

.acct-card__head { display: flex; align-items: center; gap: 12px; min-width: 0; }

/* Плашка канала: ГЕОМЕТРИЯ, и только она. Тон приходит правилами
   [data-upbadge][data-channel] .msg раздела 5 — они объявлены выше и имеют
   бо́льшую специфичность, поэтому фон и цвет глифа сюда не выписываются: вторая
   копия тех же трёх тонов разъехалась бы с первой молча. Размеры — из макета
   (unpacked.html:882): плашка 40px, глиф 19px. */
.acct-card__ch .msg {
  width: 40px; height: 40px; flex: none;
  padding: 0; border-radius: var(--r-xl);
  display: grid; place-items: center;
}
.acct-card__ch .msg__glyph { width: 19px; height: 19px; }

/* min-width: 0 обязателен: без него flex-элемент не сжимается ниже своего
   содержимого, и длинная подпись канала выдавила бы точку состояния из шапки. */
.acct-card__ident {
  display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto;
}
.acct-card__chan {
  min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: var(--fs-lg); font-weight: 600; letter-spacing: -.01em;
}

/* Точка состояния прижата вправо ТЕМ ЖЕ приёмом, что у макета
   (unpacked.html:891, margin-left:auto), и flex: none не даёт ей сжаться.
   Цвет здесь не единственный носитель смысла: слово состояния стоит строкой
   ниже, ключом «Состояние». */
.acct-card__dot {
  flex: none; margin-left: auto; width: 9px; height: 9px;
  border-radius: var(--r-circle);
}
.acct-card__dot--ok { background: var(--ok); }
.acct-card__dot--warn { background: var(--warn); }
.acct-card__dot--danger { background: var(--danger); }
.acct-card__dot--muted { background: var(--text-muted); }
/* Пульсация у идущей синхронизации — существующий кадровый набор раздела 2. */
.acct-card__dot--live { animation: pulse 1.4s ease-in-out infinite; }

/* Тело карточки — существующий примитив .kv, поставленный СТОЛБЦОМ: у макета
   это тот же столбец пар «ключ — значение» с зазором 7px (unpacked.html:893).
   Вертикальный ритм .kv + .kv снимается — его заменяет gap контейнера.
   Значение прижато вправо, как у макета (justify-content:space-between):
   правило адресовано ПОСЛЕДНЕМУ ребёнку строки, потому что значением может быть
   и .kv__v, и mono-метка состояния со своим тоном.

   Свободное место карточки забирает ИМЕННО ЭТОТ блок (flex: 1 1 auto), и ряд
   действий этим прижимается к низу — в сетке из карточек с разным числом
   заполненных значений подвалы встают на одной высоте. Макет прижимает подвал
   сам (unpacked.html:897, margin-top:auto), и ЗДЕСЬ ЭТОТ ПРИЁМ НЕПРИМЕНИМ:
   `margin-top: auto` во всём файле объявлен ровно один раз — на виджете доступа
   сайдбара, — и единственность закреплена проверкой
   test_the_bottom_pinning_rides_with_the_widget_selector. Исход тот же, средство
   другое; ровно так же обошёлся раздел 10. */
.acct-card__kv {
  display: flex; flex-direction: column; gap: 7px; min-width: 0; flex: 1 1 auto;
}
.acct-card__kv .kv + .kv { margin-top: 0; }
.acct-card__kv .kv > :last-child { margin-left: auto; text-align: right; }

/* Ряд действий подвала. Вход на экран групп забирает свободное место ряда — у
   макета он растянут (unpacked.html:898); разрушительное действие и повтор
   синхронизации не сжимаются. */
.acct-card__actions {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; min-width: 0; flex: none;
}
.acct-card__actions > .btn:first-child { flex: 1 1 auto; justify-content: center; }
.acct-card__actions form { flex: none; }
/* Разрушительное наведение адресовано ФОРМЕ УДАЛЕНИЯ, а не любой ghost-кнопке
   карточки: тот же приём, что у карточки объявления в разделе 10. */
.acct-card__actions form[action$="/delete"] .btn:hover { color: var(--danger); }

@media (prefers-reduced-motion: reduce) {
  /* Подъём карточки и пульсация точки — единственные движения раздела, и оба
     декоративны: состояние несёт слово, стоящее ключом «Состояние». */
  .acct-card { transition: none !important; }
  .acct-card:hover { transform: none !important; }
  .acct-card__dot--live { animation: none !important; }
}
