/* ============================================================
   ШРИФТЫ. Лежат локально в core/fonts/ — интерфейс не делает ни одного
   запроса наружу: на проде нет зависимости от доступности Google, а
   персональные данные не утекают в чужие логи запросом шрифта.

   Golos Text (F-111, выбор заказчика 08.08.2026) — переменный, один файл
   на подмножество отдаёт все начертания 400–700. Лицензия OFL, тексты
   рядом с файлами. IBM Plex Mono — цифры, суммы, телефоны.
   ============================================================ */
@font-face {
  font-family: "Golos Text";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/golos-text-cyrillic.024646dc00da.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: "Golos Text";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/golos-text-cyrillic-ext.52208e41e353.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: "Golos Text";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/golos-text-latin.3ba0fde64bcf.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: "Golos Text";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/golos-text-latin-ext.1cfacdd6d054.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-cyrillic.cd34c7793d97.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-cyrillic-ext.f572f7b57d27.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-latin.814fe1615d4d.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: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-400-latin-ext.17a5b2a15bf5.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-cyrillic.0ba762bde65a.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-cyrillic-ext.de1d49f26130.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-latin.6708425c446b.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-latin-ext.216ee1a35b06.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;
}

/* ============================================================
   ДИЗАЙН-СИСТЕМА. Единственный источник — этот :root.
   Перекраска приложения — правкой токенов, не поиском по файлу.
   Сырые hex/rgba вне :root запрещены; оттенки — color-mix от токена.
   ============================================================ */
:root {
  /* Поверхности и текст */
  --color-page: #eff1f5;
  --color-surface: #ffffff;
  --color-surface-raised: #ffffff;
  --color-surface-sunken: #f5f7fa;
  --color-text: #1a2230;
  --color-muted: #64748b;
  --color-faint: #94a3b8;
  --color-border: #dde3ec;
  --color-border-strong: #c3ccd9;
  --color-on-primary: #ffffff;

  /* Сайдбар — тёмный: навигация отделена от рабочей области */
  --color-sidebar: #1c2431;
  --color-sidebar-text: #cbd5e1;
  --color-sidebar-muted: #64748b;
  --color-sidebar-hover: #27303f;
  --color-sidebar-active: #2f6fd0;
  --color-sidebar-active-text: #ffffff;
  --color-sidebar-border: #2a3342;

  /* Акцент */
  --color-primary: #2f6fd0;
  --color-primary-hover: #285fb4;
  --color-primary-active: #224f96;
  --color-primary-soft: #e8effb;
  --color-focus: #2f6fd0;

  /* Статусы: фон / текст / граница + сплошной для индикаторов */
  --color-success-bg: #e4f2e9;
  --color-success-text: #21663e;
  --color-success-border: #b5dcc4;
  --color-success-solid: #2e8b57;
  --color-warning-bg: #faf0d3;
  --color-warning-text: #75560e;
  --color-warning-border: #e8d494;
  --color-warning-solid: #d99a06;
  --color-danger-bg: #f9e7e4;
  --color-danger-text: #9e3323;
  --color-danger-border: #eec4bd;
  --color-danger-solid: #c73a26;
  --color-neutral-bg: #e8ecf3;
  --color-neutral-text: #465063;
  --color-neutral-border: #cfd7e3;

  /* Канал Авито */
  --color-avito-bg: #e2f4fd;
  --color-avito-text: #086da8;
  --color-avito-border: #b3e0f7;
  --color-avito-solid: #1fa3e0;

  /* Стадии воронки: открытые — холодный→тёплый по ходу воронки,
     закрытые — семантика исхода. Контраст каждого ≥3:1 к --color-page
     и к --color-surface (WCAG 1.4.11, проверено расчётом 07.08.2026).
     Маппинг стадии на тон — фильтр stage_tone (apps/core/templatetags). */
  --stage-1: #64748b; /* 4.2:1 */
  --stage-2: #2f6fd0; /* 4.3:1 */
  --stage-3: #7053c7; /* 5.0:1 */
  --stage-4: #9c4382; /* 5.3:1 */
  --stage-5: #a06f03; /* 3.9:1 */
  --stage-6: #b45511; /* 4.4:1 */
  --stage-won: var(--color-success-solid);  /* 3.8:1 */
  --stage-lost: var(--color-danger-solid);  /* 4.6:1 */

  /* Тона кружка собеседника. Восемь — по числу, которое считает
     apps/core/avatars.py; разъедутся — часть людей останется без цвета,
     сторожит tests_avatars.py.

     🔴 Это НЕ статусные цвета и не цвета стадий: тон здесь не значит ничего,
     кроме «это тот же человек, что и вчера». Поэтому свой набор, а не
     переиспользованные --stage-*: у стадии цвет несёт смысл, и одолжить его
     под другое значило бы научить менеджера читать цвет двумя способами.

     Контраст замерен, не подобран на глаз (23.08.2026): белая буква на тоне —
     от 5.09:1 (тон 3) до 7.38:1 (тон 8) при пороге AA 4.5 для текста.
     Тронешь тон — пересчитай, иначе часть имён станет нечитаемой молча.

     🔴 Серого среди восьми нет намеренно. Тон 8 был #4a5568 — тот же серо-синий,
     от которого весь этот кружок и уводит: каждый восьмой собеседник получал бы
     почти серый круг. Ревью 23.08.2026 это назвало, заменён на коричневый. */
  --avatar-tone-1: #2c5fa8;
  --avatar-tone-2: #0f766e;
  --avatar-tone-3: #4f7a16;
  --avatar-tone-4: #9a5b07;
  --avatar-tone-5: #b03f1d;
  --avatar-tone-6: #a03063;
  --avatar-tone-7: #6544b5;
  --avatar-tone-8: #7a4a2e;

  /* Типографика. Моно — суммы, номера, телефоны.
     Смена шрифта интерфейса — правка одной строки --font-base. */
  --font-base: "Golos Text", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 16px;
  --text-lg: 18px;
  --text-xl: 22px;
  --text-2xl: 28px;
  --leading-tight: 1.3;
  --leading-normal: 1.5;
  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Спейсинг, шаг 4px */
  --space-05: 2px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 40px;

  /* Форма */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;
  --radius-full: 999px;
  --shadow-sm: 0 1px 2px color-mix(in srgb, var(--color-text) 6%, transparent);
  --shadow-md: 0 4px 12px color-mix(in srgb, var(--color-text) 12%, transparent);
  --focus-ring: 0 0 0 3px color-mix(in srgb, var(--color-focus) 22%, transparent);

  /* Радиус по роли, а не по вкусу. Одинаковое скругление на всём подряд —
     признак брака из списка заказчика: форма перестаёт различать элементы.
     Панель мягче контрола, контрол мягче поля ввода, пилюля круглая. */
  --radius-panel: var(--radius-lg);
  --radius-control: var(--radius-md);
  --radius-input: var(--radius-sm);
  --radius-pill: var(--radius-full);

  /* Высота по роли. Тень означает «элемент физически над страницей»:
     всплывающее меню, модалка, липкая панель. Обычная панель рабочей
     области лежит в плоскости страницы — её отделяет граница, не тень. */
  --elevation-flat: none;
  --elevation-raised: var(--shadow-sm);
  --elevation-overlay: var(--shadow-md);

  /* Состояния — слой поверх собственного фона элемента, один на всё
     приложение. Не «свой оттенок серого в каждом компоненте». */
  --state-hover: color-mix(in srgb, var(--color-text) 5%, transparent);
  --state-active: color-mix(in srgb, var(--color-text) 9%, transparent);
  --state-selected: var(--color-primary-soft);
  --opacity-disabled: 0.5;

  /* Подпись поля. Капслок жирным на каждой подписи — крик: подпись
     становится заметнее значения, которое менеджер и пришёл прочитать. */
  --label-size: var(--text-xs);
  --label-weight: var(--weight-normal);
  --label-color: var(--color-muted);
  --label-transform: none;
  --label-tracking: 0;

  /* Цвета RAL/RR (образцы профлиста и черепицы) сюда НЕ попадают: это
     данные каталога, а не тема. Они живут в data-hex карточки образца и
     красят квадратик скриптом — в разметке сырых hex всё равно нет. */

  /* Движение */
  --transition-fast: 120ms ease;
  --transition: 180ms ease;

  /* Размеры */
  --icon-sm: 14px;
  --icon-md: 16px;
  --icon-lg: 18px;
  --control-h: 34px;
  --control-h-lg: 40px;
  --table-row-height: 44px;
  --sidebar-width: 216px;

  /* Сетка проекта (F-108, числа — docs/reference/layout-rules.md).
     --field-min: колонка формы не уже самой длинной подписи варианта
     (30 символов на 13px ≈ 260px + внутренние отступы селекта).
     --field-max: поле шире 480px нечитаемо — глаз теряет начало строки.
     --pane-min: панель многоколоночного экрана уже 260px рвёт подписи. */
  --field-min: 280px;
  --field-max: 480px;
  --pane-min: 260px;

  /* Слои */
  --z-sticky: 10;
  --z-dropdown: 40;
  --z-modal: 100;

  /* Высота нижней панели разделов на телефоне (F-220). Живёт токеном, потому
     что её знают трое: сама панель, отступ снизу у содержимого и полоса итога
     калькулятора, которая обязана стоять НАД панелью. Разъедутся числа —
     кнопка расчёта уедет под панель, и никто не заметит. */
  --nav-bar-height: 57px;
  --nav-bar-space: calc(var(--nav-bar-height) + env(safe-area-inset-bottom));
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--color-page);
  color: var(--color-text);
  font-family: var(--font-base);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}

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

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

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--color-focus) 22%, transparent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  min-height: 100vh;
}

.sidebar {
  background: var(--color-sidebar);
  color: var(--color-sidebar-text);
  padding: var(--space-4) var(--space-3);
}

.brand {
  color: var(--color-on-primary);
  display: block;
  margin-bottom: var(--space-5);
  padding: var(--space-2) var(--space-3);
}

.brand:hover {
  color: var(--color-on-primary);
  text-decoration: none;
}

.brand strong {
  display: block;
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
}

.brand small {
  color: var(--color-sidebar-muted);
  display: block;
  font-size: var(--text-xs);
  margin-top: var(--space-05);
}

.nav-list {
  display: grid;
  gap: var(--space-05);
}

.nav-item {
  align-items: center;
  border-radius: var(--radius-md);
  color: var(--color-sidebar-text);
  display: flex;
  font-weight: var(--weight-medium);
  gap: var(--space-2);
  min-height: 36px;
  padding: var(--space-2) var(--space-3);
  transition: background var(--transition-fast), color var(--transition-fast);
}

.nav-item:hover {
  background: var(--color-sidebar-hover);
  color: var(--color-on-primary);
  text-decoration: none;
}

/* Иконки — единый набор Lucide, спрайт core/icons.svg, компонент c-icon.
   Красятся currentColor: цвет задаёт родитель, не сама иконка. */
.icon {
  flex-shrink: 0;
  height: var(--icon-md);
  vertical-align: -0.15em;
  width: var(--icon-md);
}

.icon--sm {
  height: var(--icon-sm);
  width: var(--icon-sm);
}

.icon--lg {
  height: var(--icon-lg);
  width: var(--icon-lg);
}

/* ── Кружок собеседника (компонент c-avatar) ─────────────────────────
   Фотография, а когда её нет — знак из набора Thumbs. Фотографий сегодня нет
   ни одной (0 из 116 в Telegram, 0 из 15 в MAX, замер 22.08.2026), поэтому
   заглушка и есть обычный вид кружка, а не редкий случай.

   Вид выбран владельцем 23.08.2026 после показа наборов; лицензия CC0, почему
   не настоящие мемы — в apps/core/avatars.py. Переоткрывать не надо.

   Размер здесь один на все места намеренно: 28 px — то, ради чего кружок
   существует, узнавание боковым зрением, и он же не даёт ленте распухнуть.
   -------------------------------------------------------------------- */
.avatar {
  align-items: center;
  border-radius: var(--radius-full);
  display: inline-flex;
  flex-shrink: 0;
  height: 28px;
  justify-content: center;
  overflow: hidden;
  user-select: none;
  width: 28px;
}

.avatar--photo { object-fit: cover; }

/* Тон — единственное, что отличает одного человека от другого. Класс ставит
   переменную, дальше все три вида заглушки читают только её. */
.avatar--tone-1 { --avatar-color: var(--avatar-tone-1); }
.avatar--tone-2 { --avatar-color: var(--avatar-tone-2); }
.avatar--tone-3 { --avatar-color: var(--avatar-tone-3); }
.avatar--tone-4 { --avatar-color: var(--avatar-tone-4); }
.avatar--tone-5 { --avatar-color: var(--avatar-tone-5); }
.avatar--tone-6 { --avatar-color: var(--avatar-tone-6); }
.avatar--tone-7 { --avatar-color: var(--avatar-tone-7); }
.avatar--tone-8 { --avatar-color: var(--avatar-tone-8); }

/* Знак на плотном тоне: тело знака светлое, тон — фоном кружка. Так цвет
   принадлежит человеку, а рисунок остаётся читаемым на всех восьми тонах
   (белое тело на тоне — 5.09…7.38:1, тот же замер, что для буквы). */
.avatar--thumb {
  background: var(--avatar-color);
  color: var(--color-surface);
}

.avatar-thumb {
  height: 100%;
  width: 100%;
}

/* Имени нет вовсе — кружок остаётся пустым и тихим: выдуманный знак хуже
   пустоты, а плотный тон без рисунка читался бы как «тут что-то не
   загрузилось». Так стоят сигналы площадки. */
.avatar--empty {
  background: color-mix(in srgb, var(--avatar-color) 18%, var(--color-surface));
}

.nav-icon,
.icon.nav-icon {
  flex: 0 0 var(--icon-lg);
  height: var(--icon-lg);
  width: var(--icon-lg);
}

.nav-item.active {
  background: var(--color-sidebar-active);
  color: var(--color-sidebar-active-text);
  font-weight: var(--weight-semibold);
}

.nav-item.disabled {
  color: var(--color-sidebar-muted);
  cursor: default;
}

.nav-item.disabled:hover {
  background: transparent;
  color: var(--color-sidebar-muted);
}

/* «Ещё» — пятый слот нижней панели и только он (F-220). В сайдбаре кнопки нет:
   там видны все разделы сразу, и прятать было бы нечего. */
.nav-more {
  display: none;
}

.sidebar-note {
  border-top: 1px solid var(--color-sidebar-border);
  color: var(--color-sidebar-muted);
  font-size: var(--text-xs);
  margin-top: var(--space-6);
  padding-top: var(--space-4);
}

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

/* Шапка — 48 px вместо 72 (F-212). На 1280×800 каждый ярус над списком стоит
   одной строки данных, а на название раздела уходило 9% высоты экрана. */
.topbar {
  align-items: center;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-4);
  justify-content: space-between;
  min-height: 48px;
  padding: var(--space-2) var(--space-6);
}

.topbar-title {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
}

.topbar-title .eyebrow {
  margin: 0;
}

/* Служебные кнопки шапки — 32 px. Кнопка уведомлений держала 40 и одна задавала
   высоту всей шапки, а с ней и высоту, на которую съезжал список. */
.topbar :is(.notification-button, .secondary-button, .admin-link, .ghost-button) {
  min-height: 32px;
}

.topbar h1,
.summary-main h2,
.panel-header h2,
.info-panel h2 {
  margin: 0;
}

.topbar h1 {
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1.25;
}

.topbar-actions {
  align-items: center;
  display: flex;
  flex: 0 0 auto;
  gap: var(--space-3);
}

/* Вкладки видов внутри шапки: своей подложки и своей нижней линии у них здесь
   нет — линия шапки уже есть, вторая читалась бы двойным разделителем. */
/* Вкладок может стать четыре, и переносить их нельзя: перенос вырастил бы
   шапку и вернул список туда, откуда его подняли. Поэтому строка одна, а
   лишнее уезжает горизонтальной прокруткой. */
.view-tabs--topbar {
  align-items: center;
  border-bottom: 0;
  flex-wrap: nowrap;
  margin: 0;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}

.view-tabs--topbar .view-tab {
  padding: var(--space-1) var(--space-2);
  white-space: nowrap;
}

/* Полоса «Найдено / Таблица» стоит в шапке справа от вкладок и прижата к
   действиям аккаунта. `margin-left: auto` у неё, а не у `.topbar-actions`:
   тогда пустое место остаётся между вкладками экрана и служебными кнопками. */
.topbar .deal-result-bar {
  flex: 0 0 auto;
  margin: 0 0 0 auto;
  min-height: 0;
}

.topbar .deal-result-bar .record-count {
  margin: 0;
  white-space: nowrap;
}

/* На узком экране шапка встаёт в столбец, и надзаголовок из двух слов забирает
   у неё целую строку, ничего не сообщая: раздел уже назван в боковом меню и в
   заголовке. Действия аккаунта переносятся, иначе «Django admin» уезжает за
   правый край — так было и до правки. */
@media (max-width: 1100px) {
  .topbar-title .eyebrow {
    display: none;
  }

  .topbar-actions {
    flex-wrap: wrap;
  }
}

/* --- Кнопки: primary / secondary / ghost / danger, единые состояния --- */
.admin-link,
.row-action,
.primary-action,
.primary-button,
.secondary-button,
.ghost-button,
.danger-button,
button {
  align-items: center;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  cursor: pointer;
  display: inline-flex;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  gap: var(--space-2);
  justify-content: center;
  min-height: var(--control-h);
  padding: 6px 12px;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.admin-link,
.row-action,
.secondary-button {
  background: var(--color-surface);
  color: var(--color-text);
}

.admin-link:hover,
.row-action:hover,
.secondary-button:hover {
  background: var(--color-surface-sunken);
  border-color: var(--color-border-strong);
  color: var(--color-text);
  text-decoration: none;
}

.primary-action,
.primary-button,
button {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
  text-decoration: none;
}

.primary-action:hover,
.primary-button:hover,
button:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  color: var(--color-on-primary);
  text-decoration: none;
}

.primary-action:active,
.primary-button:active,
button:active {
  background: var(--color-primary-active);
}

button.secondary-button,
button.ghost-button,
button.danger-button {
  background: var(--color-surface);
}

button.secondary-button { color: var(--color-text); }
button.secondary-button:hover { background: var(--color-surface-sunken); }

.ghost-button,
button.ghost-button {
  background: transparent;
  border-color: transparent;
  color: var(--color-muted);
}

.ghost-button:hover,
button.ghost-button:hover {
  background: var(--color-surface-sunken);
  border-color: transparent;
  color: var(--color-text);
}

.danger-button,
button.danger-button {
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

.danger-button:hover,
button.danger-button:hover {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

/* Состояния: описаны один раз, работают на всём */
button:disabled,
.primary-button[aria-disabled="true"],
.secondary-button[aria-disabled="true"],
input:disabled,
select:disabled,
textarea:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

button:disabled:hover {
  background: var(--color-primary);
}

button.secondary-button:disabled:hover,
.row-action[aria-disabled="true"]:hover {
  background: var(--color-surface);
}

/* Загрузка: htmx-indicator показывается на время запроса */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline-flex; }
.htmx-request button[type="submit"],
button.htmx-request {
  cursor: progress;
  opacity: 0.7;
  pointer-events: none;
}

.spinner {
  animation: spin 700ms linear infinite;
  border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
  border-radius: var(--radius-full);
  border-top-color: currentColor;
  height: var(--icon-sm);
  width: var(--icon-sm);
}

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

/* Сверху 4 px, а не 24: между линией шапки и первым блоком экрана воздух не
   нужен — там уже есть разделитель, а высота на 1280×800 конечна (F-212). */
.content {
  padding: var(--space-1) var(--space-6) var(--space-6);
}

.page-actions {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  margin-bottom: var(--space-4);
}

.logout-form {
  margin: 0;
}

.prototype-note {
  background: var(--color-warning-bg);
  border-bottom: 1px solid var(--color-warning-border);
  color: var(--color-warning-text);
  margin: 0;
  padding: var(--space-3) var(--space-6);
}

.eyebrow {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0;
  margin: 0 0 var(--space-1);
  text-transform: uppercase;
}

.toolbar-actions {
  display: flex;
  gap: var(--space-2);
}

.toolbar {
  align-items: end;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-3);
  grid-template-columns: minmax(240px, 1.5fr) minmax(170px, 1fr) minmax(190px, 1fr) auto;
  margin-bottom: var(--space-4);
  padding: var(--space-4);
}

label {
  color: var(--label-color);
  display: grid;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  gap: var(--space-1);
}

/* Подпись поля отдельным узлом: нужна там, где рядом с текстом стоит
   маркер обязательности, и там, где подпись должна занимать свою строку. */
.field-caption {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: var(--label-transform);
}

input,
select,
textarea {
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-base);
  min-height: var(--control-h);
  padding: 6px 10px;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

input:hover,
select:hover,
textarea:hover {
  border-color: var(--color-faint);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--color-focus);
  outline: none;
  box-shadow: var(--focus-ring);
}

/* ── Обязательные поля (F-045) ────────────────────────────
   Жалоба менеджеров: пустое обязательное поле молча отклоняет отправку,
   и кажется, что кнопка сломана. Паттерн один на все формы проекта:

   1) до отправки — маркер у подписи, чтобы поле не стало сюрпризом;
   2) после ухода из поля или первой попытки отправки — красная граница
      и мягкая заливка. Именно :user-invalid, а не :invalid: последний
      красит пустое поле сразу при открытии формы и обесценивает сигнал;
   3) причина — строкой под полем (.field-error), не общим списком сверху.

   Маркер ставится разметкой (<span class="req">) там, где подпись пишем
   мы, и правилом ниже — там, где форму рисует Django (as_div): шаблоны
   Django-форм ради звёздочки не переписываем. */
.req {
  color: var(--color-danger-solid);
  font-weight: var(--weight-semibold);
  text-decoration: none;
}

.entity-form div:has(> input[required], > select[required], > textarea[required]) > label::after,
.form-section div:has(> input[required], > select[required], > textarea[required]) > label::after {
  color: var(--color-danger-solid);
  content: " *";
  font-weight: var(--weight-semibold);
}

input:user-invalid,
select:user-invalid,
textarea:user-invalid {
  background: color-mix(in srgb, var(--color-danger-bg) 55%, var(--color-surface));
  border-color: var(--color-danger-solid);
}

input:user-invalid:focus,
select:user-invalid:focus,
textarea:user-invalid:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger-solid) 20%, transparent);
}

.field-error,
.errorlist {
  color: var(--color-danger-text);
  font-size: var(--text-xs);
  list-style: none;
  margin: var(--space-05) 0 0;
  padding: 0;
}

select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2364748b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
  background-position: right 10px center;
  background-repeat: no-repeat;
  padding-right: 28px;
}

input[type="checkbox"],
input[type="radio"] {
  appearance: auto;
  -webkit-appearance: auto;
  accent-color: var(--color-primary);
  background: none;
  min-height: auto;
}

textarea {
  line-height: var(--leading-normal);
  min-height: 64px;
  resize: vertical;
}

.table-panel,
.summary-panel,
.info-panel,
.mobile-lead-card,
.empty-state {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-flat);
}

.panel-header {
  align-items: start;
  border-bottom: 1px solid var(--color-border);
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4);
}

.panel-header h2,
.info-panel h2 {
  font-size: var(--text-lg);
  font-weight: 700;
}

.panel-header p,
.summary-main p,
.info-panel p {
  color: var(--color-muted);
  margin: var(--space-1) 0 0;
}

.record-count,
.status-pill,
.status-badge {
  border-radius: 999px;
  display: inline-flex;
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1;
  padding: 5px 8px;
  white-space: nowrap;
}

.record-count,
.status-pill.neutral,
.status-pill {
  background: var(--color-neutral-bg);
  color: var(--color-neutral-text);
}

.table-scroll {
  overflow-x: auto;
}

table {
  border-collapse: collapse;
  width: 100%;
}

th,
td {
  border-bottom: 1px solid var(--color-border);
  padding: 10px var(--space-4);
  text-align: left;
  vertical-align: middle;
}

th {
  background: var(--color-surface-sunken);
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 700;
  white-space: nowrap;
}

tbody tr {
  height: var(--table-row-height);
}

tbody tr:hover {
  background: var(--color-surface-sunken);
}

.lead-title {
  color: var(--color-text);
  display: block;
  font-weight: var(--weight-semibold);
}

.lead-title:hover {
  color: var(--color-primary-hover);
}

/* F-231. Знак канала у названия лида: 16 px справа от него, названию —
   вся оставшаяся ширина. `float`, а не flex-обёртка: ячейка «Лид» держит
   название и имя клиента двумя блоками, и вставлять между ними контейнер
   значило бы переверстать строку ради одной картинки. */
.lead-channel {
  float: left;
  margin: 2px var(--space-1) 0 0;
}

/* Ждёт дольше суток — тот же порог и тот же цвет, что в очереди инбокса и на
   карточке канбана. Три места, красящие ожидание по-разному, менеджер читает
   как три разных смысла. */
.cell-waiting--overdue {
  color: var(--color-danger-text);
  font-weight: var(--weight-medium);
}

.muted {
  color: var(--color-muted);
  display: block;
  font-size: var(--text-xs);
  margin-top: 2px;
}

.status-new {
  background: var(--color-neutral-bg);
  color: var(--color-neutral-text);
}

.status-in-work,
.status-calculation {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
}

.status-order {
  background: var(--color-success-bg);
  color: var(--color-success-text);
}

.status-closed {
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
}

/* Отменённый заказ (F-030). Свой тон нужен потому, что `status-closed` уже
   занят статусом «Подтверждён клиентом» — то есть без этого правила отменённый
   заказ выглядел в списке ровно как заказ, который клиент подтвердил. Фон
   нейтральный, а не красный: отмена это выход из работы, а не авария. */
.status-cancelled {
  background: var(--color-neutral-bg);
  color: var(--color-danger-text);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.mobile-lead-list {
  display: none;
}

.empty-state {
  margin: var(--space-4);
  padding: var(--space-5);
}

.empty-state h3 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-2);
}

.empty-state p {
  color: var(--color-muted);
  margin: 0 0 var(--space-3);
}

.empty-state code {
  background: var(--color-neutral-bg);
  border-radius: var(--radius-sm);
  color: var(--color-neutral-text);
  display: inline-block;
  padding: 6px 8px;
}

.detail-layout {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.summary-panel {
  display: grid;
  gap: var(--space-4);
  grid-column: 1 / -1;
  grid-template-columns: minmax(0, 1.4fr) minmax(300px, 1fr);
  padding: var(--space-5);
}

.priority-panel {
  align-items: start;
}

.summary-main h2 {
  font-size: var(--text-xl);
  font-weight: 700;
}

.summary-grid,
.field-list {
  display: grid;
  gap: var(--space-3);
  margin: 0;
}

.summary-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.priority-grid {
  align-items: start;
}

.summary-grid div,
.field-list div {
  min-width: 0;
}

dt {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 700;
}

dd {
  margin: 3px 0 0;
}

.info-panel {
  padding: var(--space-4);
}

.workspace-grid {
  align-items: flex-start;
  display: grid;
  gap: var(--space-4);
  grid-column: 1 / -1;
  grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr);
}

.workspace-column {
  align-items: stretch;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.workspace-secondary {
  align-items: flex-start;
  display: grid;
  gap: var(--space-4);
  grid-column: 1 / -1;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.workspace-secondary > * {
  min-width: 0;
}

.order-fullspan {
  grid-column: 1 / -1;
}

.doc-text {
  background: var(--color-neutral-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font: inherit;
  line-height: 1.5;
  margin: var(--space-3) 0 0;
  padding: var(--space-3) var(--space-4);
  white-space: pre-wrap;
  word-break: break-word;
}

.workspace-panel {
  align-self: stretch;
}

.workspace-panel h2 {
  font-size: var(--text-md);
  margin: 0;
}

.detail-section {
  align-self: start;
}

.detail-section > summary {
  align-items: center;
  cursor: pointer;
  display: flex;
  font-size: var(--text-md);
  font-weight: 700;
  gap: var(--space-2);
  list-style: none;
}

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

.detail-section > summary::before {
  border: solid var(--color-muted);
  border-width: 0 1.5px 1.5px 0;
  content: "";
  display: inline-block;
  height: 7px;
  transform: rotate(-45deg);
  transition: transform var(--transition-fast);
  width: 7px;
}

.detail-section[open] > summary::before {
  transform: rotate(45deg);
}

.detail-section > summary:focus-visible {
  border-radius: var(--radius-sm);
  outline: 2px solid var(--color-focus);
  outline-offset: 4px;
}

.detail-section > summary + * {
  margin-top: var(--space-4);
}

.field-list.compact {
  margin-top: var(--space-4);
}

.work-list,
.comment-list,
.activity-list {
  display: grid;
  gap: var(--space-3);
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}

.work-list li,
.comment-list li,
.activity-list li {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}

.work-list li {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
}

.work-list small,
.comment-list time {
  color: var(--color-muted);
  display: block;
  font-size: var(--text-xs);
  margin-top: 2px;
}

.comment-list p {
  margin: var(--space-2) 0 0;
}

.activity-list {
  list-style: decimal;
  padding-left: var(--space-5);
}

.quote-panel-header {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
  margin-bottom: var(--space-3);
}

.quote-create-link {
  min-height: 30px;
  padding: 5px 9px;
}

.quote-list {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.quote-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3);
}

.quote-card-meta {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.quote-empty {
  color: var(--color-muted);
  margin: var(--space-2) 0 0;
}

.quote-status {
  background: var(--color-neutral-bg);
  border-radius: 999px;
  color: var(--color-neutral-text);
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 5px 8px;
}

.quote-total {
  font-weight: 700;
}

.quote-lead-summary {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: flex;
  gap: var(--space-4);
  justify-content: space-between;
  margin-bottom: var(--space-4);
  padding: var(--space-4);
}

.quote-lead-summary h2 {
  font-size: var(--text-lg);
  margin: 0;
}

.quote-page {
  margin: 0 auto;
  max-width: 1180px;
}

.quote-detail-page {
  max-width: 1240px;
}

.quote-hero {
  align-items: stretch;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: grid;
  gap: var(--space-3);
  grid-template-columns: minmax(0, 1fr) minmax(320px, auto);
  margin-bottom: var(--space-3);
  padding: var(--space-4);
}

.quote-hero-main {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.quote-hero-main h2 {
  font-size: var(--text-xl);
  line-height: 1.25;
  margin: 0;
}

.quote-hero-main .muted {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

.quote-hero-side {
  align-items: flex-end;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  justify-content: flex-end;
  min-width: 320px;
}

.quote-hero-total {
  align-self: end;
  background: var(--color-primary-soft);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-1);
  min-width: 0;
  padding: var(--space-3);
  text-align: right;
  width: min(100%, 360px);
}

.quote-hero-total span {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: var(--label-transform);
}

.quote-hero-total strong {
  color: var(--color-primary);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 32px;
  line-height: 1.1;
}

.quote-totals-strip {
  align-items: stretch;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(4, minmax(108px, 1fr));
  margin: var(--space-3) 0 0;
  max-width: 680px;
}

.quote-total-tile {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  display: grid;
  gap: 2px;
  min-height: 52px;
  padding: 6px 9px;
  position: relative;
}

.quote-total-tile dt {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: var(--label-transform);
}

.quote-total-tile dd {
  color: var(--color-text);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--text-md);
  margin: 0;
  overflow-wrap: anywhere;
}

.quote-markup-form {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  margin-top: 0;
}

.quote-markup-field {
  align-items: center;
  display: flex;
  gap: var(--space-2);
}

.quote-markup-field > span {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: var(--label-transform);
}

.quote-markup-field select {
  width: 4.5rem;
}

.quote-hero-edit {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: auto;
  padding-top: var(--space-4);
}

.quote-hero-side .quote-order-actions {
  /* Не auto: исторически auto всегда перебивался инлайновым var(--space-4)
     из партиала; когда инлайн сняли (18.08.2026), auto «ожил» и разъехал
     каталожный герой. Закреплено реальное поведение. */
  margin-top: var(--space-4);
  width: 100%;
}

.quote-hero-side .quote-order-actions .primary-button {
  width: 100%;
}

.primary-button--inactive {
  background: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-muted);
}

/* Карточка расчёта строений, вариант А (18.08.2026): слева КП, каким его
   увидит клиент, справа липкая колонка «итог → скопировать → состав →
   доставка → заказ». Каталожные расчёты остаются на старой линейной
   раскладке — их классы ниже по файлу не трогать. */
.quote-head {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  justify-content: space-between;
  margin-bottom: var(--space-3);
}

.quote-head-titles h2 {
  font-size: var(--text-xl);
  line-height: 1.25;
  margin: 0;
}

.quote-head-titles .muted {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-1) 0 0;
}

.quote-work {
  align-items: start;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 1fr) minmax(var(--pane-min), 340px);
}

.quote-work-main {
  display: grid;
  gap: var(--space-3);
  min-width: 0;
}

/* Текст КП и характеристик: не поле ввода, а предпросмотр сообщения. Ширина
   строки ограничена читаемостью (62ch, на 1280 с сайдбаром колонка даёт
   ~55–60 знаков), высота — без внутренней прокрутки: обрезанное КП менеджер
   отправлял не глядя. */
.quote-message {
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: var(--text-md);
  line-height: 1.6;
  max-width: 62ch;
  overflow-wrap: anywhere;
  padding: var(--space-3) var(--space-4);
  white-space: pre-wrap;
}

/* Рейл экрана расчёта — тот же жанр, что смета калькулятора: карточку, итог
   и строки даёт блок .qsum-* (владелец 19.08.2026: «я просил единый дизайн»).
   Здесь только то, чего у сметы нет: липкость, внутренняя прокрутка и зазор
   между группами действий. */
.quote-rail {
  gap: var(--space-3);
  /* Худший рейл (две поездки + оговорка + комментарий + длинный адрес)
     выше экрана 800px — прокручивается внутри; заказ поэтому стоит выше
     доставки в порядке групп. */
  max-height: calc(100vh - var(--space-6));
  overflow-y: auto;
  position: sticky;
  top: var(--space-4);
}

.quote-rail .qsum-total {
  padding-top: 0;
}

/* Селектор с .quote-rail: одиночный класс проигрывал бы базовому .qsum-note
   ниже по файлу. */
.quote-rail .quote-rail-markup-note {
  margin: calc(-1 * var(--space-2)) 0 0;
}

/* Разбивка не сжимается: внутри рейла прокручивается сам рейл, а не
   двухстрочный список (у .qsum-lines в калькуляторе своя прокрутка). */
.quote-rail .qsum-lines {
  flex: 0 0 auto;
  overflow: visible;
}

.quote-rail .quote-delivery-summary-line {
  font-weight: var(--weight-normal);
  row-gap: var(--space-05);
}

.quote-rail .quote-delivery-summary-line span:not(.quote-delivery-address) {
  font-weight: var(--weight-semibold);
}

.quote-rail-copy {
  display: grid;
  gap: var(--space-1);
}

.quote-rail-copy .primary-action {
  width: 100%;
}

.quote-rail-copy [data-copy-status] {
  font-size: var(--text-xs);
  min-height: 1.2em;
  text-align: center;
}

.quote-rail-group {
  border-top: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-2);
  padding-top: var(--space-3);
}

.quote-rail-heading {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  margin: 0;
  text-transform: var(--label-transform);
}

/* Окно заказа (19.08.2026): та же пара «стол + рейл», что у экрана расчёта
   (.quote-work/.quote-rail/.qsum-*), — третьего языка рейлов не заводим.
   Ниже только то, чего у расчёта нет. */
.order-next-step {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  margin: var(--space-1) 0 0;
}

.order-prewrap {
  margin: 0;
  white-space: pre-wrap;
}

.order-doc-head {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
  margin-top: var(--space-3);
}

.order-doc-head .muted {
  margin: 0;
}

.order-status-form {
  display: grid;
  gap: var(--space-2);
}

.order-status-form select {
  width: 100%;
}

/* Разбивки в рейле заказа стоят группами — первая линия у dl лишняя,
   разделение уже даёт линия группы. */
.order-rail-lines {
  margin: 0;
}

.order-detail-rail .payment-list {
  margin: 0;
}

/* Длинные значения (адрес, контакты) в узком рейле — подпись НАД значением,
   а не в строку: в 340px адрес выдавливал подпись до обрезки. */
.order-detail-rail .qsum-line--note {
  display: grid;
  gap: var(--space-05);
}

.order-detail-rail .qsum-line--note dd {
  text-align: left;
}

@media (max-width: 980px) {
  /* Рейл заказа — выше всей левой колонки: статус и деньги первым экраном,
     а «Отменить заказ» (последняя панель колонки) — последним и в прокрутке.
     Без этого разрушающее действие встречалось раньше итога (ревью
     19.08.2026, п.5). */
  .order-detail-rail {
    order: -1;
  }
}

.quote-order-actions {
  margin-top: var(--space-4);
}

.quote-rail .quote-order-actions {
  border-top: 1px solid var(--color-border);
  margin-top: 0;
  padding-top: var(--space-3);
}

.quote-rail .quote-order-actions .primary-button,
.quote-rail .quote-order-actions .secondary-button {
  text-align: center;
  width: 100%;
}

/* Полоса «итог + скопировать» ниже 980px. Родня полосы итога калькулятора:
   тот же слой и тот же отступ над панелью разделов. */
.quote-bar {
  display: none;
}

@media (max-width: 980px) {
  .quote-work {
    grid-template-columns: minmax(0, 1fr);
  }

  /* 🔴 .quote-rail.qsum-card двумя классами: ниже по файлу медиа калькулятора
     гасит .qsum-card (его карточку раскрывает чекбокс полосы), и рейл экрана
     расчёта исчезал вместе с ним — на расчёте ни чекбокса, ни полосы
     калькулятора нет. Двойной класс сильнее одиночного при любом порядке. */
  .quote-rail.qsum-card {
    display: flex;
    max-height: none;
    position: static;
  }

  /* Отступ только у домовой раскладки: у каталожных полосы нет, и 72px
     мёртвого низа им не положено. Число связано с высотой полосы токеном. */
  .quote-detail-page--house {
    --quote-bar-clearance: 108px;
    padding-bottom: var(--quote-bar-clearance);
  }

  .quote-bar {
    align-items: center;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    bottom: var(--nav-bar-space);
    box-shadow: var(--elevation-overlay);
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
    justify-content: space-between;
    left: 0;
    padding: var(--space-2) var(--space-4);
    position: fixed;
    right: 0;
    z-index: var(--z-sticky);
  }

  /* На 375 кнопки не влезают в строку итога и переносятся вниз парой на всю
     ширину; на 768 всё стоит в один ряд. */
  .quote-bar-actions {
    display: flex;
    flex: 1 1 260px;
    gap: var(--space-2);
    justify-content: flex-end;
  }

  .quote-bar-actions .primary-action,
  .quote-bar-actions .secondary-button {
    flex: 1 1 auto;
    text-align: center;
  }

  .quote-bar-total {
    display: flex;
    flex-direction: column;
    line-height: var(--leading-tight);
    min-width: 0;
  }

  /* Подпись и кегль суммы — общие с полосой калькулятора (блок .qsum-bar-*
     ниже по файлу); здесь только то, чего у той полосы нет. */
  .quote-bar-total strong {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
  }

  /* Ответ на «Скопировать» — чип над полосой, не в её потоке: длинная
     подсказка отката иначе удваивала высоту полосы. */
  .quote-bar-status {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    bottom: calc(100% + var(--space-1));
    font-size: var(--text-xs);
    line-height: var(--leading-tight);
    max-width: calc(100% - var(--space-6));
    padding: 2px var(--space-3);
    position: absolute;
    right: var(--space-3);
  }

  .quote-bar-status:empty {
    display: none;
  }
}

.quote-delivery-card {
  display: grid;
  gap: var(--space-3);
}

.quote-delivery-summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.quote-delivery-summary-main {
  max-width: min(100%, 620px);
  min-width: 0;
}

.quote-delivery-summary-main .muted {
  margin-bottom: var(--space-1);
}

.quote-delivery-summary-content {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.quote-delivery-address {
  color: var(--color-text);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: 1.4;
  margin: 0;
  overflow-wrap: anywhere;
}

.quote-delivery-summary-line {
  align-items: center;
  color: var(--color-text);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-sm);
  gap: 0 var(--space-2);
  line-height: 1.45;
  margin: 0;
  max-width: 100%;
}

.quote-delivery-summary-line .quote-delivery-address {
  font-size: var(--text-sm);
}

.quote-delivery-summary-line span:not(.quote-delivery-address) {
  font-weight: 800;
}

.quote-delivery-summary-line span + span::before {
  color: var(--color-border-strong);
  content: "·";
  margin-right: var(--space-2);
}

.quote-delivery-editor {
  flex-grow: 1;
  margin: 0;
  min-width: 0;
}

.quote-delivery-editor[open] ~ .quote-delivery-delete {
  display: none;
}

.quote-delivery-editor summary {
  cursor: pointer;
  list-style: none;
}

.quote-delivery-editor summary::-webkit-details-marker {
  display: none;
}

.quote-delivery-form {
  border-top: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
}

.quote-delivery-layout {
  display: grid;
  gap: var(--space-3);
  grid-template-areas:
    "address map"
    "route map"
    "comment map";
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  align-items: start;
}

.quote-delivery-field {
  display: grid;
  gap: 6px;
  min-width: 0;
}

.quote-delivery-field input,
.quote-delivery-field textarea {
  width: 100%;
}

.quote-delivery-address-wrap {
  position: relative;
}

.quote-delivery-address-field {
  grid-area: address;
}

/* Подсказки адреса — третье всплывающее меню проекта, и оно было собрано по
   третьему набору правил: радиус поля, тень «приподнятого», сырой `z-index: 20`
   мимо шкалы 10/40/100. Теперь как у поповера групп и меню скрепки: одинаковые
   вещи выглядят одинаково, и это ровно то, что просил владелец словом
   «в балансе с другими элементами». */
.quote-delivery-suggestions {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-overlay);
  display: grid;
  left: 0;
  max-height: 220px;
  overflow-y: auto;
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: var(--z-dropdown);
}

.quote-delivery-suggestions[hidden] {
  display: none;
}

.quote-delivery-suggestion {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text);
  cursor: pointer;
  font: inherit;
  justify-content: flex-start;
  padding: 9px 10px;
  text-align: left;
}

.quote-delivery-suggestion:hover,
.quote-delivery-suggestion:focus-visible {
  background: var(--color-neutral-bg);
  outline: none;
}

.quote-delivery-suggestion:last-child {
  border-bottom: 0;
}

.quote-delivery-map {
  background: var(--color-neutral-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  height: clamp(260px, 32vh, 360px);
  margin-top: var(--space-2);
  overflow: hidden;
}

.quote-delivery-map-column {
  grid-area: map;
  min-width: 0;
}

.quote-delivery-map-note {
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-muted);
  margin: var(--space-2) 0 0;
  padding: var(--space-3);
}

.quote-delivery-route-group {
  border-top: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-2);
  grid-area: route;
  padding-top: var(--space-2);
}

.quote-delivery-route-title {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 700;
}

.quote-delivery-metrics {
  align-items: start;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.quote-delivery-distance-field {
  flex: 0 1 180px;
}

.quote-delivery-price-field {
  flex: 0 1 220px;
}

.quote-delivery-help {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: 2px 0 0;
}

.quote-delivery-preview {
  background: var(--color-success-bg);
  border: 1px solid var(--color-success-border);
  border-radius: var(--radius-sm);
  color: var(--color-success-text);
  display: grid;
  font-size: var(--text-sm);
  gap: 2px;
  padding: 8px 10px;
}

.quote-delivery-preview[hidden] {
  display: none;
}

.quote-delivery-preview.is-error {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

.quote-delivery-preview strong {
  color: inherit;
}

.quote-delivery-comment-field {
  grid-area: comment;
}

.quote-delivery-editor-actions {
  align-items: center;
  border-top: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
  margin-top: var(--space-3);
  padding-top: var(--space-3);
}

.quote-delivery-delete {
  margin: 0;
}

.quote-delivery-editor-actions .primary-action {
  margin-left: auto;
}

.quote-offer-section {
  display: grid;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.quote-offer-list {
  display: grid;
  gap: var(--space-3);
}

.quote-offer-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: grid;
  gap: var(--space-3);
  padding: var(--space-3);
}

.quote-offer-header {
  align-items: start;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: minmax(0, 1fr) auto;
}

.quote-offer-header h3 {
  font-size: var(--text-md);
  line-height: 1.3;
  margin: 0;
}

.quote-offer-meta {
  color: var(--color-muted);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-sm);
  gap: 4px var(--space-2);
  line-height: 1.45;
  margin: 0;
}

.quote-offer-meta span + span::before {
  color: var(--color-border-strong);
  content: "·";
  margin-right: var(--space-2);
}

.quote-price-breakdown {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 0;
}

.quote-price-breakdown div {
  align-content: start;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  display: grid;
  gap: var(--space-1);
  min-width: 0;
  padding: var(--space-2);
}

.quote-price-breakdown dt {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 700;
}

.quote-price-breakdown dd {
  color: var(--color-text);
  font-size: var(--text-md);
  font-weight: 800;
  margin: 0;
  overflow-wrap: anywhere;
}

.quote-offer-prices {
  display: grid;
  gap: var(--space-2);
}

.quote-price-breakdown .quote-price-total {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  box-shadow: none;
}

.quote-price-breakdown .quote-price-total dd {
  color: var(--color-primary);
  font-size: var(--text-lg);
}

.quote-options-list {
  display: grid;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
}

.quote-options-list li {
  align-items: start;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  display: grid;
  gap: var(--space-2);
  grid-template-columns: minmax(0, 1fr) auto;
  justify-content: space-between;
  padding: 8px 10px;
}

.quote-options-list li::before {
  content: none;
}

.quote-options-list span {
  color: var(--color-text);
  min-width: 0;
}

.quote-options-list strong {
  flex: 0 0 auto;
  font-size: var(--text-sm);
  line-height: 1.5;
  white-space: nowrap;
}

.quote-option-main {
  align-items: start;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: 16px minmax(0, 1fr);
  min-width: 0;
}

.quote-option-check {
  color: var(--color-success-text);
  font-size: var(--text-xs);
  font-weight: 800;
  line-height: 1.7;
}

.quote-option-text {
  overflow-wrap: anywhere;
}

.quote-package {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 4%, transparent);
  padding: var(--space-2);
}

.quote-package summary {
  align-items: center;
  background: var(--color-surface-sunken);
  border: 1px solid color-mix(in srgb, var(--color-primary) 22%, transparent);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 800;
  list-style: none;
  padding: 9px 11px;
}

.quote-package[open] summary {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.quote-package summary::-webkit-details-marker {
  display: none;
}

.quote-package summary span {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 700;
}

.quote-package summary::after {
  color: var(--color-muted);
  content: "▾";
  font-size: 12px;
  margin-left: auto;
  transition: transform var(--transition-fast);
}

.quote-package[open] summary::after {
  transform: rotate(180deg);
}

.quote-package summary:focus-visible {
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
  outline: none;
}

.quote-package .catalog-detail-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  margin-top: var(--space-3);
}

.quote-package .catalog-card-title {
  margin-bottom: var(--space-2);
}

.quote-package .catalog-attrs-list {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
}

.quote-technical-lines {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  margin-top: var(--space-3);
  padding: var(--space-3);
}

.quote-technical-lines summary {
  color: var(--color-muted);
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: 800;
}

.quote-technical-lines .table-scroll {
  margin-top: var(--space-3);
}

/* Смета конфигуратора товара — та же карточка, что у расчёта строения
   (.qsum-*). Осталось только то, что нужно списку выбранных допов. */
.qsum-options {
  border-top: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: var(--space-3) 0 0;
}

.qsum-options li {
  align-items: baseline;
  display: flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  justify-content: space-between;
}

.qsum-options li span {
  color: var(--color-text);
  min-width: 0;
}

.qsum-options li strong {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.qsum-options li.is-empty {
  color: var(--color-muted);
  display: block;
}

.quote-configure-form select {
  width: 100%;
}

.quote-product-form {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-3);
}

.quote-product-form button {
  width: 100%;
}

.quote-detail-grid {
  align-items: start;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  margin-bottom: var(--space-4);
}

.quote-summary .catalog-price-list {
  gap: var(--space-3);
}

.quote-product-list {
  display: grid;
  gap: var(--space-4);
}

.quote-product-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}

.quote-product-options {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
}

.quote-product-options ul {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-4);
}

.quote-product-options li {
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.quote-lines-table {
  min-width: 760px;
}

.quote-detail-actions {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
  margin-bottom: var(--space-3);
}

.quote-copy-actions {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
}

.placeholder-panel {
  border-style: dashed;
}

.entity-form {
  display: grid;
  gap: var(--space-4);
}

.form-section,
.login-panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: var(--space-5);
}

.form-section h2,
.login-panel h2,
.inline-form h3 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-4);
}

.entity-form div,
.inline-form div {
  display: grid;
  gap: 6px;
}

.entity-form div + div,
.inline-form div + div {
  margin-top: var(--space-3);
}

.form-actions {
  display: flex;
  justify-content: flex-end;
}

.inline-form {
  border-top: 1px solid var(--color-border);
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
}

.standalone-form {
  border-top: 0;
  margin-top: 0;
  padding-top: 0;
}

.panel-title-row {
  align-items: center;
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
}

.compact-form {
  max-width: 420px;
}

.form-error {
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border);
  border-radius: var(--radius-md);
  color: var(--color-danger-text);
  padding: var(--space-3);
}

.messages {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

/* Плашка сообщения. Зелёный — только у успеха: до 15.08.2026 уровень в разметке
   не участвовал вовсе, и `messages.error` выглядел как «Задача поставлена». */
.message {
  background: var(--color-success-bg);
  border: 1px solid var(--color-success-border);
  border-radius: var(--radius-md);
  color: var(--color-success-text);
  margin: 0;
  padding: var(--space-3);
}

.message--error {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

.message--warning {
  background: var(--color-warning-bg);
  border-color: var(--color-warning-border);
  color: var(--color-warning-text);
}

.message--info,
.message--debug {
  background: var(--color-surface-sunken);
  border-color: var(--color-border);
  color: var(--color-text);
}

/* Плашка отказа поверх страницы (F-215). Форма та же, что у `.kanban-toast`,
   но эта не гаснет по таймеру: она сообщает об отказе, который менеджер обязан
   отработать (войти заново и отправить снова), а не о состоявшемся действии.
   Отсюда же ссылка и крестик — по правилу «у ошибки есть путь выхода». */
.crm-toast {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  bottom: var(--space-4);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
  display: flex;
  font-size: var(--text-sm);
  gap: var(--space-3);
  left: 50%;
  margin: 0;
  max-width: min(92vw, 520px);
  padding: var(--space-3) var(--space-4);
  position: fixed;
  transform: translateX(-50%);
  z-index: var(--z-modal);
}

.crm-toast--danger {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

.crm-toast-action {
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
  flex: none;
  font-weight: 600;
  text-decoration: underline;
}

/* 44 px — минимальная цель нажатия; крестик у края плашки её не имеет. */
.crm-toast-close {
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  flex: none;
  font-size: var(--text-lg);
  line-height: 1;
  min-height: 44px;
  min-width: 44px;
}

.contact-primary {
  font-size: var(--text-md);
}

.quick-edit {
  display: inline-block;
  margin-left: var(--space-2);
  position: relative;
}

.quick-edit summary {
  color: var(--color-primary);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-xs);
  font-weight: 700;
  list-style: none;
}

.quick-edit summary::-webkit-details-marker {
  display: none;
}

.quick-edit summary::before {
  content: "✎";
  font-size: 13px;
  line-height: 1;
  margin-right: 4px;
}

.quick-edit[open] {
  display: block;
  margin: var(--space-2) 0 0;
}

.quick-edit form {
  align-items: end;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-2);
  grid-template-columns: minmax(180px, 1fr) auto;
  margin-top: var(--space-2);
  padding: var(--space-3);
}

.quick-edit input,
.quick-edit select,
.quick-edit textarea {
  min-width: 0;
  width: 100%;
}

.catalog-toolbar {
  align-items: end;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  display: grid;
  gap: var(--space-3);
  grid-template-columns: minmax(220px, 1.4fr) minmax(180px, 1fr) auto auto;
  margin-bottom: var(--space-4);
  padding: var(--space-4);
}

.catalog-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.catalog-category-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

.catalog-category-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  color: var(--color-text);
  display: grid;
  gap: var(--space-3);
  min-height: 150px;
  padding: var(--space-4);
}

.catalog-category-card:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
  text-decoration: none;
}

.catalog-freeform-cta {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: flex;
  gap: var(--space-4);
  justify-content: space-between;
  margin-bottom: var(--space-4);
  padding: var(--space-4);
}

.catalog-freeform-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.catalog-freeform-title {
  color: var(--color-text);
  font-size: 1.05rem;
}

.catalog-freeform-hint {
  color: var(--color-muted);
  font-size: 0.875rem;
}

@media (max-width: 640px) {
  .catalog-freeform-cta {
    align-items: stretch;
    flex-direction: column;
  }
}

.catalog-category-icon {
  align-items: center;
  background: var(--color-primary-soft);
  border-radius: var(--radius-md);
  color: var(--color-primary);
  display: inline-flex;
  font-size: var(--text-xl);
  font-weight: 800;
  height: 52px;
  justify-content: center;
  width: 52px;
}

.quote-category-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

.quote-category-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text);
  display: grid;
  gap: var(--space-2);
  min-height: 190px;
  padding: var(--space-4);
  text-decoration: none;
}

.quote-category-card:hover {
  border-color: var(--color-primary);
  box-shadow: var(--shadow-sm);
  color: var(--color-text);
  text-decoration: none;
}

.quote-category-card-freeform {
  border-color: var(--color-primary);
}

/* ═══════════════════════════════════════════════════════
   КАЛЬКУЛЯТОР СТРОЕНИЙ (F-043)

   Экран одной задачи: менеджер набирает конфигурацию и называет клиенту
   сумму, часто прямо в трубку. Отсюда единственное сильное решение экрана —
   смета не уезжает: на широком экране липнет к верху своей колонки, на
   телефоне превращается в полосу «итого + создать» поверх страницы.

   Своего вида у расчёта нет: панели, подписи и сетка — общие
   (.panel, --label-*, .form-grid). Класс .lead-col-panel удалён вместе с
   капслочными заголовками — вторая система панелей в приложении не нужна.
   ═══════════════════════════════════════════════════════ */

.configurator {
  align-items: start;
  display: grid;
  gap: var(--space-4);
  /* Ширина сметы — ровно под сумму и десяток строк; всё остальное форме.
     Число не из головы: при 280px колонка формы на 1280px выходит 341px,
     а самой длинной подписи варианта («Профнастил оцинкованный — включено»)
     нужно 315px вместе с рамкой и стрелкой селекта. Замер — canvas
     measureText по Golos Text 14px, 08.08.2026. */
  grid-template-columns: minmax(0, 1fr) minmax(var(--pane-min), 280px);
}

.configurator-form {
  min-width: 0;
}

/* ── Переписка рядом с расчётом (F-186) ────────────────────────────────────
   Владелец 11.08.2026: «нажимаю создать расчёт — страница обновляется,
   переписка уходит». Способ выбран разведкой, а не на вкус: у amoCRM, с которой
   нас сравнивают каждый день, лента разговора — ПОСТОЯННАЯ колонка карточки, а
   рабочая область меняется рядом с ней. Модалка для длинной формы — прямой
   антипаттерн (NN/g, «Modal dialogs become problematic … for noncritical
   activities»), а соседняя вкладка (мера с 11.08) задачу «видеть оба сразу» не
   решает вовсе. Постоянная колонка заодно бесплатно закрывает «параметры не
   теряются»: закрывать нечего.

   🔴 Колонка слева, а не справа: справа липкая смета, менеджер называет по ней
   сумму в трубку (F-043), и её место занято. Порядок чтения совпадает с
   работой: читаю клиента → ввожу → называю цену.

   🔴 Пороги замерены в браузере 20.08.2026, а не назначены на глаз. Форме нужно
   720 px: уже при 644 верхний ряд «тип · крыша · размер» (три поля по
   --field-min 280) переносится, и документ растёт с 800 до 1024 px. Полная
   раскладка без прокрутки требует 320 + 16 + 720 + 16 + 280 + 48 полей
   страницы + 216 бокового меню = 1616 px. То есть от ~1616 обе области живут
   без прокрутки; на 1280 прокрутка неизбежна — это физика ширины, и цена
   названа числом: 1024 px документа вместо 800. Ниже 1200 колонок рядом нет
   совсем — лента сворачивается в строку над формой. */
.configurator--with-chat {
  grid-template-columns: minmax(0, 320px) minmax(0, 1fr) minmax(var(--pane-min), 280px);
}

.calc-chat {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-flat);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  /* Лента не уезжает вместе с формой: менеджер листает опции, глядя в то, что
     клиент написал. 80 px — замер этого экрана (верх .configurator 55 px плюс
     нижнее поле 24), а не число из карточки сделки. */
  position: sticky;
  top: var(--space-1);
  height: calc(100vh - 80px);
  height: calc(100dvh - 80px);
}

/* Полоса-переключатель нужна только там, где колонки рядом не помещаются.
   На широком экране заголовок ленте даёт её собственная шапка. */
.calc-chat-toggle,
.calc-chat-bar {
  display: none;
}

.calc-chat-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
}

/* 🔴 Ряды формы считают ширину СВОЕЙ КОЛОНКИ, а не ширину окна. С лентой рядом
   форма живёт в 384–784 px при том же окне 1280–1680, а медиазапросы
   `.form-grid--3` стоят по вьюпорту (1020 и 700) и про это ничего не знают:
   верхний ряд оставался в три колонки по 115 px, где «Дачный дом» обрезан, а
   подпись «Ширина, м» переносится по слогам. Оба пункта — брак по списку
   приёмки вёрстки (п. 1 и п. 3), поэтому сетка здесь интринсик: auto-fit с
   min()-защитой, паттерн проекта.

   224 px замерены под сохранение сегодняшнего вида: при форме 784 (окно 1680)
   ряд остаётся тем же, что и был, — три колонки; при 596 их две; при 384 одна.
   Больше 224 — и на 1680 ряд развалился бы на два, то есть правка испортила бы
   ровно тот экран, ради которого затевалась. */
.configurator--with-chat .configurator-fields .form-grid,
.configurator--with-chat .configurator-fields .form-grid--3 {
  grid-template-columns: repeat(auto-fit, minmax(min(224px, 100%), 1fr));
}

/* Ниже 1200 px рядом не помещается: 320 на ленту оставили бы форме 644, а это
   ровно тот порог, за которым верхний ряд полей ломается. Лента уходит строкой
   над формой — один клик, и она раскрыта. Приём тот же, что у разбивки сметы
   (#qsum-expand): галочка вместо второй системы состояния. */
@media (max-width: 1199px) {
  .configurator--with-chat {
    grid-template-columns: minmax(0, 1fr) minmax(var(--pane-min), 280px);
  }

  .calc-chat {
    grid-column: 1 / -1;
    height: auto;
    position: static;
  }

  .calc-chat-bar {
    align-items: center;
    cursor: pointer;
    display: flex;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    gap: var(--space-2);
    justify-content: space-between;
    /* Тап-цель полной высоты строки: полоса — единственный вход в переписку на
       этих ширинах. */
    min-height: 44px;
    padding: var(--space-1) var(--space-4);
  }

  .calc-chat-bar-title {
    align-items: center;
    display: flex;
    gap: var(--space-2);
  }

  .calc-chat-caret {
    transition: transform var(--transition-fast);
  }

  .calc-chat-body {
    display: none;
  }

  .calc-chat-toggle:checked ~ .calc-chat-body {
    display: flex;
    /* 🔴 `flex: 0 0 auto` обязателен: базовый `flex: 1` — это `1 1 0%`, и
       flex-basis 0 перебивает height по главной оси. С ним лента растягивалась
       во весь экран и хоронила под собой форму. */
    flex: 0 0 auto;
    /* Лента прокручивается внутри себя, а не растит страницу: форма обязана
       оставаться под рукой. */
    height: min(60vh, 420px);
  }

  /* Заголовок ленты здесь повторяет полосу-переключатель прямо над ним. Убираем
     повтор, но не саму шапку: в ней живут знак канала и подпись «разговор
     продолжился в …» (F-145) — их прятать нельзя. Шапку, в которой не осталось
     ни того ни другого, убираем целиком, чтобы не висела пустая полоса. */
  .calc-chat-body .panel-head--feed .panel-title {
    display: none;
  }

  .calc-chat-body .panel-head--feed:not(:has(.panel-head-channel, .panel-head-moved)) {
    display: none;
  }

  .calc-chat-toggle:checked ~ .calc-chat-bar .calc-chat-caret {
    transform: rotate(180deg);
  }

  /* Галочка скрыта, но остаётся в обходе табом: полоса — это <label>, и без
     фокусируемого управления переписка была бы недоступна с клавиатуры. */
  .calc-chat-toggle {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    width: 1px;
  }

  .calc-chat-toggle:focus-visible ~ .calc-chat-bar {
    outline: 2px solid var(--color-focus);
    outline-offset: -2px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .calc-chat-caret {
    transition: none;
  }
}

/* Плотнее общего вида приложения — и это решение, а не экономия на глаз: цель
   экрана в том, чтобы бытовка без допов влезала в 1280×800 без прокрутки,
   потому что менеджер называет цену в трубку и листать не может. Каждая шапка
   панели стоит 44px, каждый ряд отступов — 8px, и на пяти панелях это ~90px. */
.configurator-fields {
  display: grid;
  gap: var(--space-2);
}

.configurator-fields > .panel > .panel-head,
.configurator-fields > .panel > summary.panel-head {
  min-height: 38px;
  padding: var(--space-1) var(--space-4);
}

/* ── Пять ярусов веса ─────────────────────────────────────
   🔴 До F-175 все пять заголовков экрана были одинаковы: 13px, вес 600, цвет
   #1a2230 — «Строение», которое трогают в 100 % расчётов, выглядело ровно как
   справочник «Входит в цену», который не трогают никогда. Слова заказчика:
   «глаз теряется, акцентности нет, всё сливается».

   Ярус задаётся тремя каналами сразу — размером, весом и цветом, — а не одним
   цветом: одинаковый размер с разным оттенком серого читается как случайность.
   Сверху вниз: строение (18/700, без карточки) → проёмы (15/600) → что
   добавить (14/600) → материалы (13/500, приглушённый) → входит в цену
   (12/400, приглушённый). Самый крупный текст экрана — не заголовок, а цена. */
.configurator-fields > .panel--bare {
  background: transparent;
  border: 0;
  border-radius: 0;
}

.configurator-fields > .panel--bare > .panel-head {
  border-bottom: 0;
  padding-left: 0;
}

/* Без карточки нет и её внутренних отступов: заголовок стоит прямо над полями,
   а 8px сверху — те самые пиксели, на которые экран переставал влезать в 800. */
.configurator-fields > .panel--bare > .panel-body {
  padding: 0 0 var(--space-2);
}

.configurator-fields > .panel--bare .panel-title {
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
}

/* Поля первого яруса крупнее остальных: тип и размер строения — единственное,
   что менеджер заполняет в каждом расчёте без исключения. */
.configurator-fields > .panel--bare select {
  font-size: var(--text-md);
  min-height: 44px;
}

.configurator-fields > .panel--openings .panel-title {
  font-size: 15px;
}

.configurator-fields > .panel--quiet .panel-title {
  color: var(--color-muted);
  font-weight: var(--weight-medium);
}

.configurator-fields > .panel--included .panel-title {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
}

/* Пятый ярус — самый лёгкий: справочник, а не поле. Рамки нет, только
   утопленный фон, который уже был. */
.configurator-fields > .panel--included {
  border-color: transparent;
}

.configurator-fields .panel-body {
  padding: var(--space-2) var(--space-4);
}

.configurator-form > .configurator-fields > .panel:first-child {
  margin-top: 0;
}

/* ── Поля ─────────────────────────────────────────────────
   Подпись над полем и тише значения (--label-*), капслока нет. */
.configurator-fields label {
  display: flex;
  flex-direction: column;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  color: var(--label-color);
  gap: var(--space-1);
  min-width: 0;
}

.configurator-fields select,
.configurator-fields input:not([type="checkbox"]):not([type="radio"]) {
  color: var(--color-text);
  min-height: var(--control-h-lg);
  width: 100%;
}

/* На телефоне колонка одна и равна ширине экрана: 13px — минимум плотных
   данных по layout-rules, при нём подпись влезает в 375px с запасом.
   Обрезать подпись нельзя, переносить select не умеет — рычаг только шрифт. */
@media (max-width: 480px) {
  .configurator-fields select,
  .configurator-fields input:not([type="checkbox"]) {
    font-size: 13px;
    padding-left: 8px;
    padding-right: 24px;
  }

  /* Каждые лишние 8px внутреннего отступа — 8px, которых не хватит подписи */
  .configurator-fields .panel-body {
    padding-left: var(--space-3);
    padding-right: var(--space-3);
  }
}

/* ── Сворачиваемая панель (details) ──────────────────────
   Шапка та же .panel-head, чтобы свёрнутая секция не выглядела другим
   элементом. Маркер браузера снят, стрелка — иконка из спрайта. */
.panel--collapsible > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
}

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

.panel--collapsible:not([open]) > summary {
  border-bottom-color: transparent;
}

.panel--collapsible > summary:hover {
  background: var(--state-hover);
}

.panel-caret {
  color: var(--color-faint);
  margin-left: auto;
  transition: transform var(--transition-fast);
}

.panel--collapsible[open] > summary .panel-caret {
  transform: rotate(180deg);
}

/* ── «Входит в цену» ──────────────────────────────────────
   Единственная панель формы без единого поля ввода — и в этом весь приём:
   разницу с апгрейдами видно раньше, чем прочитан заголовок. Фон утопленный,
   галочка вместо маркера списка, ширина колонки от самой длинной строки
   («Каркас: брус 50×100 мм (хвойный, 1 сорт)» — 40 символов, ~300px при 14px
   Golos Text). Список заполняет ширину панели: 12 коротких строк в одну
   колонку на 1280px оставляли бы две трети панели пустыми. */
.panel--included {
  background: var(--color-surface-sunken);
}

.panel--included > .panel-head {
  border-bottom-color: var(--color-border);
}

.panel-note {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

.included-list {
  display: grid;
  gap: var(--space-2) var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}

.included-list li {
  align-items: baseline;
  color: var(--color-text);
  display: flex;
  font-size: var(--text-base);
  gap: var(--space-2);
  line-height: var(--leading-normal);
}

.included-list .icon {
  color: var(--color-success-solid);
  flex-shrink: 0;
  position: relative;
  top: 2px;
}

/* ── Перегородки, окна, двери ────────────────────────────
   Три группы в одной панели, разделённые линией. Карточка внутри
   карточки — брак из списка заказчика, поэтому фона и рамки у групп нет. */
/* Колонка не уже своей самой длинной подписи: «Распашная, 1.4 м, две
   створки (3 465 ₽)» требует 273px вместе с рамкой и стрелкой. Отсюда
   минимум 300px — при трёх жёстких колонках на 1280px выходило 161px и
   подписи резались. Группы разделяет воздух, а не рамка: рамка внутри
   панели даёт «карточку в карточке». */
.house-dyn-groups {
  display: grid;
  gap: var(--space-5) var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
}

.house-dyn-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

/* Заголовок группы и «Добавить» — одна строка: кнопка под пустым списком
   занимала собственный ряд там, где справа от заголовка пусто. */
.house-dyn-group-head {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
  min-height: var(--control-h);
}

.house-dyn-group-title {
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

.house-dyn-list {
  display: grid;
  gap: var(--space-2);
}

.house-dyn-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.house-dyn-row + .house-dyn-row {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-2);
}

/* Строки списков плотнее основных полей: подпись «Металлическая РФ,
   терморазрыв (34 650 ₽)» — самая длинная в проекте, на 14px она не влезает
   в колонку 342px. 13px — документированный минимум плотных данных. */
.house-dyn-row select {
  font-size: var(--text-sm);
  min-width: 0;
  width: 100%;
}

.house-dyn-row-bottom {
  align-items: center;
  display: flex;
  gap: var(--space-2);
}

.house-dyn-qty-label {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin-right: auto;
}

.house-dyn-row input[type="number"] {
  flex: 0 0 68px;
  text-align: center;
  width: 68px;
}

/* Счётчик количества (20.08.2026). Нормы NN/g и Baymard сошлись независимо:
   кнопки вокруг числа, а не голое поле; цель под палец; граница диапазона
   видна состоянием кнопки. Стрелки браузера у поля убраны — их роль забрали
   кнопки, а два механизма на одном контроле путают. */
.stepper {
  align-items: stretch;
  display: flex;
  flex: 0 0 auto;
}

.stepper input[type="number"] {
  border-radius: 0;
  flex: 0 0 40px;
  /* Рамка одна на весь счётчик: у поля и кнопок она общая, иначе между
     ними двоится линия и контрол выглядит собранным из кусков. */
  margin: 0 -1px;
  text-align: center;
  width: 40px;
  -moz-appearance: textfield;
}

.stepper input[type="number"]::-webkit-outer-spin-button,
.stepper input[type="number"]::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}

.stepper-btn {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  justify-content: center;
  /* 32px — не 44: строка живёт в колонке 342px рядом с полем и крестиком,
     а весь экран обязан влезать в 1280×800 (F-172). Цель добирается
     паддингом: ширина кнопки с ним 32px, вертикаль — высотой строки. */
  min-height: 32px;
  padding: 0 var(--space-2);
}

.stepper-btn:first-child {
  border-radius: var(--radius-control) 0 0 var(--radius-control);
}

.stepper-btn:last-child {
  border-radius: 0 var(--radius-control) var(--radius-control) 0;
}

.stepper-btn:hover:not(:disabled) {
  background: var(--color-surface-sunken);
}

.stepper-btn:disabled {
  color: var(--color-faint);
  cursor: default;
}

/* База проёмов в шапке панели: строка «что включено» стала счётчиком
   (20.08.2026). Ряд в ряд с названием — панель обязана остаться в пределах
   экрана 1280×800, ради которого делалась F-172. */
/* Итог по виду у названия группы: база из шапки плюс добавленные ряды. */
.house-dyn-count {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
}

.house-dyn-count:empty {
  display: none;
}

/* Счётчик базы подсвечен, когда добавлен ряд того же вида: менеджер видит,
   что базовые окна остались в составе, и решает — оставить или обнулить
   (решение штаба 20.08.2026 вместо молчаливой замены). */
/* Базовая строка внутри поповера: тот же язык, что у бесплатных опций
   «в цене» в блоке «Что добавить» — владелец 21.08.2026 назвал строку
   «Перегородка посередине — в цене» образцом для окон и дверей. */
.house-dyn-base {
  align-items: center;
  color: var(--color-text);
  display: flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
}

/* Базовая строка со счётчиком: название слева, счётчик справа — как у
   строк добавленных видов ниже, чтобы список читался одной колонкой. */
.house-dyn-base--counted {
  border-bottom: 1px solid var(--color-border);
  /* Вторая строка — комплектация победителя и цена следующего шага — переносится
     под первую целиком: на 375 px она рядом не встаёт, а рвать её по словам
     нельзя (правило приёмки: подпись не переносится по слогам). */
  flex-wrap: wrap;
  justify-content: space-between;
  padding-bottom: var(--space-2);
}

/* «— 3 в цене» рвать по словам нельзя: «в / цене» на 375 px читается как обрывок
   строки, а не как подпись. Не влезло — уезжает целиком на свою строку. */
.house-dyn-base-included {
  white-space: nowrap;
}

/* Что у ПОБЕДИТЕЛЯ в базе — его словами — и во что обойдётся следующий шаг
   счётчика. 🔴 Цена стоит здесь, а не всплывает после клика: цена-сюрприз это
   первая причина отказа (Baymard; docs/reference/calculator-ux.md §3). */
.house-dyn-base-note {
  color: var(--color-muted);
  flex: 1 0 100%;
  font-size: var(--text-xs);
  line-height: 1.4;
  margin-top: var(--space-1);
}

/* Пустой узел всё равно есть в разметке: заменить по id можно только то, что
   уже стоит в документе (hx-swap-oob). Прячем классом, а не `:empty` —
   у шаблонного узла внутри перевод строки, и `:empty` его не поймает. */
.house-dyn-base-note.is-empty {
  display: none;
}

/* Сумма читается, а не тонет в сноске: она и есть то, ради чего сноска. */
.house-dyn-base-price {
  color: var(--color-text);
  font-family: var(--font-mono);
}

/* Метраж базовой перегородки: поле с единицей, не счётчик — величина
   непрерывная (NN/g прямо против степперов на таких). */
.house-dyn-meters {
  align-items: center;
  display: flex;
  flex: 0 0 auto;
  gap: var(--space-1);
}

/* Каждое поле размеров со своей подписью; знак × выравнивается по строке
   контролов, а не по подписям — иначе он висит над полем. */
.field-pair-item {
  display: grid;
  /* Равные доли: до подписей селекты тянулись по содержимому и «6 × 8»
     съезжалось в угол ряда — владелец назвал это «криво». */
  flex: 1 1 0;
  gap: var(--space-1);
  min-width: 0;
}

.field-pair-item select {
  width: 100%;
}

.house-dyn-meters input[type="number"] {
  flex: 0 0 56px;
  min-height: 32px;
  text-align: center;
  width: 56px;
}

.house-dyn-unit {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

.house-dyn-base-name {
  align-items: center;
  display: flex;
  gap: var(--space-1);
  min-width: 0;
}

/* Подсвечен, когда добавлен ряд того же вида: менеджер видит, что базовые
   остались в составе, и решает — оставить или обнулить (решение штаба
   20.08.2026 вместо молчаливой замены). */
.house-dyn-base.is-duplicated .house-dyn-base-name {
  color: var(--color-warning-text);
}

.house-dyn-remove {
  background: transparent;
  border: none;
  box-shadow: none;
  color: var(--color-muted);
  cursor: pointer;
  display: inline-flex;
  flex: 0 0 auto;
  height: var(--control-h);
  padding: 0 var(--space-2);
}

.house-dyn-remove:hover {
  background: transparent;
  color: var(--color-danger-text);
}

/* 🔴 justify-self, а не только align-self: внутри раскрытой группы кнопка —
   элемент грида (.opt-field), и по умолчанию грид растягивает её на всю
   колонку. Замер до правки: «Добавить перегородку» занимала 655px = 51 %
   экрана на 1280 ради опции, нужной в 18 % расчётов. Кнопка действия не может
   быть шире того, что она добавляет (F-175 §7). */
.house-dyn-add {
  align-items: center;
  align-self: start;
  justify-self: start;
  background: transparent;
  border: none;
  box-shadow: none;
  color: var(--color-primary);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  gap: var(--space-1);
  padding: var(--space-1) 0;
  width: auto;
}

.house-dyn-add:hover {
  background: transparent;
  color: var(--color-primary-hover);
  text-decoration: underline;
}

/* ── Апгрейды: строка выбора с ценой ──────────────────────
   Один компонент на все платные пункты формы — и переключатели вариантов, и
   галочки допов. Цена стоит в строке у названия, а не серым в скобках: до
   10.08.2026 ветро-влагоизоляция за 11 000 ₽ выглядела примечанием, и заказчик
   назвал это «допы прям очень сильно спрятаны».

   Выбранная строка отмечена фоном и рамкой, а не только точкой переключателя:
   цвет здесь дублирует форму контрола, а не заменяет её (WCAG 1.4.1).

   Строка занимает всю ширину поля, но само поле не шире --field-max: цена,
   уехавшая на 900px вправо от названия, перестаёт с ним читаться в паре.
   Исключение — раскрытая группа (F-172): она и так занимает ряд целиком,
   и там ограничение оставило бы половину ряда пустой. */

/* align-content: start — иначе строки растягиваются по высоте самого длинного
   поля в ряду: у «Каркаса» с двумя вариантами выбранная строка раздувалась
   на пол-колонки. */
.opt-field {
  align-content: start;
  border: 0;
  display: grid;
  gap: var(--space-2);
  margin: 0;
  max-width: var(--field-max);
  min-width: 0;
  padding: 0;
}

.optgrp-body > .opt-field {
  max-width: none;
}

.opt-field > .field-caption {
  padding: 0;
}

.opt-list {
  align-content: start;
  display: grid;
  gap: var(--space-05);
}

.configurator-fields .opt {
  align-items: center;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  flex-direction: row;
  font-size: var(--text-base);
  font-weight: var(--weight-normal);
  gap: var(--space-2);
  line-height: var(--leading-normal);
  min-height: var(--control-h);
  padding: var(--space-1) var(--space-2);
}

.configurator-fields .opt:hover {
  background: var(--state-hover);
}

.configurator-fields .opt:has(:checked) {
  background: var(--color-primary-soft);
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
  font-weight: var(--weight-medium);
}

.configurator-fields .opt:has(:disabled) {
  cursor: default;
  opacity: var(--opacity-disabled);
}

.opt input {
  flex-shrink: 0;
  height: 15px;
  margin: 0;
  width: 15px;
}

.opt-name {
  min-width: 0;
}

/* Вариант внутри группы: имя сверху, ставка за м² под ним — мельче и тише.
   Сравнивают варианты по сумме справа, ставка отвечает на второй вопрос,
   «почему столько», и мешать первому она не должна (F-175 §5). */
.opt-body {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
}

.opt-rate {
  color: var(--color-muted);
  font-size: var(--text-2xs);
  line-height: var(--leading-tight);
}

/* Базовый вариант помечен словом, а не нулём и не пустотой — так же, как
   «Included» у Salesforce CPQ (docs/reference/calculator-ux.md §3). */
.opt-price--included {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
}

/* Цена — цветом обычного текста, а не приглушённым: это то, что менеджер
   называет вслух. Табличные цифры, чтобы столбец цен не гулял. */
.opt-price {
  color: var(--color-text);
  flex-shrink: 0;
  margin-left: auto;
  padding-left: var(--space-3);
  white-space: nowrap;
}

.opt-price--calc {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

/* Вариант, которого у поставщика нет: значение строкой, без мёртвого контрола.
   Показать выбор, которого не существует, хуже, чем не показать выбор. */
.opt-fixed {
  color: var(--color-text);
  font-size: var(--text-base);
  margin: 0;
  min-height: var(--control-h);
  padding: var(--space-1) var(--space-2);
}

/* ── «Что добавить»: галочки допов в две колонки ──────────
   Веранда 33 %, сваи 31 %, сетка 8 %, ступени и пандус по 1 % — всё, что
   реально продаётся, в одной панели и без единого клика на раскрытие.
   Две колонки здесь законны: это не поля ввода, а строки-варианты, и правило
   Baymard про игнорируемую вторую колонку формы к ним не относится.

   Отмеченный пункт разворачивает свои поля и занимает ряд целиком: три поля
   веранды в колонке 300px встали бы столбиком. */
.opt-checks-grid {
  display: grid;
  gap: var(--space-05) var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
}

/* Платные галочки приезжают одним блоком на каждый пересчёт: их цена и само
   «включено» зависят от того, кто победил в сравнении поставщиков. Корень
   прозрачен для сетки — строки остаются её ячейками, как у .optgrp-set. */
.opt-checks-set {
  display: contents;
}

.opt-check-block {
  display: grid;
  gap: var(--space-2);
  min-width: 0;
}

.opt-check-block:has(.opt input:checked) {
  grid-column: 1 / -1;
}

/* ── Свёрнутая группа вариантов ───────────────────────────
   Главный приём экрана: строка «Внешняя отделка · Вагонка класса С —
   включено · изменить» одновременно показывает базу и открывает выбор.
   Поэтому этих позиций больше нет в «Входит в цену»: до F-172 база стояла на
   экране дважды, а 27 редких альтернатив занимали три четверти страницы.

   Раскрытие — галочка `open_groups` внутри формы: она уезжает на пересчёт и
   приезжает обратно, поэтому переживает внеполосный своп блока материалов.
   CSS открывает тело сразу по :checked, не дожидаясь ответа сервера.

   🔴 Тело раскрывается ПОВЕРХ страницы, а не в потоке (F-175 §1). Замер до
   правки: раскрытие двигало соседние строки на 157px и растило страницу с
   800 до 923px, то есть за пределы экрана 1280×800, ради которого F-172 и
   делалась. Резервировать высоту под самую высокую группу не годится — это
   постоянные 157px пустоты; у поповера сдвиг ноль ПО ПОСТРОЕНИЮ, при любом
   числе вариантов. Ширина поповера равна ширине своей строки: тогда он не
   может вылезти за панель ни на 375, ни на 1680, и горизонтального флипа не
   нужно вовсе. Вертикальный флип ставит класс optgrp--up (JS в шаблоне). */
.optgrp-list {
  display: grid;
  gap: 0 var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
}

/* Блок материалов приезжает по HTMX отдельным узлом, но его строки обязаны
   стоять в общей сетке рядом с перегородками, которые в него класть нельзя. */
.optgrp-set {
  display: contents;
}

.optgrp {
  border-bottom: 1px solid var(--color-border);
  min-width: 0;
  position: relative;
}

.optgrp-toggle {
  position: absolute;
  height: 1px;
  width: 1px;
  clip-path: inset(50%);
  overflow: hidden;
}

/* Селектор с .configurator-fields не для красоты: строка группы — это <label>,
   а он в форме расчёта по умолчанию колонка (подпись над полем). Без этого
   название, выбор и стрелка встают друг под другом. */
.configurator-fields .optgrp-row {
  align-items: baseline;
  border-radius: var(--radius-input);
  cursor: pointer;
  display: flex;
  flex-direction: row;
  gap: var(--space-2);
  min-height: var(--control-h);
  padding: var(--space-1) var(--space-2);
}

.configurator-fields .optgrp-row:hover {
  background: var(--state-hover);
}

.optgrp-toggle:focus-visible ~ .optgrp-row {
  box-shadow: var(--focus-ring);
}

.optgrp-title {
  color: var(--color-text);
  flex: 0 0 auto;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

/* Выбор — приглушённым, пока он базовый, и голосом основного текста, когда
   менеджер от базы отступил: отклонение обязано читаться не раскрывая группу. */
/* 🔴 nowrap, потому что перенос здесь ломает не строку, а число: подпись
   «Каменная вата 100 мм · +24 816 ₽» браузер рвал по пробелу внутри разряда —
   24 на одной строке, 816 на другой (замечено заказчиком 10.08.2026). Цену из
   строки после этого убрали совсем (она есть в панели итога), но запрет
   переноса остаётся: любое значение с числом рвалось бы так же. */
.optgrp-value {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin-left: auto;
  min-width: 0;
  overflow: hidden;
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* F-192: в утеплении теперь есть важная часть комплектации — мембрана.
   Эту единственную длинную базовую подпись нельзя прятать многоточием: цена
   из строки давно убрана, поэтому безопасно переносим текст целиком. */
#optgrp-insulation ~ .optgrp-row .optgrp-value {
  line-height: 1.25;
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}

.optgrp--changed .optgrp-value {
  color: var(--color-text);
  font-weight: var(--weight-medium);
}

/* Перегородки: строк в списке сервер не знает — их добавляет JS. Подпись
   переключается на само наличие строки, без второго механизма состояния. */
.optgrp-value--filled {
  display: none;
  margin-left: 0;
}

.optgrp:has(.house-dyn-row) .optgrp-value:not(.optgrp-value--filled) {
  display: none;
}

.optgrp:has(.house-dyn-row) .optgrp-value--filled {
  color: var(--color-text);
  display: block;
  font-weight: var(--weight-medium);
  margin-left: auto;
}

.optgrp-caret {
  color: var(--color-faint);
  flex: 0 0 auto;
  position: relative;
  top: 3px;
  transition: transform var(--transition-fast);
}

.optgrp-toggle:checked ~ .optgrp-row .optgrp-caret {
  transform: rotate(180deg);
}

/* Поповер: тень означает «элемент физически над страницей» — ровно тот случай,
   под который в системе заведён --elevation-overlay. Ширина от строки, потолок
   высоты 60vh со своей прокруткой: семь высот стен в поповере у нижнего края
   иначе не поместились бы даже после флипа вверх. */
/* 🔴 position: fixed, а не absolute — и это ровно то, чем измеряется §1.
   Абсолютно спозиционированный поповер всё ещё входит в область прокрутки
   страницы: соседей он не двигает, но высоту документа растит (замер: +70…102px
   на разных группах, то есть экран 1280×800 снова получает прокрутку).
   Фиксированный слой не участвует в потоке ни одним способом — высота документа
   не меняется ПО ПОСТРОЕНИЮ, а не потому что мы подобрали отступы.
   Координаты ставит JS в --pop-* при открытии; он же решает, вниз или вверх. */
.optgrp-body {
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-overlay);
  display: none;
  left: var(--pop-left, 0);
  max-height: var(--pop-max-h, 60vh);
  max-width: min(440px, calc(100vw - 2 * var(--space-2)));
  min-width: var(--pop-width, var(--field-min));
  overflow-y: auto;
  padding: var(--space-2);
  position: fixed;
  top: var(--pop-top, 0);
  width: max-content;
  z-index: var(--z-dropdown);
}

.optgrp-toggle:checked ~ .optgrp-body {
  display: block;
}

/* Внутри поповера варианты столбиком: ширина у него ровно строки (≈460px на
   1280), и вторая колонка резала бы подписи вроде «Металлочерепица». */
.optgrp-body .opt-list {
  grid-template-columns: 1fr;
}

/* Строка группы, отличающаяся от базы, обязана быть заметна без чтения всех
   восьми (F-175 §3). Три канала сразу, а не один цвет: заливка, точка перед
   названием и голос основного текста у значения. Точка — потому что цветом
   одним ничего сообщать нельзя (WCAG 1.4.1), а слово «изменено» в колонку
   326px не влезает — оно осталось для скринридера. */
.optgrp--changed > .optgrp-row {
  background: var(--color-primary-soft);
}

.optgrp--changed .optgrp-title::before {
  background: var(--color-primary);
  border-radius: var(--radius-full);
  content: "";
  display: inline-block;
  height: 6px;
  margin-right: var(--space-1);
  vertical-align: middle;
  width: 6px;
}

/* Строка без выбора — каркас там, где у поставщика он один. Тот же ряд, но не
   притворяется нажимаемым: ни курсора, ни подсветки, ни шеврона. */
.configurator-fields .optgrp-row--static,
.configurator-fields .optgrp-row--static:hover {
  background: transparent;
  cursor: default;
}

/* ── Ширина × длина одним полем ───────────────────────────
   «6 × 2» — то, как менеджер называет размер вслух; два поля в ряд вместо
   двух рядов. Знак умножения — разделитель, а не подпись: aria-hidden. */
.field-pair {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.field-pair-controls {
  align-items: stretch;
  display: flex;
  gap: var(--space-2);
}

.field-pair-controls select {
  flex: 1 1 0;
  min-width: 0;
}

.field-pair-sign {
  /* Прижат к строке контролов: у полей появились свои подписи, и знак,
     центрированный по всей высоте, повисал бы между ними. */
  align-self: end;
  color: var(--color-muted);
  flex: 0 0 auto;
  padding-bottom: 9px;
}

/* ── Шапка расчёта одной строкой ──────────────────────────
   Три ряда шапки — 112px, которых не хватает форме: цель F-172 в том, чтобы
   бытовка без допов влезала в экран без прокрутки. */
.quote-head-line {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
}

.quote-head-line h1 {
  margin: 0;
}

.quote-head-line .back-link,
.quote-head-line .deal-subtitle {
  margin: 0;
}

/* Поля веранды и свай не занимают места, пока галочка снята: до F-172 они
   стояли на экране всегда и стоили 150px в состоянии «выключено». Погашенными
   их держать нечестно — выключенное поле читается как «сломано», а не как
   «включи галочку». Сами поля при этом disabled — их ставит JS. */
[data-dependent-fields].is-disabled {
  display: none;
}

.house-quote-dependent-grid {
  display: grid;
  gap: var(--space-2) var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
}

/* ── Смета ────────────────────────────────────────────────
   Липнет к верху колонки: сумма видна на любой прокрутке формы. */
.configurator-rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-height: calc(100vh - var(--space-8));
  min-width: 0;
  position: sticky;
  top: var(--space-4);
}

/* Прокручивается список добавок, а не карточка целиком: «Итого» и объект
   расчёта обязаны оставаться в кадре при любом числе строк. */
.qsum-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-flat);
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--space-4);
}

.qsum-head {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.qsum-object {
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

.qsum-dims {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

/* Третья строка шапки: комплектация и поставщик. Тон ещё глуше размеров —
   это справка, а не то, что менеджер называет клиенту. Отдельной панели они
   не заслуживают: это подпись к строению, а панель читалась бы как новая
   сущность на экране, где их и так восемь. */
.qsum-meta {
  align-items: center;
  color: var(--color-faint);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-2xs);
  gap: var(--space-1);
  margin-top: 2px;
}

/* Комплектацию отделяем рамкой, а не цветом: цвет в этом проекте занят
   состоянием (docs/reference/crm-ux-patterns.md), и зелёный «Комфорт» рядом с
   красной просрочкой читался бы как ещё один светофор. */
.qsum-package {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0 5px;
  white-space: nowrap;
}

/* Итог — самое крупное на экране: его читают вслух. */
.qsum-total {
  align-items: baseline;
  border-bottom: 1px solid var(--color-border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: space-between;
  padding: var(--space-3) 0;
}

.qsum-total-label {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

/* Второй ярус веса и самый крупный текст на экране: цену менеджер называет в
   трубку, и она обязана находиться взглядом раньше всего остального, включая
   заголовок страницы (F-175 §2). */
.qsum-total-price {
  color: var(--color-text);
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
}

.qsum-lines {
  display: block;
  margin: 0;
  min-height: 0;
  overflow-y: auto;
}

.qsum-head,
.qsum-total {
  flex: 0 0 auto;
}

.qsum-line {
  align-items: baseline;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
  padding: var(--space-2) 0;
}

.qsum-line + .qsum-line {
  border-top: 1px solid var(--color-border);
}

.qsum-line dt {
  color: var(--color-muted);
  font-size: var(--text-xs);
  min-width: 0;
}

.qsum-line dd {
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  margin: 0;
  text-align: right;
  white-space: nowrap;
}

/* Состав, вошедший в базовую цену, — перечисление словами, а не сумма. Поэтому
   переносится по строкам (в отличие от денег, которые не рвутся) и приглушён:
   спорить с итогом справа ему нельзя. */
.qsum-line--note dd {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  white-space: normal;
}

.qsum-warnings {
  color: var(--color-warning-text);
  display: grid;
  gap: var(--space-1);
  padding-top: var(--space-3);
}

.qsum-warnings p {
  align-items: center;
  display: flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  margin: 0;
}

.qsum-note {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: var(--space-3) 0 0;
}

/* Оговорка мягкого предела на карточке расчёта. Свой класс, не .qsum-warnings:
   тот принадлежит рельсу калькулятора и меняется вместе с ним. */
.quote-delivery-approval {
  color: var(--color-warning-text);
  font-size: var(--text-xs);
  margin: var(--space-2) 0 0;
}

/* Загрузка сметы: цифры не мигают, панель слегка гаснет */
#configurator-summary.htmx-request .qsum-card {
  opacity: 0.6;
  transition: opacity var(--transition);
}

.qsum-actions {
  display: flex;
  flex: 0 0 auto;
  gap: var(--space-3);
}

.qsum-actions .primary-action {
  flex: 1;
  justify-content: center;
}

/* На широком экране сумма уже названа в карточке — в кнопочной строке
   её дублировать незачем; и раскрывать там нечего, разбивка видна всегда,
   поэтому галочка не занимает таб-стоп. */
.qsum-bar-total {
  display: none;
}

.qsum-expand-toggle {
  display: none;
}

/* ── Куда везём: строка адреса, карта и лист ──────────────────────────────
   Проект и замеры — docs/reference/delivery-on-quote-screen.md. Главное число
   раскладки: в форму влезает ОДИН ряд (97 px свободных), поэтому доставка —
   строка, а не панель; карта живёт в рельсе, где простаивает 418 px. */
.delivery-row {
  align-items: center;
  display: grid;
  gap: var(--space-1) var(--space-3);
  /* 🔴 Подпись стоит В СТРОКЕ, а не над полем: замер 18.08.2026 — подпись
     сверху стоила 23 px, а свободного места под формой всего 97. Адрес
     забирает всё, что осталось: он длиннее всего и его читают. Поле
     километров — ровно под «1234,5» с единицей, кнопка карты — под слово. */
  grid-template-columns: auto minmax(0, 1fr) 104px auto;
}

/* Селектор с .configurator-fields — иначе подпись перебивает общее правило
   `.configurator-fields label` (label-токены 12/400) и строка «сливается и
   не имеет веса» (владелец 19.08.2026). Голос — ярус «Что добавить»: 13/600. */
.configurator-fields .delivery-row-label,
.delivery-row-label {
  align-items: center;
  color: var(--color-text);
  display: inline-flex;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  gap: var(--space-1);
  white-space: nowrap;
}

/* Панель доставки чуть плотнее обычной: строка обязана оставить экрану
   1280×800 жизнь без прокрутки (замер: свободных было 0 px). */
.delivery-panel .panel-body,
.panel--openings .panel-body {
  padding: var(--space-2) var(--space-3);
}

.delivery-address-wrap {
  min-width: 0;
  position: relative;
}

.delivery-address-wrap input {
  width: 100%;
}

.delivery-km-wrap {
  align-items: center;
  display: flex;
  gap: var(--space-1);
}

.delivery-km-unit {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

.delivery-km-wrap input {
  min-width: 0;
  width: 100%;
}

/* «карта» — не кнопка действия, а ссылка на подтверждение: рядом с полем,
   которое уже несёт цену, вторая заметная кнопка спорила бы с «Создать
   расчёт». На широком экране её нет: карта уже видна справа, и второй вход
   в тот же лист давал «2 экрана карты» (владелец 19.08.2026) — лист там
   открывается кликом по самой карте. */
.delivery-sheet-open {
  background: none;
  border: 0;
  color: var(--color-primary);
  cursor: pointer;
  display: none;
  font-size: var(--text-xs);
  padding: 0;
  white-space: nowrap;
}

/* Откуда взялись километры — словом, а не только цветом (WCAG 1.4.1). */
.delivery-row-meta {
  color: var(--color-muted);
  font-size: var(--text-2xs);
  grid-column: 1 / -1;
  margin: 0;
  min-height: 1em;
}

.delivery-row-meta.is-manual {
  color: var(--color-warning-text);
}

/* Пустая мета схлопывается: без адреса панель не тратит высоту на пустоту
   (разметка узла однострочная намеренно — перенос сделал бы его непустым). */
.delivery-row-meta:empty {
  display: none;
}

.delivery-suggestions {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--elevation-overlay);
  display: grid;
  left: 0;
  max-height: 280px;
  overflow-y: auto;
  position: absolute;
  right: 0;
  top: calc(100% + 2px);
  z-index: var(--z-dropdown);
}

.delivery-suggestion {
  background: none;
  border: 0;
  border-radius: 0;
  /* 🔴 color и weight обязательны: глобальное `button` красит голую кнопку в
     синий с БЕЛЫМ текстом; фон здесь снят, и без своего цвета текст подсказки
     оставался белым на белой панели — невидимые, но кликабельные строки.
     Найдено владельцем на проде 19.08.2026, локально не ловилось: подсказки
     живут только с прод-ключом Яндекса. */
  color: var(--color-text);
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  justify-content: flex-start;
  min-height: 0;
  padding: var(--space-2);
  text-align: left;
}

.delivery-suggestion:hover,
.delivery-suggestion:focus-visible {
  background: var(--color-surface-sunken);
  /* Тот же капкан, что и в базовом состоянии: глобальный button:hover красит
     текст в --color-on-primary (белый). Без своего цвета подсказка на ховере
     сливалась со светлым фоном — поймал владелец на проде 19.08.2026. */
  color: var(--color-text);
}

/* Карта стоит СНАРУЖИ #configurator-summary соседним узлом рельса: панель
   подменяется целиком на каждый пересчёт, и карта внутри неё пересоздавалась
   бы на каждый клик по любой опции.

   🔴 Рельс и карта завёрнуты в .configurator-side — ОДИН узел грида. Слот сам
   по себе авторазмещением уезжал во вторую строку — ниже всей формы (замер
   18.08.2026: рельс кончался на 256px, слот начинался на 782px). Липкость
   живёт на обёртке: грид-область одиночного рельса ростом с сам рельс, и
   sticky в ней инертен. */
.configurator-side {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
  position: sticky;
  top: var(--space-4);
}

.configurator-map-slot {
  min-width: 0;
}

/* Фасада-заглушки больше нет (владелец 19.08.2026): карта живая с открытия —
   до адреса склад поставщика, после адреса маршрут. Клик открывает лист. */
.delivery-map {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  cursor: pointer;
  display: block;
  height: 176px;
  overflow: hidden;
  width: 100%;
}

/* Подпись — тем же голосом, что подписи строк сметы: карта не сирота под
   рельсом, а продолжение строки «Доставка» разбивки. */
.delivery-map-caption {
  align-items: center;
  color: var(--color-muted);
  display: flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  margin: 0 0 var(--space-1);
}

.delivery-map-note {
  font-size: var(--text-2xs);
  margin: 0;
}

/* Лист адреса — position: fixed, как поповеры групп (F-175): 0 сдвига соседей
   и 0 роста документа. За листом прокручивать нечего, поэтому карта на
   телефоне не становится ловушкой прокрутки. */
.delivery-sheet {
  inset: 0;
  position: fixed;
  z-index: var(--z-modal);
}

.delivery-sheet[hidden] {
  display: none;
}

.delivery-sheet-backdrop {
  background: color-mix(in srgb, var(--color-text) 45%, transparent);
  inset: 0;
  position: absolute;
}

/* Лист адреса. Тени не было вовсе — панель висела над затемнением на одном
   скруглении. `--elevation-overlay` заведён в токенах ровно под «меню, модалка»
   и означает «элемент физически над страницей»; здесь это ровно тот случай. */
.delivery-sheet-body {
  background: var(--color-surface);
  border-radius: var(--radius-panel);
  bottom: auto;
  box-shadow: var(--elevation-overlay);
  display: grid;
  gap: var(--space-3);
  left: 50%;
  max-height: 90vh;
  max-width: 480px;
  overflow-y: auto;
  padding: var(--space-4);
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(92vw, 480px);
}

.delivery-sheet-head {
  align-items: center;
  display: flex;
  justify-content: space-between;
}

.delivery-sheet-close {
  background: none;
  border: 0;
  cursor: pointer;
  font-size: var(--text-md);
  padding: var(--space-1);
}

/* Крестик — единственный выход из листа на телефоне, а лист на телефоне и
   открывают: карта адреса нужна в поле. 24 px под палец мало. */
@media (hover: none) {
  .delivery-sheet-close {
    margin: calc(var(--space-1) * -1);
    min-height: 44px;
    min-width: 44px;
  }
}

.delivery-sheet-address {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: 0;
}

.delivery-sheet-map {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  height: 45vh;
  overflow: hidden;
}

.delivery-sheet-routes {
  display: grid;
  gap: var(--space-1);
  margin: 0;
}

.delivery-sheet-routes > div {
  display: flex;
  font-size: var(--text-xs);
  gap: var(--space-2);
  justify-content: space-between;
}

.delivery-sheet-routes dt {
  color: var(--color-muted);
}

.delivery-sheet-routes dd {
  margin: 0;
}

.delivery-sheet-field {
  display: grid;
  gap: 2px;
}

/* Строка километража под названием рейса: она объясняет сумму справа и не
   должна с ней спорить. */
.qsum-line-origin {
  color: var(--color-faint);
  display: block;
  font-size: var(--text-2xs);
}

/* «Радиус не проверен» — подпись к поставщику, а не ещё одна сущность. */
.qsum-radius-note {
  white-space: nowrap;
}

/* Состояние 2 — notification, не warning: чинить нечего, цена верна. Тон
   приглушённый, цвета нет (NN/g: indicators / validations / notifications). */
.qsum-notes {
  color: var(--color-muted);
  display: grid;
  gap: var(--space-1);
  padding-top: var(--space-3);
}

.qsum-notes p {
  align-items: flex-start;
  display: flex;
  font-size: var(--text-2xs);
  gap: var(--space-1);
  margin: 0;
}

/* 🔴 Заблокированная кнопка обязана говорить, почему. Фраза — ссылка на поле
   адреса: на 1280 оно уже в кадре, путь нулевой. */
.qsum-block-reason {
  font-size: var(--text-xs);
  margin: var(--space-2) 0 0;
  text-align: center;
}

/* ── Телефон и планшет: смета полосой поверх страницы ─────
   Форма длинная; без этой полосы менеджер видит сумму только долистав
   до конца, а он в этот момент говорит с клиентом.

   До F-172 в полосе оставалась одна сумма, и «что именно вошло в цену»
   на 375 и 768 px не было видно нигде. Теперь разбивка та же самая и
   разворачивается тапом по полосе — второй копии строк на экране нет. */
@media (max-width: 980px) {
  /* Обёртка исчезает из раскладки: полоса итога у рельса fixed, карта скрыта,
     и колонка-контейнер на телефоне только мешала бы. Селекторы соседства
     (#qsum-expand ~ .configurator-rail) живут в DOM и от этого не зависят. */
  .configurator-side {
    display: contents;
  }

  /* 🔴 На 375 инлайновой карты нет вообще: она ловушка прокрутки, а
     документированного gestureHandling у Яндекс JS API 2.1 не нашлось. Маршрут
     остаётся словами, карта открывается листом поверх экрана. */
  .configurator-map-slot {
    display: none;
  }

  /* Без инлайновой карты слово «карта» — единственный вход в лист. */
  .delivery-sheet-open {
    display: inline;
  }

  /* Адрес во всю ширину: четыре ячейки в ряд на 375 рвут подписи. */
  .delivery-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .delivery-row-label {
    grid-column: 1 / -1;
  }

  .delivery-address-wrap {
    grid-column: 1 / -1;
  }

  .configurator {
    grid-template-columns: 1fr;
    /* 76px — высота полосы итога; панель разделов стоит под ней (F-220). */
    padding-bottom: calc(76px + var(--nav-bar-space));
  }

  .qsum-expand-toggle {
    clip-path: inset(50%);
    display: block;
    height: 1px;
    overflow: hidden;
    position: absolute;
    width: 1px;
  }

  /* 🔴 Полоса итога поднята над панелью разделов (F-220). Обе прижаты к низу и
     обе `fixed`: при `bottom: 0` панель закрывала сумму и кнопку расчёта
     целиком — а расчёт это единственное, чем менеджеры в CRM пользуются. */
  .configurator-rail {
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    bottom: var(--nav-bar-space);
    box-shadow: var(--elevation-overlay);
    gap: 0;
    left: 0;
    max-height: none;
    position: fixed;
    right: 0;
    top: auto;
    z-index: var(--z-sticky);
  }

  .qsum-card {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    display: none;
    max-height: 55vh;
    padding-bottom: var(--space-2);
  }

  .qsum-expand-toggle:checked ~ .configurator-rail .qsum-card {
    display: flex;
  }

  .qsum-actions {
    align-items: center;
    justify-content: space-between;
    padding: var(--space-2) var(--space-4);
  }

  .qsum-actions .primary-action {
    flex: 0 0 auto;
  }

  .qsum-bar-total {
    cursor: pointer;
    display: flex;
    flex-direction: column;
    line-height: var(--leading-tight);
    min-width: 0;
  }

  .qsum-expand-toggle:focus-visible ~ .configurator-rail .qsum-bar-total {
    box-shadow: var(--focus-ring);
  }

  /* Полоса экрана расчёта (.quote-bar-label) говорит тем же голосом, что
     полоса калькулятора: подпись и сумма правятся здесь, в одном месте. */
  .qsum-bar-label,
  .quote-bar-label {
    color: var(--color-muted);
    font-size: var(--text-2xs);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .qsum-bar-sum {
    align-items: center;
    display: flex;
    gap: var(--space-1);
  }

  .qsum-bar-total .mono,
  .quote-bar-total strong {
    font-size: var(--text-lg);
    font-weight: var(--weight-bold);
  }

  .qsum-bar-caret {
    color: var(--color-muted);
    transition: transform var(--transition-fast);
  }

  .qsum-expand-toggle:checked ~ .configurator-rail .qsum-bar-caret {
    transform: rotate(180deg);
  }
}
/* ── Диалог выбора цвета профлиста ── */
/* ── Диалог выбора цвета кровли ── */
/* Окно выбора цвета. Радиус и высота — ПО РОЛИ, как велит система: модалка это
   панель поверх страницы, а не контрол. Было `--radius-md` (радиус кнопки) и
   `box-shadow: none` — при том что `--elevation-overlay` заведён в токенах
   ровно под «меню, модалка» и нигде здесь не применялся. */
.color-dialog {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-overlay);
  max-width: 560px;
  padding: var(--space-5) var(--space-6);
  width: 90%;
}

.color-dialog::backdrop {
  background: color-mix(in srgb, var(--color-text) 45%, transparent);
}

.color-dialog-head {
  align-items: center;
  display: flex;
  justify-content: space-between;
  margin-bottom: var(--space-4);
}

.color-dialog-title {
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
}

.color-dialog-close {
  background: none;
  border: none;
  color: var(--color-muted);
  cursor: pointer;
  font-size: var(--text-lg);
  line-height: 1;
  padding: var(--space-05) var(--space-2);
}

/* Единственный выход из окна, кроме Esc, — и он был 26×34 при норме 44 для
   пальца. Растёт только область нажатия; вид на мыши не меняется. Отступ
   компенсирован отрицательным полем, чтобы крестик не уехал от края. */
@media (hover: none) {
  .color-dialog-close {
    margin: calc(var(--space-2) * -1);
    min-height: 44px;
    min-width: 44px;
  }
}

.color-dialog-hint {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-4);
}

.color-dialog-grid {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* На телефоне три колонки дают карточку 91 px, и название цвета в неё не
   помещается. Две колонки — 143 px, помещается любое из шести. Замер, а не
   вкус: «Тёмно-коричневый» требовал 80 px при доступных 70. */
@media (max-width: 560px) {
  .color-dialog-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.color-dialog-footer {
  border-top: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}

/* Живая смета: приглушается на время пересчёта (hx-indicator), а кнопка
   сохранения рядом блокируется — менеджер не сохранит по устаревшей цифре */
#configurator-summary.htmx-request {
  opacity: 0.6;
  transition: opacity var(--transition-fast);
}

#configurator-summary.htmx-request + .qsum-submit-btn {
  opacity: 0.7;
  pointer-events: none;
}

/* Карточка цвета кровли. 🔴 Здесь было три вещи, из-за которых менеджер почти
   не видел, ЧТО он выбрал, — а выбирает он цвет крыши живому клиенту:

   1. Заливка выбранной ссылалась на `--color-primary-bg` — токена с таким
      именем в проекте нет вовсе (объявлен ноль раз, использован один). Браузер
      молча выбрасывает такую строку: ни ошибки, ни заливки. Настоящий токен —
      `--color-primary-soft`.
   2. Рамка была 0.5 px при 1 px во всём остальном проекте: на обычном мониторе
      полупиксельная линия рисуется бледнее целой, и выбор терялся.
   3. Отличие держалось ТОЛЬКО на цвете рамки. Цвет — не единственный признак
      состояния (WCAG 1.4.1): теперь ещё заливка и толщина.

   Замер 23.08.2026: до правки вычисленный фон выбранной и невыбранной карточки
   совпадал — `rgba(0, 0, 0, 0)` у обеих. */
.prof-color-card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-input);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.prof-color-card:hover { border-color: var(--color-border-strong); }

.prof-color-card.is-selected {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  border-width: 2px;
  /* Рамка растолстела на 1 px — забираем его у отступа, иначе карточка
     подпрыгивает в момент выбора. */
  padding: calc(var(--space-3) - 1px);
}

/* Название цвета переносится в любом месте: «Тёмно-коричневый» на 375 px не
   влезает в колонку 70 px и без этого вылезал за карточку (замер: нужно 80 px). */
.prof-color-name { overflow-wrap: anywhere; }

.prof-color-swatch-big {
  border: 1px solid color-mix(in srgb, var(--color-text) 15%, transparent);
  border-radius: var(--radius-sm);
  height: 48px;
  width: 100%;
}
.prof-color-code {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 500;
}
.prof-color-name {
  color: var(--color-text);
  font-size: var(--text-sm);
}

/* Строка выбора цвета кровли под группой материала. Цвет на цену не влияет,
   но обязан попасть в КП, поэтому «не выбран» — видимое состояние с действием
   рядом, а не тишина. Образец идёт вместе с названием: цветом одним смысл не
   передаётся. */
.color-pick {
  align-items: center;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  max-width: var(--field-max);
  min-height: var(--control-h);
  padding: var(--space-1) var(--space-2);
}

.color-pick-caption {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.color-pick-swatch {
  border: 1px solid color-mix(in srgb, var(--color-text) 15%, transparent);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  height: 18px;
  width: 18px;
}

.color-pick-name {
  min-width: 0;
}

.color-pick-name--empty {
  color: var(--color-muted);
}

.color-pick .secondary-button {
  margin-left: auto;
}

.quote-category-card h2 {
  font-size: var(--text-lg);
  line-height: 1.25;
  margin: 0;
}

.quote-category-icon {
  align-items: center;
  background: var(--color-primary-soft);
  border-radius: var(--radius-md);
  color: var(--color-primary);
  display: inline-flex;
  font-size: var(--text-lg);
  font-weight: 800;
  height: 48px;
  justify-content: center;
  width: 48px;
}

.quote-category-description {
  color: var(--color-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
  margin: 0;
}

.quote-category-count {
  color: var(--color-neutral-text);
  font-size: var(--text-xs);
  font-weight: 700;
  margin-top: auto;
}

.quote-product-search {
  grid-template-columns: minmax(220px, 1fr) auto auto;
}

.catalog-section {
  display: grid;
  gap: var(--space-3);
}

.catalog-section + .catalog-section {
  margin-top: var(--space-6);
}

.catalog-section-header {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
}

.catalog-section-header h2 {
  font-size: var(--text-lg);
  margin: 0;
}

.catalog-back-link {
  color: var(--color-muted);
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: 700;
  margin-bottom: var(--space-1);
}

.catalog-section-count {
  background: var(--color-neutral-bg);
  border-radius: 999px;
  color: var(--color-neutral-text);
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 5px 8px;
  white-space: nowrap;
}

.catalog-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: grid;
  gap: var(--space-4);
  padding: var(--space-4);
}

.catalog-thumb {
  align-items: center;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-neutral-text);
  display: flex;
  font-size: var(--text-lg);
  font-weight: 800;
  justify-content: center;
  min-height: 112px;
}

.catalog-thumb-large {
  font-size: 32px;
  min-height: 180px;
}

.catalog-thumb-icon {
  width: 36px;
  height: 36px;
}

.catalog-thumb-large .catalog-thumb-icon {
  width: 56px;
  height: 56px;
}

/* Цвета плейсхолдеров по категории */
.catalog-thumb-bytovki { background: var(--color-primary-soft); color: var(--color-primary); }
.catalog-thumb-hozbloki { background: var(--color-success-bg); color: var(--color-success-text); }
.catalog-thumb-sadovye-domiki { background: var(--color-warning-bg); color: var(--color-warning-text); }
.catalog-thumb-karkasnye-doma { background: var(--color-neutral-bg); color: var(--color-neutral-text); }
.catalog-thumb-default { background: var(--color-surface-sunken); color: var(--color-neutral-text); }

.catalog-card-title {
  font-size: var(--text-md);
  line-height: 1.3;
  margin: 0;
}

.catalog-meta {
  color: var(--color-muted);
  display: grid;
  font-size: var(--text-xs);
  gap: var(--space-2);
  margin: 0;
}

.catalog-meta dd {
  color: var(--color-text);
}

.catalog-card-footer {
  align-items: stretch;
  border-top: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-top: var(--space-3);
}

.catalog-card-actions {
  align-items: stretch;
  display: flex;
  flex-direction: row;
  gap: var(--space-2);
}

.catalog-quote-action,
.catalog-card-secondary-action {
  margin: 0;
  min-width: 0;
}

.catalog-quote-action {
  flex: 1.35 1 0;
}

.catalog-card-secondary-action {
  flex: 1 1 0;
}

.catalog-quote-action .primary-action,
.catalog-card-secondary-action {
  width: 100%;
}

.catalog-detail-actions {
  justify-content: space-between;
}

.catalog-detail-actions form {
  margin: 0;
}

.catalog-price {
  display: block;
  font-size: var(--text-lg);
}

.catalog-price-list {
  display: grid;
  gap: var(--space-2);
  margin: 0;
}

.catalog-price-list div {
  display: grid;
  gap: 2px;
}

.catalog-price-list dt {
  color: var(--color-muted);
}

.catalog-price-list dd {
  color: var(--color-text);
  font-weight: 700;
}

.catalog-attrs-list {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
}

.catalog-attrs-list div {
  border-top: 1px solid var(--color-border);
  display: grid;
  gap: 2px;
  padding-top: var(--space-2);
}

.catalog-attrs-list dd {
  color: var(--color-text);
  overflow-wrap: anywhere;
}

.catalog-source-admin {
  flex: 0 0 auto;
}

.catalog-detail {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(0, 1fr) auto;
  margin-bottom: var(--space-4);
  padding: var(--space-5);
}

.catalog-detail-main {
  align-items: center;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
}

.catalog-detail-main h2 {
  font-size: var(--text-xl);
  margin: 0;
}

.catalog-detail-price {
  align-content: center;
  display: grid;
  gap: var(--space-3);
  justify-items: end;
}

.catalog-detail-grid {
  align-items: start;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (max-width: 980px) {
  .app-shell {
    grid-template-columns: 1fr;
    min-width: 0;
    overflow-x: hidden;
  }

  /* ── Нижняя панель разделов (F-220) ───────────────────────
     До F-220 сайдбар оставался наверху горизонтальной лентой. Замер 15.08.2026
     на 375×812: в ленту влезали три пункта, третий — обрезанное на середине
     слова «Канба», лента прокручивалась на 1033 px без единого признака
     прокрутки, а сама полоса забирала 390 px первого экрана. Обрезанное слово
     читается как дефект вёрстки, а не как приглашение прокрутить.

     Взято у рынка (`docs/reference/crm-ux-patterns.md`): нижняя панель не
     больше чем на пять слотов, иконка плюс подпись, текущий раздел выделен.
     Четыре раздела и «Ещё»; какие именно четыре — считает `templatetags/nav.py`.
     Разметка одна и та же, что в сайдбаре: вторая копия означала бы второй
     запрос за счётчиком непрочитанных на каждой странице. */
  .sidebar {
    background: var(--color-sidebar);
    border-right: 0;
    border-top: 1px solid var(--color-sidebar-border);
    bottom: 0;
    left: 0;
    min-width: 0;
    /* Жест-бар iPhone: панель поднимается над ним, а не под него. */
    padding: 0 0 env(safe-area-inset-bottom);
    position: fixed;
    right: 0;
    width: 100%;
    /* Не `--z-dropdown`: меню и подсказки, раскрывающиеся вверх у нижнего края
       (вложения в ленте, подсказки адреса), обязаны лежать поверх панели. */
    z-index: var(--z-sticky);
  }

  .main-area {
    min-width: 0;
    /* Место под фиксированную панель: без этого последняя строка списка
       оказывается под ней и до неё нельзя дотянуться. */
    padding-bottom: var(--nav-bar-space);
  }

  /* Название продукта на телефоне не нужно: заголовок раздела и так стоит в
     шапке, а строка забирала высоту у работы. */
  .brand {
    display: none;
  }

  .nav-list {
    align-items: stretch;
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    min-width: 0;
  }

  /* Слоты панели делят ширину поровну; всё остальное скрыто и приезжает по
     «Ещё». Прокрутки у панели нет вовсе — прокручивать нечего. */
  .nav-item {
    display: none;
  }

  /* Слоты растут от подписи, а не делят ширину поровну: при равных долях
     «Непрочитанные» требовало 79 px в слоте на 65 и обрезалось — тот самый
     дефект, ради которого заведена F-220. Свободное место делится поровну,
     поэтому иконки всё равно стоят ровным рядом. */
  .nav-item--bar {
    align-items: center;
    border-radius: 0;
    flex: 1 1 auto;
    flex-direction: column;
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    gap: 2px;
    justify-content: center;
    min-height: 56px;
    min-width: 0;
    padding: var(--space-1) 2px;
    display: flex;
  }

  /* Кнопка «Ещё» — обычный элемент панели, а не форма. */
  .nav-more {
    background: none;
    border: 0;
    color: var(--color-sidebar-text);
    cursor: pointer;
    font-family: inherit;
  }

  /* 10 px — размер подписи в панели у iOS и Android, и здесь он не украшение:
     в слот 75 px (375 / 5) при 12 px не помещается «Непрочитанные», а
     обрезанная подпись — ровно тот дефект, из-за которого заведена F-220.
     Название раздела на телефоне и на десктопе обязано совпадать, поэтому
     сокращать слова нельзя — уменьшается кегль. */
  .nav-item--bar .nav-label {
    font-size: 10px;
    letter-spacing: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Текущий раздел — заливкой во всю ячейку панели, как в сайдбаре. */
  .nav-item.active {
    border-radius: 0;
  }

  /* Лист «Ещё»: пункты выходят над панелью, каждый своей строкой. `order: -1`
     ставит их перед слотами панели в том же flex-контейнере — второй контейнер
     развалил бы сетку сайдбара на десктопе. */
  .nav-more-open .nav-item:not(.nav-item--bar) {
    align-items: center;
    border-radius: 0;
    display: flex;
    flex: 1 1 100%;
    min-height: 48px;
    order: -1;
    padding: var(--space-2) var(--space-4);
  }

  .nav-more-open .nav-list {
    border-bottom: 1px solid var(--color-sidebar-border);
  }

  /* Счётчик на телефоне — на углу иконки, а не после подписи: в слот панели
     подпись со счётчиком в строку не помещается. Порядок узлов в разметке при
     этом остаётся десктопным — иконка, подпись, счётчик. */
  .nav-item--bar {
    position: relative;
  }

  .nav-item--bar .nav-badge-slot {
    left: 50%;
    margin-left: 4px;
    position: absolute;
    top: 4px;
  }

  /* Пальцем, а не мышью: 30 px десктопной панели — непопадаемая цель (норма
     44 px, Apple HIG / Material). На десктопе высота остаётся прежней. */
  .order-filters :is(input, select, .secondary-button) {
    min-height: 44px;
  }

  .sidebar-note {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
  }

  .toolbar,
  .catalog-toolbar,
  .catalog-detail,
  .catalog-detail-main,
  .catalog-detail-grid,
  .quote-detail-grid,
  .summary-panel,
  .detail-layout,
  .workspace-grid,
  .workspace-secondary {
    grid-template-columns: 1fr;
  }

  .quote-product-search {
    grid-template-columns: 1fr;
  }

  .topbar {
    align-items: flex-start;
    gap: var(--space-2);
    flex-direction: column;
  }

  /* В столбце прижимать полосу вправо нечем и незачем: `margin-left: auto`
     из широкой раскладки увёл бы её к правому краю в отрыве от вкладок. */
  .topbar .deal-result-bar {
    margin: 0;
  }
}

@media (max-width: 900px) {
  .quote-hero,
  .quote-offer-header {
    grid-template-columns: 1fr;
  }

  .quote-totals-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    max-width: none;
  }

  .quote-price-breakdown {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .quote-hero {
    align-items: stretch;
  }

  .quote-hero-total {
    text-align: left;
    width: 100%;
  }

  .quote-hero-side {
    justify-items: stretch;
    min-width: 0;
  }

  .quote-options-list li {
    align-items: flex-start;
    grid-template-columns: 1fr;
  }

  .quote-options-list strong {
    justify-self: start;
  }

}

@media (max-width: 820px) {
  .quote-delivery-layout {
    grid-template-areas:
      "address"
      "map"
      "route"
      "comment";
    grid-template-columns: 1fr;
  }
}

@media (max-width: 720px) {
  body {
    font-size: var(--text-md);
  }

  .sidebar,
  .topbar,
  .content,
  .prototype-note {
    padding-left: var(--space-4);
    padding-right: var(--space-4);
  }

  .topbar h1,
  .summary-main h2 {
    font-size: 22px;
  }

  .toolbar {
    align-items: stretch;
  }

  .toolbar button,
  .row-action,
  .primary-action,
  .secondary-button {
    min-height: 42px;
  }

  .page-actions {
    align-items: stretch;
    flex-direction: column;
  }

  .catalog-card-actions {
    flex-direction: column;
  }

  .catalog-detail-actions form,
  .catalog-detail-actions .primary-action,
  .catalog-detail-actions .secondary-button {
    width: 100%;
  }

  .quote-delivery-map {
    height: 260px;
  }

  .quote-delivery-metrics {
    display: grid;
    grid-template-columns: 1fr;
  }

  .quote-delivery-distance-field,
  .quote-delivery-price-field {
    width: 100%;
  }

  .quote-delivery-editor-actions {
    align-items: stretch;
    flex-direction: column;
  }

  .quote-delivery-editor-actions .primary-action {
    margin-left: 0;
    order: 1;
    width: 100%;
  }

  .quote-delivery-delete-editor {
    order: 2;
  }

  .quote-delivery-delete-editor,
  .quote-delivery-delete-editor .secondary-button {
    width: 100%;
  }

  .table-panel {
    display: none;
  }

  .mobile-lead-list {
    display: grid;
    gap: var(--space-3);
  }

  .muted,
  dt,
  .work-list small,
  .comment-list time {
    font-size: 13px;
  }

  .summary-grid {
    grid-template-columns: 1fr;
  }

  .workspace-grid {
    display: grid;
  }

  .workspace-column,
  .workspace-secondary {
    display: contents;
  }

  .tasks-panel {
    order: 1;
  }

  .comments-panel {
    order: 2;
  }

  .add-comment-panel {
    order: 3;
  }

  .add-task-panel {
    order: 4;
  }

  .request-panel {
    order: 5;
  }

  .contact-details-panel {
    order: 6;
  }

  .activity-panel {
    order: 7;
  }

  .handoff-panel {
    order: 8;
  }

  .work-list li {
    align-items: flex-start;
    flex-direction: column;
  }

  .quick-edit {
    display: block;
    margin: var(--space-2) 0 0;
  }

  .quick-edit form {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 560px) {
  .quote-totals-strip,
  .quote-price-breakdown {
    grid-template-columns: 1fr;
  }
}

/* ── Kanban ──────────────────────────────────────────────── */

/* Чем отсортирована доска — словами над ней. Владелец 15.08.2026: «как понять
   порядок лидов?». Порядок, о котором надо догадываться, читается как
   случайный, а случайному порядку не доверяют. */
.kanban-order {
  margin: calc(var(--space-2) * -1) 0 var(--space-3);
}

.kanban-board {
  display: flex;
  gap: var(--space-4);
  overflow-x: auto;
  overflow-y: visible;
  align-items: flex-start;
  padding-bottom: var(--space-4);
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}

/* Колонка делит ширину долями (F-108): при 4 стадиях растягивается и
   заполняет экран, при 8 — прокрутка, но внутри доски (у неё overflow-x).
   flex-shrink: 0 — колонка не сжимается уже 280px: сжатие рвёт подписи
   и выталкивает кнопки из карточек, честнее прокрутка доски.
   Индикатор стадии — тонкая полоса над заголовком (слева у карточек —
   запрещённый маркёр), цвет ставит класс .stage-tone--* на колонке. */
.kanban-column {
  background: var(--color-page);
  border: 1px solid var(--color-border);
  border-top: 3px solid var(--stage-color, var(--color-border-strong));
  border-radius: var(--radius-md);
  flex: 1 0 280px;
  padding: var(--space-3);
}

.kanban-column-head {
  margin-bottom: var(--space-3);
}

.kanban-column-title {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  justify-content: space-between;
  margin: 0;
  color: var(--color-text);
}

.kanban-column-count {
  background: var(--color-surface-sunken);
  border-radius: var(--radius-full);
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  padding: 4px 8px;
}

.kanban-column-sum {
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  margin: var(--space-05) 0 0;
}

/* Столбик — цель для drop: пустой не схлопывается */
.kanban-cards {
  min-height: 72px;
}

/* Карточка сделки — компонент cotton/deal_card.html */
.deal-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  margin-bottom: var(--space-3);
  padding: var(--space-3);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.deal-card:hover {
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-md);
}

/* Заголовок карточки: знак канала + название слева, дата создания справа —
   приём amoCRM («Сделки»: дата в углу карточки, отдельно от суммы и тегов).
   align-items: flex-start — если название переносится на вторую строку, дата
   не съезжает вниз вместе с ним. */
.deal-card-top {
  align-items: flex-start;
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
  margin-bottom: var(--space-1);
}

/* Знак канала и название в одну группу, не `space-between` друг с другом: знак
   принадлежит названию и стоит вплотную к нему, как в строке очереди инбокса,
   — разнесённые по краям, они читались бы как два разных поля. */
.deal-card-title-group {
  align-items: center;
  display: flex;
  gap: var(--space-1);
  min-width: 0;
}

.deal-card-channel { flex-shrink: 0; }

/* Дата создания лида — «давно ли он у нас», отдельный вопрос от строки
   ожидания/движения ниже. Тот же приглушённый тон, что у остальной меты
   карточки: не конкурирует с названием и суммой. */
.deal-card-created {
  color: var(--color-muted);
  flex-shrink: 0;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Строка времени. Красный — только когда клиент ждёт дольше суток; это тот же
   порог и тот же цвет, что в очереди инбокса. Заливку карточки за простой
   («rotting» у Pipedrive) мы намеренно не берём: цветовой бюджет проекта —
   три состояния плюс пустота, и второй красный язык поверх первого превращает
   доску в светофор (docs/reference/crm-ux-patterns.md). */
.deal-card-when {
  align-items: center;
  color: var(--color-muted);
  display: flex;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  gap: var(--space-05);
  margin: 0 0 var(--space-2);
}

.deal-card-when--overdue {
  color: var(--color-danger-text);
  font-weight: var(--weight-medium);
}

.deal-card-title {
  color: var(--color-text);
  display: block;
  font-weight: var(--weight-semibold);
}

.deal-card-title:hover {
  color: var(--color-primary);
  text-decoration: none;
}

.deal-card-meta {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: 0 0 var(--space-2);
}

.deal-card-move {
  border-top: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  align-items: center;
}

.deal-card-move select {
  flex: 1;
  font-size: var(--text-xs);
  min-height: 30px;
  padding: 4px 6px;
}

.deal-card-move button {
  font-size: var(--text-xs);
  min-height: 30px;
  padding: 4px 10px;
  white-space: nowrap;
}

.kanban-empty {
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-faint);
  font-size: var(--text-xs);
  margin: 0;
  padding: var(--space-3);
  text-align: center;
}

/* Подсказка «перетащите сюда» видна только в пустой колонке */
.kanban-cards:has(.deal-card) .kanban-empty {
  display: none;
}

/* Карточка — перетаскиваемая: курсор и состояния SortableJS */
.deal-card {
  cursor: grab;
  touch-action: manipulation;
}

.deal-card-chosen {
  cursor: grabbing;
}

.deal-card-ghost {
  border-style: dashed;
  opacity: 0.45;
}

/* Пока POST переноса летит, карточка приглушена и не принимает клики */
.deal-card.htmx-request {
  opacity: 0.55;
  pointer-events: none;
}

.deal-card-amount {
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  margin: 0 0 var(--space-1);
}

.kanban-more {
  background: none;
  border: none;
  color: var(--color-primary);
  cursor: pointer;
  display: block;
  font-size: var(--text-xs);
  padding: var(--space-2);
  text-align: center;
  width: 100%;
}

.kanban-more:hover {
  text-decoration: underline;
}

.kanban-toast {
  background: var(--color-danger-solid);
  border-radius: var(--radius-md);
  bottom: var(--space-4);
  box-shadow: var(--shadow-md);
  color: var(--color-on-primary);
  font-size: var(--text-sm);
  left: 50%;
  margin: 0;
  max-width: min(90vw, 420px);
  padding: var(--space-2) var(--space-4);
  position: fixed;
  transform: translateX(-50%);
  z-index: var(--z-modal);
}

/* 375: доска деградирует в вертикальный список стадий */
@media (max-width: 640px) {
  .kanban-board {
    cursor: auto;
    flex-direction: column;
    overflow-x: visible;
  }

  .kanban-column {
    flex: 1 1 auto;
    width: 100%;
  }

  /* Пустые стадии в списке не занимают экран подсказками */
  .kanban-cards:not(:has(.deal-card)) .kanban-empty {
    padding: var(--space-2);
  }
}

/* ── Orders ─────────────────────────────────────────────── */
.order-price {
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--color-text);
}

/* ── Авито блок в карточке лида ─────────────────────────── */
.source-badge {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  padding: 2px 6px;
  border-radius: var(--radius-sm, 4px);
  text-transform: uppercase;
  vertical-align: middle;
  margin-right: var(--space-1);
}

.source-avito {
  background: var(--color-avito-bg);
  color: var(--color-avito-text);
  border: 1px solid var(--color-avito-border);
}

.avito-item-image {
  margin-bottom: var(--space-3);
  border-radius: var(--radius-md, 6px);
  overflow: hidden;
  max-height: 160px;
}

.avito-item-image img {
  width: 100%;
  height: 160px;
  object-fit: cover;
  display: block;
}

.avito-buyer-row {
  display: flex;
  align-items: center;
  gap: 6px;
}


/* ── Inbox — переписка ──────────────────────────────────── */
.inbox-panel { padding: var(--space-4); }

.inbox-channel-badge {
  font-size: 0.7rem;
  font-weight: 600;
  background: var(--color-avito-bg);
  color: var(--color-avito-text);
  border: 1px solid var(--color-avito-border);
  padding: 2px 6px;
  border-radius: 4px;
  margin-left: 6px;
  vertical-align: middle;
}

.inbox-messages {
  height: 320px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  margin: var(--space-2) 0;
}

.inbox-loading, .inbox-empty {
  text-align: center;
  color: var(--color-muted);
  font-size: 0.875rem;
  padding: var(--space-4);
  margin: auto;
}

.inbox-message { display: flex; }

.inbox-message-inner {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  max-width: 80%;
}

.inbox-message--in { justify-content: flex-start; }
.inbox-message--out { justify-content: flex-end; margin-left: auto; }
.inbox-message--out .inbox-message-inner { flex-direction: row-reverse; }

.inbox-bubble {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 12px;
  padding: 8px 12px;
  min-width: 60px;
}

.inbox-message--out .inbox-bubble {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
}

.inbox-meta {
  display: flex;
  gap: 6px;
  align-items: baseline;
  margin-bottom: 2px;
}

.inbox-author {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-muted);
}

.inbox-message--out .inbox-author,
.inbox-message--out .inbox-time { color: color-mix(in srgb, var(--color-on-primary) 70%, transparent); }

.inbox-time {
  font-size: 0.7rem;
  color: var(--color-muted);
}

.inbox-text {
  font-size: 0.875rem;
  line-height: 1.4;
  word-break: break-word;
}

/* `flex-wrap` — чтобы переключатель канала занял свою строку над полем, а поле
   с кнопкой остались в одном ряду под ним. Без переноса ряд один, и третий
   элемент сплющивает поле ввода. */
.inbox-form {
  align-items: flex-end;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.inbox-input {
  flex: 1;
  resize: vertical;
  min-height: 40px;
  padding: 8px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md, 6px);
  font-size: 0.875rem;
  font-family: inherit;
}

.inbox-input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}

.inbox-send {
  white-space: nowrap;
  flex-shrink: 0;
}

/* ── Карточка сделки ───────────────────────────────────────
   Рабочее место менеджера, а не витрина. Три зоны и ни одной вложенной
   карточки: слева «кто клиент», в центре переписка (главное содержимое
   дня), справа «что с этим делать». Панели плоские — их отделяет
   граница; тень означала бы, что панель висит над страницей.
   ──────────────────────────────────────────────────────── */

/* Шапка страницы — сам объект. Название сделки живёт здесь и только здесь. */
.topbar-head {
  min-width: 0;
}

.back-link {
  align-items: center;
  color: var(--color-muted);
  display: inline-flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  margin-bottom: var(--space-1);
}

/* «Все лиды» — самостоятельная ссылка возврата, а не слово внутри фразы,
   поэтому на пальце ей нужна высота: замер на 375 px давал 18 px. Отступ
   компенсирован отрицательным полем слева, чтобы она осталась на одной
   вертикали с заголовком под ней. */
@media (hover: none) {
  .back-link {
    margin-left: calc(var(--space-2) * -1);
    min-height: 40px;
    padding: 0 var(--space-2);
  }
}

.back-link:hover {
  color: var(--color-primary);
}

.deal-title {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  margin: 0;
}

/* Заголовок редактируется по клику — но выглядит заголовком, а не полем:
   рамка появляется на наведении, чтобы не спорить с текстом. */
h1.deal-title.hx-value {
  border: 1px solid transparent;
  border-radius: var(--radius-input);
  cursor: text;
  display: block;
  margin-left: -6px;
  padding: 0 6px;
}

h1.deal-title.hx-value:hover {
  background: var(--state-hover);
  border-color: var(--color-border);
}

.deal-subtitle {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin: var(--space-1) 0 0;
}

/* ── Полоса управления ────────────────────────────────────
   Рельс воронки занимает свою строку целиком: делить её с кнопками
   значит обрезать текущую стадию — а именно её менеджер и ищет глазами.
   Ниже — строка «что делать»: следующий шаг слева, действия справа. */
.deal-bar {
  display: flex;
  margin-bottom: var(--space-2);
}

.deal-bar-stage {
  max-width: 100%;
  min-width: 0;
}

.deal-bar-row {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: space-between;
  margin-bottom: var(--space-4);
}

.deal-bar-stage form {
  margin: 0;
}

.deal-bar-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ── Рельс воронки ────────────────────────────────────────
   Главный орган управления: весь путь сделки виден целиком, перевод —
   один клик. Сегменты в общей дорожке, разделены волосяной линией;
   текущий залит цветом своей стадии, пройденные — приглушены. */
.stage-rail {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  display: flex;
  max-width: 100%;
  overflow-x: auto;
  padding: 3px;
  position: relative;
  scrollbar-width: thin;
}

/* F-215: край растушёвывается только тогда, когда за ним действительно есть
   стадии — класс ставит скрипт карточки. Растушёвка, а не стрелка: она не
   отнимает ширину у самих стадий, которой здесь и не хватает. */
.stage-rail.has-more-end {
  mask-image: linear-gradient(to left, transparent 0, #000 32px);
}

.stage-rail.has-more-start {
  mask-image: linear-gradient(to right, transparent 0, #000 32px);
}

.stage-rail.has-more-start.has-more-end {
  mask-image: linear-gradient(to right, transparent 0, #000 32px, #000 calc(100% - 32px), transparent 100%);
}

.stage-rail-step {
  align-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--radius-pill);
  color: var(--color-muted);
  cursor: pointer;
  display: inline-flex;
  flex: 0 0 auto;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  gap: var(--space-2);
  min-height: 30px;
  padding: 0 var(--space-3);
  transition: background var(--transition-fast), color var(--transition-fast);
  white-space: nowrap;
}

.stage-rail-step .stage-dot {
  background: var(--stage-color, var(--color-faint));
  flex: 0 0 auto;
}

.stage-rail-step:hover {
  background: var(--state-hover);
  border: 0;
  color: var(--color-text);
}

/* Пройденные стадии — тише будущих: взгляд идёт вперёд, а не назад. */
.stage-rail-step.is-done {
  color: var(--color-faint);
}

.stage-rail-step.is-done .stage-dot {
  opacity: 0.55;
}

.stage-rail-step.is-current {
  background: var(--stage-color, var(--color-primary));
  color: var(--color-on-primary);
  cursor: default;
  font-weight: var(--weight-semibold);
}

.stage-rail-step.is-current .stage-dot {
  background: var(--color-on-primary);
}

/* ── Следующий шаг ────────────────────────────────────────
   Ответ на вопрос «что с этим делать» — строкой, а не иконкой в углу. */
.deal-next {
  align-items: center;
  color: var(--color-text);
  display: flex;
  flex: 1 1 320px;
  font-size: var(--text-sm);
  gap: var(--space-2);
  margin: 0;
  min-width: 0;
}

.deal-next .icon {
  color: var(--color-primary);
  flex: 0 0 auto;
}

/* «Следующий шаг» — подпись, а не содержимое: она не переносится. На 375 px
   строка ломалась по подписи, и «Следующий» стояло над «шаг», хотя переносить
   надо было текст задачи справа. */
.deal-next-label {
  color: var(--color-muted);
  white-space: nowrap;
}

.deal-next-text {
  font-weight: var(--weight-semibold);
}

.deal-next-text--empty {
  color: var(--color-muted);
  font-weight: var(--weight-normal);
}

/* ── Сетка карточки ───────────────────────────────────────
   Колонки долями (сетка F-108): на широком мониторе растут вместе с
   окном, пустой середины нет. Высота — экран минус шапка, чтобы лента
   и композер были видны без прокрутки страницы. */
.deal-grid {
  align-items: stretch;
  display: grid;
  gap: var(--space-3);
  grid-template-columns: minmax(var(--pane-min), 1fr) minmax(0, 2fr) minmax(var(--pane-min), 1fr);
  height: calc(100vh - 236px);
  height: calc(100dvh - 236px);
  min-height: 520px;
}

.deal-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 0;
  min-width: 0;
}

.deal-col--about,
.deal-col--work {
  overflow-y: auto;
  scrollbar-color: var(--color-border) transparent;
  scrollbar-width: thin;
}

/* Центр — одна панель на всю высоту: лента прокручивается внутри себя. */
.deal-col--feed {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-flat);
  gap: 0;
  overflow: hidden;
}

/* ── Панель ───────────────────────────────────────────────
   Компонент cotton/panel.html. Плоская, граница вместо тени, скругление
   панели мягче скругления контролов внутри неё. */
.panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-flat);
}

/* 32 px, а не 44 (F-212): заголовок панели — подпись в одно слово капителью, а
   двенадцать лишних пикселей на каждой панели стоили строки данных на каждом
   экране. Кнопки внутри заголовка ниже не стали — им хватает своей высоты. */
.panel-head {
  align-items: center;
  border-bottom: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
  min-height: 32px;
  padding: var(--space-1) var(--space-4);
}

.panel-title {
  align-items: center;
  color: var(--color-text);
  display: flex;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  gap: var(--space-2);
  letter-spacing: var(--label-tracking);
  margin: 0;
  text-transform: var(--label-transform);
}

.panel-title .icon {
  color: var(--color-faint);
}

.panel-head-actions {
  display: flex;
  gap: var(--space-2);
}

.panel-body {
  padding: var(--space-3);
}

.panel-head--feed {
  background: var(--color-surface);
  flex: 0 0 auto;
  /* Подпись «разговор продолжился в …» (F-145) должна называть канал целиком:
     обрезанное «в Те…» отвечает ровно на тот вопрос, ради которого подпись и
     появилась. Не влезла в строку — уходит на свою, а не под многоточие. */
  flex-wrap: wrap;
}

.panel--tabs .panel-body {
  padding: 0;
}

/* Рубрика раздела вне панели: та же типографика, что у .panel-title,
   чтобы заголовок секции и заголовок панели читались как один уровень. */
.section-title {
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-2);
}

/* ── Поле «подпись → значение» ────────────────────────────
   Компонент cotton/field.html. Подпись тихая, значение читаемое.
   Поля идут строками с разделителем — это список данных, а не сетка
   карточек. */
.field + .field {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
}

.field-label {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  margin-bottom: 1px;
  text-transform: var(--label-transform);
}

.field-value {
  align-items: center;
  color: var(--color-text);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-base);
  gap: var(--space-2);
  min-height: 24px;
}

/* Поле с кружком собеседника: значение течёт обычным текстом, кружок обтекается.
   Флекс здесь не годится — имя «Ольга Вячеславовна Перепёлкина-Стародубцева» не
   влезает в колонку 250 px и целиком уезжает под кружок, вместо того чтобы
   переноситься рядом с ним. */
.field--person .field-value {
  display: block;
}

.field--person .field-avatar {
  float: left;
  margin-right: var(--space-2);
}

.field-value--mono,
.mono {
  font-family: var(--font-mono);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
}

.field-inline-form {
  display: flex;
  gap: var(--space-2);
  margin: 0;
  width: 100%;
}

.field-inline-form select {
  flex: 1 1 auto;
  min-width: 0;
}

/* ── Раскрывашка ──────────────────────────────────────────
   Редкое прячется, но не исчезает. Стрелка поворачивается — состояние
   видно без чтения текста. */
.disclosure {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
}

.disclosure-summary {
  align-items: center;
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  gap: var(--space-2);
  list-style: none;
  min-height: 32px;
  padding: 0 var(--space-4);
}

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

.disclosure-summary .icon {
  color: var(--color-faint);
  transition: transform var(--transition-fast);
}

.disclosure[open] > .disclosure-summary .icon {
  transform: rotate(90deg);
}

.disclosure-body {
  border-top: 1px solid var(--color-border);
  padding: var(--space-3) var(--space-4);
}

.disclosure--inline {
  background: transparent;
  border: 0;
  border-top: 1px solid var(--color-border);
  border-radius: 0;
  margin-top: var(--space-3);
}

.disclosure--inline .disclosure-summary {
  padding: 0;
}

/* ── Списки внутри панели ─────────────────────────────────
   Задачи, комментарии и история — строки списка с разделителем, а не
   рамка внутри рамки: карточка в карточке — брак из списка заказчика.
   Строка переносится, поэтому переживает узкую колонку в 260px. */
.panel .work-list,
.panel .comment-list,
.panel .activity-list {
  gap: 0;
  margin: 0;
}

.panel .work-list li,
.panel .comment-list li,
.panel .activity-list li {
  border: 0;
  border-radius: 0;
  padding: var(--space-2) 0;
}

/* Строка задачи — грид с явными рядами, а не перенос флексом: название
   занимает ряд целиком, статус и «Закрыть» встают под ним. На флексе
   статус оставался на одной строке с названием и срезался краем панели
   в колонке 260px. */
.panel .work-list li {
  align-items: center;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: 1fr auto;
}

.panel .work-list li > span:first-child {
  grid-column: 1 / -1;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Пилюля статуса — по содержимому, а не во всю ячейку грида. */
.panel .work-list li > .status-pill {
  justify-self: start;
}

.panel .work-list li form {
  margin: 0;
}

.panel .work-list li + li,
.panel .comment-list li + li,
.panel .activity-list li + li {
  border-top: 1px solid var(--color-border);
}

.panel .work-list li:first-child,
.panel .comment-list li:first-child {
  padding-top: 0;
}

/* ── Пустые состояния ─────────────────────────────────────
   Строка текста, объясняющая следующий шаг. Не центрированный экран с
   большой картинкой: в рабочем инструменте это трата места. */
.empty-hint {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin: 0;
}

/* ── Расчёты и заказы в правой колонке ───────────────────── */
.quote-list,
.order-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.quote-row {
  display: grid;
  gap: 0 var(--space-2);
  grid-template-columns: 1fr auto;
  padding: var(--space-2) 0;
}

.quote-row + .quote-row {
  border-top: 1px solid var(--color-border);
}

.quote-row:first-child {
  padding-top: 0;
}

.quote-row-title {
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  min-width: 0;
  overflow-wrap: anywhere;
}

.quote-row-total {
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.quote-row-meta {
  color: var(--color-muted);
  font-size: var(--text-xs);
  grid-column: 1 / -1;
}

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

.order-row + .order-row {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
}

/* Фото объявления Авито — компактное */
.panel .avito-item-image {
  border-radius: var(--radius-input);
  margin: 0 0 var(--space-3);
  overflow: hidden;
}

.panel .avito-item-image img {
  display: block;
  height: 140px;
  object-fit: cover;
  width: 100%;
}

/* Зона inbox — занимает всё свободное место */
.lead-inbox-area {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border);
  min-height: 0;
}

.lead-inbox-area .inbox-messages {
  flex: 1;
  min-height: 300px;
  max-height: 500px;
  overflow-y: auto;
}

/* Кнопка отправки */
.btn-primary {
  background: var(--color-primary);
  border: 1px solid var(--color-primary);
  color: var(--color-on-primary);
  border-radius: var(--radius-md);
  font-weight: 650;
  padding: 7px 14px;
  min-height: 36px;
  cursor: pointer;
  white-space: nowrap;
}

.btn-primary:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  color: var(--color-on-primary);
}

/* Вкладки */
.lead-tabs-bar {
  display: flex;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-page);
  flex-shrink: 0;
}

.lead-tab-btn {
  flex: 1;
  padding: var(--space-3) var(--space-4);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

.lead-tab-btn:hover {
  color: var(--color-text);
  background: var(--color-neutral-bg);
}

.lead-tab-btn.active {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
  background: var(--color-surface);
}

.lead-tab-content {
  padding: var(--space-4);
  display: none;
  flex-shrink: 0;
}

.lead-tab-content.active {
  display: block;
}

/* Планшет: «кто клиент» и переписка рядом, действия — широкой полосой под
   ними. Высота перестаёт быть экранной: страница прокручивается целиком,
   иначе на 768 в каждой колонке появляется свой скролл. */
@media (max-width: 1200px) {
  .deal-grid {
    grid-template-columns: minmax(var(--pane-min), 1fr) minmax(0, 2.2fr);
    grid-template-rows: auto auto;
    height: auto;
    min-height: 0;
  }
  .deal-col--about,
  .deal-col--work {
    overflow-y: visible;
  }
  .deal-col--work {
    grid-column: 1 / -1;
  }
  .deal-col--feed {
    min-height: 460px;
  }
}

/* Мобильный: одна колонка, переписка первой — менеджер открывает карточку
   ради неё. Действия и данные клиента ниже. */
@media (max-width: 768px) {
  .deal-grid {
    grid-template-columns: 1fr;
    height: auto;
    min-height: 0;
  }
  .deal-col--feed {
    min-height: 420px;
    order: -1;
  }
  .deal-col--about,
  .deal-col--work {
    grid-column: 1;
    overflow-y: visible;
  }
  .deal-bar-actions {
    width: 100%;
  }
  .deal-bar-actions > * {
    flex: 1 1 auto;
  }
}

/* Quick-edit — поля с hover-карандашом */
.qe-field {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.qe-value {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 24px;
}

.qe-pencil {
  opacity: 0;
  transition: opacity var(--transition-fast);
  background: none;
  border: none;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  color: var(--color-muted);
  font-size: var(--text-xs);
  flex-shrink: 0;
  line-height: 1;
}

.qe-field:hover .qe-pencil {
  opacity: 1;
}

.qe-pencil:hover {
  background: var(--color-neutral-bg);
  color: var(--color-primary);
}

.qe-form {
  display: none;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
  flex-wrap: wrap;
}

.qe-form.open {
  display: flex;
}

.qe-form input,
.qe-form select,
.qe-form textarea {
  flex: 1;
  min-width: 0;
  padding: 5px 8px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-family: inherit;
}

.qe-form input:focus,
.qe-form select:focus,
.qe-form textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: var(--focus-ring);
}

.qe-save {
  background: var(--color-primary);
  color: var(--color-on-primary);
  border: none;
  border-radius: var(--radius-sm);
  padding: 5px 10px;
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.qe-save:hover {
  background: var(--color-primary-hover);
}

.qe-cancel {
  background: none;
  border: none;
  color: var(--color-muted);
  font-size: var(--text-xs);
  cursor: pointer;
  padding: 5px 6px;
}

.qe-cancel:hover {
  color: var(--color-text);
}

/* Убрать старые details/summary стили quick-edit в левой колонке */
.lead-col-left .quick-edit {
  display: none;
}

/* sticky поле ввода inbox */
.lead-inbox-area .inbox-form {
  position: sticky;
  bottom: 0;
  background: var(--color-surface);
  padding-top: var(--space-2);
  margin-top: var(--space-2);
  border-top: 1px solid var(--color-border);
}

.header-stage-form {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.header-stage-select {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 4px 8px;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  appearance: auto;
}

/* Цвет select по статусу — как у status-badge */
.status-select-new {
  background: var(--color-neutral-bg);
  color: var(--color-neutral-text);
  border-color: var(--color-neutral-text);
}

.status-select-in-work,
.status-select-calculation {
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  border-color: var(--color-warning-text);
}

.status-select-order {
  background: var(--color-success-bg);
  color: var(--color-success-text);
  border-color: var(--color-success-text);
}

.status-select-closed {
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
  border-color: var(--color-danger-text);
}

/* HX inline edit fields */
.hx-field { margin-bottom: var(--space-3); }
.hx-label {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: var(--label-transform);
  margin-bottom: 2px;
}
.hx-value { cursor: pointer; padding: 3px 6px; border-radius: var(--radius-sm); border: 1px solid transparent; transition: border-color var(--transition-fast), background var(--transition-fast); min-height: 28px; display: flex; align-items: center; font-size: var(--text-sm); }
.hx-value:hover { border-color: var(--color-border); background: var(--color-neutral-bg); }
.hx-value-static { padding: 3px 0; font-size: var(--text-sm); min-height: 28px; display: flex; align-items: center; gap: var(--space-2); }
.hx-input { width: 100%; padding: 4px 8px; border: 1px solid var(--color-primary); border-radius: var(--radius-sm); font-size: var(--text-sm); font-family: inherit; box-shadow: var(--focus-ring); background: var(--color-surface); }
.hx-input:focus { outline: none; }
.hx-interest { font-size: var(--text-sm); color: var(--color-muted); line-height: 1.5; margin: 0 0 var(--space-2); font-style: italic; }

/* ═══════════════════════════════════════════════════════
   TIMELINE — единая лента событий карточки лида
   ═══════════════════════════════════════════════════════ */

/* Обёртка ленты: кнопка «вниз» позиционируется от неё, а не от самой ленты —
   лента перерисовывается опросом, и всё, что внутри неё, при этом исчезает. */
.tl-feed {
  display: flex;
  flex: 1;
  min-height: 0;
  position: relative;
}

.tl-jump {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  bottom: var(--space-3);
  box-shadow: var(--elevation-overlay);
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  height: 32px;
  justify-content: center;
  position: absolute;
  right: var(--space-4);
  width: 32px;
}

/* `display: flex` перебивает браузерное `[hidden] { display: none }` — без этой
   строки кнопка «вниз» видна всегда, даже когда лента уже внизу. */
.tl-jump[hidden] { display: none; }

.tl-jump:hover { background: var(--color-surface-sunken); }
.tl-jump:focus-visible { box-shadow: var(--focus-ring); outline: none; }

/* Разделитель непрочитанных: лента открывается на нём (Telegram). Линия во всю
   ширину с подписью посередине — не значок и не цветная плашка: он должен
   читаться как граница, а не как ещё одно сообщение. */
.tl-unread-sep {
  align-items: center;
  color: var(--color-primary);
  display: flex;
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  gap: var(--space-2);
  margin: var(--space-2) 0;
}

.tl-unread-sep::before,
.tl-unread-sep::after {
  background: color-mix(in srgb, var(--color-primary) 35%, transparent);
  content: "";
  flex: 1;
  height: 1px;
}

/* Неотправленное сообщение. Остаётся в переписке с причиной и повтором: до
   09.08.2026 отказ показывался alert() и исчезал бесследно — ответ клиенту мог
   пропасть молча. Урок Front: ошибка обязана называть, что именно не ушло. */
.chat-failed:empty { display: none; }

.chat-fail {
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border);
  border-radius: var(--radius-input);
  display: grid;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
  padding: var(--space-2) var(--space-3);
}

.chat-fail-why {
  align-items: center;
  color: var(--color-danger-text);
  display: flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  margin: 0;
}

.chat-fail-text {
  color: var(--color-text);
  font-size: var(--text-sm);
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
}

.chat-fail-acts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

.chat-fail-acts .ghost-button { font-size: var(--text-xs); padding: var(--space-1) var(--space-2); }

.timeline-area {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Сверху отступа нет: его даёт панель фильтра `.tl-controls`, которая липнет
     к верху. С паддингом контейнера она прилипала бы ниже его края, и в щель
     над кнопками просвечивала полоска сообщения. */
  padding: 0 var(--space-4) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* ── Сообщение переписки ── */
.tl-message { display: flex; }

.tl-message-inner {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  max-width: 85%;
}

.tl-message--in .tl-message-inner { margin-right: auto; }
.tl-message--out .tl-message-inner { margin-left: auto; }

.tl-bubble {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  max-width: 100%;
}

.tl-message--in .tl-bubble {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.tl-message--out .tl-bubble {
  background: var(--color-primary);
  color: var(--color-on-primary);
}

/* F-184: сигнал площадки («посмотрел номер», «ознакомился с предложением») —
   не реплика человека, поэтому не цветной пузырь ни на входящей, ни на
   исходящей стороне. Текст — обычным цветом (AA к --color-surface-sunken):
   приглушённый --color-muted держит контраст ~4.43:1, ниже порога 4.5:1 для
   текста 13px — отличие от реплики человека даёт рамка и иконка, а не блеклый
   текст сигнала, который как раз обязан быть читаемым. */
.tl-message--in .tl-bubble--system,
.tl-message--out .tl-bubble--system {
  background: var(--color-surface-sunken);
  border: 1px dashed var(--color-border);
  color: var(--color-text);
}

.tl-system-text {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
}

.tl-system-text .icon { flex-shrink: 0; margin-top: 2px; color: var(--color-muted); }

/* F-064: статус доставки нашего ответа. Пузырь исходящего залит
   --color-primary, поэтому цветного текста внутри него быть не может: подпись
   идёт currentColor. 🔴 Без приглушения, в отличие от соседнего .tl-time: белый
   на --color-primary даёт 4,88:1, а тот же белый под opacity 0.75 — 3,48:1,
   ниже порога AA для 12px. Время можно не дочитать, «отправлено» нельзя. */
.tl-delivery {
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--space-1);
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
}

/* К первой строке, а не по центру: на 375px подпись отказа переносится на две
   строки, и центрированная иконка съезжает в межстрочье. Тот же приём, что у
   .tl-system-text. */
.tl-delivery .icon { flex-shrink: 0; margin-top: 2px; }

/* Отказ перекрашивает пузырь целиком, а не дописывает красную строчку к пузырю,
   выглядящему доставленным: не дошедшее сообщение не должно читаться дошедшим.
   #9e3323 на #f9e7e4 — 6,6:1, пара из тех же токенов, что и остальные отказы. */
.tl-message--out .tl-bubble--failed {
  background: var(--color-danger-bg);
  border: 1px solid var(--color-danger-border);
  color: var(--color-danger-text);
}

.tl-bubble--failed .tl-delivery { font-weight: 500; }

/* 🔴 Приглушение (`opacity`) висит на подписях, а не на всей строке `.tl-meta`.
   Внутри строки стоит знак канала, а прозрачность родителя ребёнку не отменить:
   при `opacity: .7` на `.tl-meta` чёрный знак Telegram на синем исходящем пузыре
   падал до ~2.6:1 — ниже порога 3:1 для нетекстовых элементов (WCAG 1.4.11).
   Знак канала обязан оставаться в полную силу: это опознание канала, а не
   служебная подпись. */
/* Шапка пузыря: имя, знак канала, время.

   🔴 `flex-wrap` обязателен, и это не украшение. Имена клиентов в мессенджерах
   бывают длинные («Ольга Вячеславовна Перепёлкина-Стародубцева»), а строка не
   переносилась: имя занимало три строки, время рядом сжималось до своей ширины
   и ломалось пополам — «22.08» над «11:33». Замерено на 375 px, воспроизводится
   и на 1280 в колонке карточки. Теперь имя берёт ширину и переносится, а знак
   со временем уезжают на свою строку целиком. */
.tl-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin-bottom: var(--space-1);
  font-size: var(--text-xs);
}

.tl-message--out .tl-meta { justify-content: flex-end; }

/* Имя жмётся первым: оно единственное здесь переносимое. `anywhere` — потому
   что бывает одно слово в 30 знаков без пробелов и дефисов. */
.tl-author {
  font-weight: 600;
  min-width: 0;
  opacity: 0.7;
  overflow-wrap: anywhere;
}

/* Знак канала в пузыре: чуть мельче иконок интерфейса — он опознаёт источник,
   а не зовёт нажать. Размер задан здесь, а не в `.ch-mark`, чтобы плотная лента
   не диктовала размер вкладкам и кнопкам. */
.ch-mark.tl-channel { height: 14px; width: 14px; }

/* Время не рвётся никогда: «22.08 11:33» пополам — это не перенос, а поломка. */
.tl-time { color: inherit; opacity: 0.6; white-space: nowrap; }

.tl-text { font-size: var(--text-sm); line-height: 1.45; word-break: break-word; }

/* Кружок в ленте — общий `.avatar`; здесь только его место в строке.
   Он прижат к низу пузыря, а не к середине: у длинного сообщения середина
   уезжает на три строки вниз, и кружок повисает в стороне от имени, к
   которому относится. */
.tl-avatar { align-self: flex-end; }

/* ── Заметка менеджера ── */
/* Внутренняя заметка — не предупреждение. Жёлтый статусный цвет делал её
   самым громким элементом ленты, хотя это самая тихая её часть: отличать
   заметку от сообщения клиента достаточно фоном и подписью «Заметка». */
.tl-comment {
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  padding: var(--space-2) var(--space-3);
}

.tl-comment-inner {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
}

.tl-comment-icon { font-size: 14px; margin-top: 2px; flex-shrink: 0; }

.tl-comment-body { flex: 1; min-width: 0; }

.tl-label {
  opacity: 0.7;
  background: var(--color-neutral-bg);
  border-radius: var(--radius-input);
  color: var(--color-neutral-text);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  padding: 1px 5px;
}

/* ── Системное событие ── */
.tl-event {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--color-muted);
  padding: 2px 0;
}

/* F-206: у события в ленте — иконка типа, а не серая точка. Точка была одна на
   восемнадцать действий: «Заказ оформлен» и «Поле обновлено» выглядели
   одинаково, и различать их приходилось чтением.

   Цвета у иконки нет — только `--color-faint`, тот же приглушённый тон, что у
   самой строки. Цвет в этом проекте принадлежит состоянию, и раздать его типам
   значило бы отнять его у просрочки (`docs/reference/crm-ux-patterns.md`). */
.tl-event-icon {
  /* `--color-muted`, а не `--color-faint`: значок здесь несёт смысл (по нему
     тип события и опознаётся), значит подчиняется 3:1 по WCAG 1.4.11.
     `--color-faint` (#94a3b8) даёт к белому 2.56:1 — не проходит; `--color-muted`
     (#64748b) даёт 4.76:1 и совпадает по тону с подписью, отчего строка
     читается одним куском, а не значком плюс текстом. */
  color: var(--color-muted);
  display: inline-flex;
  flex-shrink: 0;
}

.tl-event-label { color: var(--color-text-secondary, var(--color-muted)); }
.tl-event-actor { font-weight: 500; }

/* ── Пустое состояние ── */
.tl-empty {
  text-align: center;
  color: var(--color-muted);
  padding: var(--space-6);
  font-size: var(--text-sm);
}

/* ── Вложения в сообщениях (F-077, F-083) ──
   Цвета от currentColor, а не токеном напрямую: один и тот же чип живёт и в
   белом входящем пузыре, и в синем исходящем. */
.tl-photo {
  display: block;
  border-radius: var(--radius-sm);
}

.tl-photo img {
  display: block;
  max-width: min(320px, 100%);
  max-height: 280px;
  border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
  border-radius: var(--radius-sm);
}

.tl-photo:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Плеер голосового в пузыре: свой вид на родном `<audio>` (F-142, F-247).

   🔴 Замеры 23.08.2026, почему не родной и почему не библиотека — в разметке
   (`_timeline_item.html`) и в `docs/reference/inbox-ux.md`. Коротко: у родного
   скорость в меню «три точки», а полоса перемотки не расширяется в принципе;
   у Plyr скорость за шестерёнкой — те же три нажатия, и 151 536 байт впридачу.

   Волну как в WhatsApp не рисуем — решение владельца; полоса перемотки не волна.

   Всё строится на `currentColor`, как соседний `.tl-doc`: голосовое приходит и
   во входящем пузыре (тёмный текст на белом), и в исходящем (белый на синем).
   Свои цвета здесь развалили бы вторую половину случаев. */
.tl-voice {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  margin: var(--space-1) 0;
  /* 🔴 Ширина в пикселях, а не в процентах, и это не вкус. Пузырь тянется по
     содержимому, содержимое — по пузырю: `width: 100%` замыкает круг на
     минимальной ширине, и на 1280 плеер остаётся такой же узкий, как на
     телефоне. 260 px задают пузырю ширину, `max-width` сжимает плеер на 375,
     где столько не помещается. */
  max-width: 100%;
  min-width: 0;
  width: 260px;
}

/* Родной плеер снят: его показывает только `<noscript>`-стиль из base.html. */
.tl-voice .tl-audio { display: none; }

/* Главная кнопка. 40 px — «крупная play/pause» из требования владельца;
   зону нажатия `::after` расширяет до 44 px, не раздувая пузырь. */
.tl-voice-play {
  align-items: center;
  background: color-mix(in srgb, currentColor 12%, transparent);
  border: 0;
  border-radius: var(--radius-full);
  color: inherit;
  cursor: pointer;
  display: flex;
  flex: none;
  height: 40px;
  justify-content: center;
  padding: 0;
  position: relative;
  transition: background-color 150ms ease-out;
  width: 40px;
}

.tl-voice-play::after {
  content: "";
  inset: -2px;
  position: absolute;
}

/* 🔴 `color` в наведении объявлен нарочно. Общий стиль `button:hover` красит
   любую кнопку в белый по синему — для кнопок-плашек это верно, а здесь кнопка
   живёт внутри пузыря и красится от него: без этой строки знак play белеет и
   пропадает на белом пузыре. Замечено владельцем 23.08.2026 на живом экране. */
.tl-voice-play:hover {
  background: color-mix(in srgb, currentColor 20%, transparent);
  color: inherit;
}

.tl-voice-play:active {
  background: color-mix(in srgb, currentColor 28%, transparent);
  color: inherit;
}

.tl-voice-glyph { pointer-events: none; }

/* Кнопка одна, а знаков на ней два: показывается тот, что отвечает состоянию
   записи. Состояние ставит `chat.js` классом на `.tl-voice`. */
.tl-voice-glyph--pause { display: none; }
.tl-voice--playing .tl-voice-glyph--play { display: none; }
.tl-voice--playing .tl-voice-glyph--pause { display: block; }

.tl-voice-track {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-05);
  min-width: 0;
}

/* Полоса во всю ширину — то, чего родной плеер не давал ни при каких стилях.
   Высота элемента 16 px против дорожки в 4 px: остальное — запас под палец,
   промахиваться по главному органу нельзя. */
.tl-voice-seek {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  /* Рамка и внутренние поля сняты нарочно: общий стиль полей ввода рисует
     ползунку рамку в 1 px и растит его до 34 px, и полоса превращается в
     пустое поле с точкой внутри; `min-height` там же держит 34 px. */
  border: 0;
  box-shadow: none;
  cursor: pointer;
  display: block;
  height: 16px;
  margin: 0;
  min-height: 16px;
  padding: 0;
  touch-action: manipulation;
  width: 100%;
}

/* Пройденная часть — заливка дорожки, а не второй элемент: положение приезжает
   одной переменной `--voice-pos` (0..1), которую ставит `chat.js`.

   🔴 52 %, а не «поаккуратнее»: при 25 % непройденная часть дорожки давала
   контраст 1,68:1 к белому пузырю — орган управления при пороге 3:1
   (WCAG 1.4.11), и менеджер не видел, докуда можно мотать. Замер 23.08.2026. */
.tl-voice-seek::-webkit-slider-runnable-track {
  background:
    linear-gradient(to right,
      currentColor 0 calc(var(--voice-pos, 0) * 100%),
      color-mix(in srgb, currentColor 52%, transparent) calc(var(--voice-pos, 0) * 100%) 100%);
  border-radius: var(--radius-full);
  height: 4px;
}

.tl-voice-seek::-moz-range-track {
  background:
    linear-gradient(to right,
      currentColor 0 calc(var(--voice-pos, 0) * 100%),
      color-mix(in srgb, currentColor 52%, transparent) calc(var(--voice-pos, 0) * 100%) 100%);
  border-radius: var(--radius-full);
  height: 4px;
}

.tl-voice-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  background: currentColor;
  border: 0;
  border-radius: var(--radius-full);
  height: 12px;
  margin-top: -4px;   /* центр бегунка на середине дорожки в 4 px */
  width: 12px;
}

.tl-voice-seek::-moz-range-thumb {
  background: currentColor;
  border: 0;
  border-radius: var(--radius-full);
  height: 12px;
  width: 12px;
}

.tl-voice-seek:focus-visible { outline: none; }
.tl-voice-seek:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 35%, transparent);
}
.tl-voice-seek:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 35%, transparent);
}

.tl-voice-row {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
}

/* Табличные цифры: без них «0:09 / 0:30» дёргает соседей на каждой секунде. */
.tl-voice-time {
  color: color-mix(in srgb, currentColor 75%, transparent);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Скорость — одно нажатие вместо трёх. `min-width` держит ширину на «1,5×»,
   иначе кнопка дёргается по кругу вместе с подписью. */
.tl-voice-rate {
  background: none;
  border: 1px solid color-mix(in srgb, currentColor 52%, transparent);
  border-radius: var(--radius-full);
  color: inherit;
  cursor: pointer;
  flex: none;
  font: inherit;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  min-width: 44px;
  padding: 4px var(--space-2);
  position: relative;
  transition: background-color 150ms ease-out;
}

/* Сама кнопка 44×34 — по высоте меньше пальца. Зону нажатия расширяет
   псевдоэлемент, чтобы строка «время — скорость» не разъезжалась. */
.tl-voice-rate::after {
  content: "";
  inset: -5px 0;
  position: absolute;
}

/* Та же история, что и у кнопки play, плюс рамка: `button:hover` перекрашивает
   и её, и подпись «1,5×» исчезает белым по белому. */
.tl-voice-rate:hover {
  background: color-mix(in srgb, currentColor 12%, transparent);
  border-color: color-mix(in srgb, currentColor 52%, transparent);
  color: inherit;
}

.tl-voice-rate:active {
  background: color-mix(in srgb, currentColor 20%, transparent);
  border-color: color-mix(in srgb, currentColor 52%, transparent);
  color: inherit;
}

@media (prefers-reduced-motion: reduce) {
  .tl-voice-play,
  .tl-voice-rate { transition: none; }
}

.tl-doc {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
  padding: 6px 10px;
  border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, currentColor 8%, transparent);
  color: inherit;
  font-size: var(--text-sm);
  text-decoration: none;
}

.tl-doc:hover { background: color-mix(in srgb, currentColor 14%, transparent); }

.tl-doc-name {
  overflow-wrap: anywhere;
  font-weight: 600;
}

.tl-doc-size {
  font-size: var(--text-xs);
  opacity: 0.7;
  white-space: nowrap;
}

/* Медиа, которого у нас нет: тип не поддержан каналом или скачать не вышло. */
.tl-media-missing {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
  opacity: 0.85;
}

.tl-media-missing a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ── Файл в карточке, клиенту не отправленный (F-110) ──
   Пунктир — состояние «черновика»: файл уже в CRM, но ещё не в переписке. */
.tl-file {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  align-items: start;
  gap: var(--space-3);
  max-width: 85%;
  padding: var(--space-2) var(--space-3);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
}

.tl-file-state { color: var(--color-warning-text); }

.tl-file-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.tl-file-send {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 28px;
  padding: 4px 10px;
  border: 1px solid color-mix(in srgb, var(--color-primary) 35%, transparent);
  border-radius: var(--radius-sm);
  background: var(--color-primary-soft);
  color: var(--color-primary);
  font-size: var(--text-xs);
  font-weight: 600;
  cursor: pointer;
}

.tl-file-send:hover,
.tl-file-send:focus-visible {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-primary);
}

.tl-file-send:disabled {
  opacity: 0.5;
  cursor: default;
}

/* F-113: почему кнопки отправки нет. Стоит на её месте, тем же размером —
   отсутствие кнопки без объяснения менеджер читает как поломку CRM. */
.tl-file-nosend {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

/* ── Форма ввода ── */
.tl-compose-area {
  border-top: 1px solid var(--color-border);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* Композер на телефоне: поле во всю ширину, кнопка под ним.

   🔴 Замер на 375 px: поле рядом с кнопкой получало 230 px, подсказка
   «Написать сообщение в Telegram… (Ctrl+Enter для отправки)» требовала 79 px
   высоты при 58 доступных — и была обрезана посередине первой строки. Менеджер
   на телефоне видел обрубок текста в поле, в которое собирался писать клиенту.
   Стопкой поле получает всю ширину, подсказка укладывается, а кнопка заодно
   дорастает до пальца. Ctrl+Enter на телефоне не нужен, но текст подсказки —
   не наше решение, и мы его не трогаем: чиним место, а не слова. */
@media (max-width: 560px) {
  .inbox-form {
    align-items: stretch;
    flex-direction: column;
  }

  .inbox-form .inbox-send,
  .tl-note-form button[type="submit"] {
    min-height: 44px;
    width: 100%;
  }
}

.tl-compose-tabs {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

/* ── Скрепка: загрузка файла из переписки (F-110) ── */
.tl-attach {
  position: relative;
  margin-left: auto;
}

.tl-attach-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 28px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

.tl-attach-btn:hover { background: var(--color-surface-sunken); }

.tl-attach-btn[aria-expanded="true"] {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

/* Меню раскрывается вверх: композер прижат к низу карточки. */
/* Меню скрепки. Приведено к тому же виду, что поповер групп `.optgrp-body`:
   это два всплывающих меню одного жанра, и они были собраны по разным правилам
   — радиус кнопки вместо радиуса панели, тень «просто md» вместо роли
   «над страницей», и `z-index: 20` сырым числом мимо шкалы 10/40/100. Разные
   правила на одинаковые вещи — это и есть «не в балансе». */
.tl-attach-menu {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  bottom: calc(100% + var(--space-1));
  box-shadow: var(--elevation-overlay);
  padding: var(--space-3);
  position: absolute;
  right: 0;
  width: min(340px, calc(100vw - 2 * var(--space-4)));
  z-index: var(--z-dropdown);
}

/* ── Переключатель канала над полем ввода ─────────────────────────────
   Радиогруппа, выглядящая рядом вкладок. Само радио спрятано визуально, но
   остаётся в потоке фокуса: обводка рисуется на подписи через `:focus-visible`
   внутри — иначе переключение с клавиатуры становится невидимым.

   Один клик на переключение против двух у HubSpot и Zendesk (разведка §9), и
   ноль строк JS: выбор уезжает вместе с формой сам. */
/* Своя строка НАД полем ввода, а не сосед поля. `.inbox-form` — флекс-ряд
   (поле и кнопка «Отправить»), и без этого переключатель встал бы третьим
   элементом ряда и сплющил поле до пары сантиметров. Проверено на экране. */
.tl-channels,
.tl-channel-blocked {
  flex-basis: 100%;
}

.tl-channels {
  border: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
  padding: 0;
}

/* 🔴 Селекторы переключателя ЗАПЕРТЫ внутри `.tl-channels` намеренно. Имя
   `.tl-channel` уже принадлежало значку канала в пузыре ленты (`.ch-mark.tl-channel`
   выше), и без запора рамка, отступы и `min-height: 40px` доставались значку:
   на телефонах (любое устройство без hover) вместо знака выходил пустой
   прямоугольник 18×40. Найдено на проде 23.08.2026. */
.tl-channels .tl-channel {
  align-items: center;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  color: var(--color-muted);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  transition: background var(--transition-fast), border-color var(--transition-fast),
              color var(--transition-fast);
}

.tl-channels .tl-channel input {
  height: 0;
  opacity: 0;
  position: absolute;
  width: 0;
}

.tl-channels .tl-channel:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* Выбранный канал — светлый с синей рамкой, а не залитый синим. Заливка
   отправила бы нас к подложке под знаком канала: знаки цветные и в фирменных
   цветах, голубой Telegram на синем не читается (F-206, решение заказчика
   15.08.2026 «без лишних фонов»). */
.tl-channels .tl-channel:has(input:checked) {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  color: var(--color-text);
  font-weight: var(--weight-semibold);
}

/* Обводка на подписи, потому что само радио невидимо. */
.tl-channels .tl-channel:has(input:focus-visible) {
  box-shadow: var(--focus-ring);
}

/* Канал, в который сейчас отправить нельзя. Показан, а не спрятан: исчезнувший
   пункт менеджер читает как поломку, а причина объясняет, почему так. */
.tl-channel--blocked {
  opacity: var(--opacity-disabled);
}

.tl-channel-blocked {
  align-items: center;
  color: var(--color-warning-text);
  display: flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
}

/* Палец вместо курсора: канал переключают и с телефона. */
@media (hover: none) {
  .tl-channels .tl-channel { min-height: 40px; }
}

.tl-compose-tab {
  padding: 4px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: transparent;
  font-size: var(--text-xs);
  cursor: pointer;
  color: var(--color-muted);
  transition: all var(--transition-fast);
}

.tl-compose-tab:hover {
  background: var(--color-surface-sunken);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* Активная вкладка — светлая, а не залитая синим (F-206, правка по замечанию
   заказчика). Причина не вкусовая: на вкладке стоит знак канала, и на синей
   заливке он требовал белой подложки — вкладка становилась самым тяжёлым
   объектом области ввода, тяжелее самой кнопки «Отправить». Светлая подложка
   с синей рамкой и синим текстом читается как выбранная не слабее, а знак
   канала стоит на своём фоне. Текст на подложке — 4.59:1, выше порога AA. */
.tl-compose-tab.active,
.tl-compose-tab.active:hover {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  color: var(--color-primary);
  font-weight: var(--weight-semibold);
}

.tl-compose-panel { display: none; }
.tl-compose-panel.active { display: block; }

.tl-note-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* ── Сетка проекта (F-108) ───────────────────────────────────
   Правила из docs/reference/layout-rules.md, экраны собираются на них:
   1) колонка не уже своей самой длинной подписи (--field-min/--pane-min);
   2) ширина делится долями (fr/flex-grow), а не фиксируется в px;
   3) при избытке места колонки растягиваются, а не оставляют пустоту;
   4) прокрутка живёт внутри контейнера (.scroll-x), не у страницы.
   min-width: 0 на потомках обязателен: дефолт min-width:auto не даёт
   flex/grid-элементу сжаться, родитель вылезает за вьюпорт и включает
   ложную прокрутку страницы. */

/* Форма: полей в ряд 2-3 по смыслу, не сколько поместится. Подпись над
   полем. Для «сколько влезет по месту» есть .auto-grid ниже. */
.form-grid {
  display: grid;
  gap: var(--space-4) var(--space-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.form-grid--3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.form-grid > * {
  min-width: 0;
}

.form-grid input:not([type="checkbox"]):not([type="radio"]),
.form-grid select,
.form-grid textarea {
  max-width: var(--field-max);
}

/* Значение, у которого выбора не бывает (каркас 50×100 там, где 50×50 у
   поставщика нет), показывается текстом, а не задизейбленным select: тот
   читается как поломка. Высота — как у контрола, иначе текст висит выше
   соседнего поля и ряд выглядит съехавшим. */
.form-grid .field-value--control {
  min-height: var(--control-h);
}

@media (max-width: 1020px) {
  .form-grid--3 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 700px) {
  .form-grid,
  .form-grid--3 {
    grid-template-columns: 1fr;
  }
}

/* Экран из панелей: доли вместо фиксированных полос. Боковая панель не
   уже --pane-min, центр берёт остальное и умеет сжиматься. */
.split {
  align-items: start;
  display: grid;
  gap: var(--space-4);
}

.split > * {
  min-width: 0;
}

.split--2 {
  grid-template-columns: minmax(var(--pane-min), 1fr) minmax(0, 2.2fr);
}

.split--3 {
  grid-template-columns: minmax(var(--pane-min), 1fr) minmax(0, 2fr) minmax(var(--pane-min), 1fr);
}

@media (max-width: 980px) {
  .split--2,
  .split--3 {
    grid-template-columns: 1fr;
  }
}

/* Широкое содержимое (таблица, доска) скроллится у себя, не у страницы */
.scroll-x {
  min-width: 0;
  overflow-x: auto;
}

/* Тон стадии: --stage-color ставится классом, красит индикаторы ниже */
.stage-tone--s1 { --stage-color: var(--stage-1); }
.stage-tone--s2 { --stage-color: var(--stage-2); }
.stage-tone--s3 { --stage-color: var(--stage-3); }
.stage-tone--s4 { --stage-color: var(--stage-4); }
.stage-tone--s5 { --stage-color: var(--stage-5); }
.stage-tone--s6 { --stage-color: var(--stage-6); }
.stage-tone--won { --stage-color: var(--stage-won); }
.stage-tone--lost { --stage-color: var(--stage-lost); }

/* Точка стадии — для списков, селектов и бейджей рядом с названием */
.stage-dot {
  background: var(--stage-color, var(--color-neutral-border));
  border-radius: var(--radius-full);
  display: inline-block;
  flex: none;
  height: 8px;
  width: 8px;
}

/* Переиспользуемая интринсик-сетка (RAM auto-fit, без media-queries):
   колонки встают рядом при наличии места, иначе стопкой. min() не даёт колонке
   переполнить контейнер уже минимума. Ширина колонки — через --auto-grid-min. */
.auto-grid {
  align-items: start;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min, 300px), 100%), 1fr));
}

/* Форма оформления заказа — scoped, НЕ трогает общие .info-panel */
.order-form .info-panel {
  align-content: start;
  display: grid;
  gap: var(--space-3);
}
.order-form .info-panel h2 {
  font-size: var(--text-md);
  margin: 0;
}
.order-form .order-rail {
  align-self: start;
}
.order-form .order-total {
  color: var(--color-primary);
  font-size: var(--text-lg);
  font-weight: 700;
  margin: 0;
}

/* Несколько строений в одном заказе (F-093): разбивка в правой колонке.
   Выбора строений в форме больше нет (владелец 20.08.2026) — состав приходит
   из расчёта фактом; серверная сборка source_quotes жива. */
.order-building-sum {
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  margin-left: auto;
  white-space: nowrap;
}
.order-buildings {
  display: grid;
  gap: var(--space-1);
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0;
}
.order-buildings li {
  display: flex;
  gap: var(--space-3);
}
.order-building-name {
  font-weight: var(--weight-medium);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ─── Знаки внешних каналов (F-206, поглотила F-199) ────────────────────────
   Telegram, WhatsApp, MAX, Авито. Менеджер обязан видеть, В КАКОЙ канал он
   пишет, до того как нажмёт «Отправить»; знак читается боковым зрением за то
   время, за которое глаз только начинает разбирать слово.

   🔴 Это НЕ иконки интерфейса. Иконки интерфейса — Lucide, спрайт, `<c-icon>`,
   `currentColor`, один набор на проект. Знак канала — чужой товарный знак: его
   нельзя перекрасить под тему, перерисовать под общую толщину штриха и собрать
   из текста. Отсюда `<img>` на готовый файл и полное отсутствие `filter`,
   `opacity` и заливки в этом блоке. Происхождение файлов и лицензии —
   `static/core/icons/channels/ПРОИСХОЖДЕНИЕ.md`.

   Размер один — 16 px, ровно между `--icon-sm` и `--icon-md`. Отклонения ровно
   два и оба объяснены на месте: 14 px в пузыре переписки и подложка на синем.

   Файлы разнородны намеренно: Telegram и WhatsApp — чёрные силуэты Simple
   Icons, MAX и Авито — в собственных цветах. Приводить их к одному виду
   означало бы перекрасить чужой знак, что запрещено. Общий вид им задаёт не
   цвет, а одинаковый бокс и одинаковое место в строке. */
.ch-mark {
  flex: 0 0 auto;
  height: 16px;
  object-fit: contain;
  vertical-align: -0.2em;
  width: 16px;
}

/* Знак с подписью: вкладка канала, заголовок панели, вкладка композера. */
.ch-tag {
  align-items: center;
  display: inline-flex;
  gap: var(--space-1);
  min-width: 0;
}

.ch-tag-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Канал без знака — «Сайт», «Телефония», «Другое». Показывается текстом и
   ничего не ломает: это отдельный пункт приёмки F-206, а не аварийный путь.
   Никакой заглушки-квадратика: пустой квадрат читается как «знак не
   загрузился», то есть как поломка. */
.ch-name { white-space: nowrap; }

/* 🔴 НИКАКИХ ПОДЛОЖЕК ПОД ЗНАКОМ. Первый заход F-206 ставил знак на белый
   квадратик везде, где фон насыщенный, — и заказчик забраковал: подрисованный
   фон делал знак крупнее и тяжелее всего вокруг, особенно на кнопке отправки.

   Причина подложки была в файлах, а не в вёрстке: знаки Telegram и WhatsApp
   были чёрными силуэтами Simple Icons и на синем не читались. Теперь взяты
   официальные файлы в фирменных цветах, и правило стало простым:

   **знак канала живёт только на светлой поверхности.** Там, где фон залит
   `--color-primary` (кнопка «Написать в …», кнопка «Отправить», исходящий
   пузырь), знака нет вовсе — вместо него иконка Lucide или ничего. Так знак
   всегда стоит на том фоне, для которого нарисован, и подрисовывать ему
   собственный не приходится. */

/* Знак канала у заголовка «Переписка»: канал разговора назван один раз и
   крупно — там, где менеджер ищет, «где я вообще нахожусь». */
.panel-head-channel {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

/* «Разговор продолжился в другом месте» (F-145) — рядом со знаком канала, в
   той же шапке ленты. Обводкой, а не заливкой: заливка соперничала бы со
   знаком канала, который здесь главный, а сказать надо ровно одно — почему
   лента ниже обрывается. Переносится на свою строку на узком экране, потому
   что шапка ленты и так тесная (`docs/reference/layout-rules.md`). */
.panel-head-moved {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  color: var(--color-muted);
  /* Не flex: многоточие работает только на блоке с текстом, а во flex-строке
     текст становится безымянным элементом, и обрезать его нечем. Иконка
     выровнена самой `.icon` (`vertical-align`), лишний контейнер не нужен. */
  display: block;
  font-size: var(--text-2xs);
  margin: 0;
  /* Шапка ленты — строка без переносов, и подпись обязана ужиматься первой:
     заголовок «Переписка» и кнопка «Прочитано» ужиматься не умеют. Полный
     текст остаётся в `title` — на узкой панели видно начало, на широкой всё. */
  min-width: 0;
  overflow: hidden;
  padding: 2px var(--space-2);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Строка очереди: знак первым, до имени клиента. `space-between` в `.iq-head`
   рассчитан на двоих, поэтому имя забирает остаток строки само, а не отдаёт
   его промежутку. `align-self` — потому что строка выровнена по базовой линии
   текста, а у картинки базовая линия — нижний край. */
.iq-channel { align-self: center; }
.iq-avatar { align-self: center; }
.iq-head .iq-open { flex: 1 1 auto; min-width: 0; }

/* Телефоны контакта: строка на номер. Список, а не пара полей «Телефон» и
   «Ещё телефон» — номеров у человека бывает сколько угодно, и третий в жёсткой
   паре не показывался вовсе. Номер моноширинный (поле несёт mono="1"),
   подписи вокруг — обычным шрифтом: моно у слов читается как код. */
.phone-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
}

.phone-row {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
}

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

/* Правимый номер того же кегля, что соседние. `.hx-value` задаёт --text-sm
   на весь проект, а поле телефона живёт на --text-base: в колонке из трёх
   номеров один оказывался мельче двух других (замер ревью 23.08.2026 —
   13 px против 14). Кегль возвращается только здесь, общее правило цело. */
.phone-row .hx-value { font-size: inherit; }

.phone-label,
.phone-source {
  align-items: center;
  color: var(--color-muted);
  display: inline-flex;
  font-family: var(--font-base);
  font-size: var(--text-2xs);
  gap: var(--space-1);
}

.phone-label {
  background: var(--color-neutral-bg);
  border-radius: var(--radius-input);
  color: var(--color-neutral-text);
  padding: 1px 5px;
}

/* Строка «Распознан в переписке» переносится целиком, а не рвётся посередине:
   в колонке карточки 250 px это единственная длинная подпись строки. */
.phone-source { flex-basis: 100%; }

/* Знаки стоят В строке номера, а не под ней: отступ сверху был нужен, пока
   они висели отдельным блоком под полем. */
.phone-row .channel-badges { margin-top: 0; }

/* Правимый номер втянут на свои внутренние отступы. Без этого он стоит на 7 px
   правее соседних (padding 6 + рамка 1 у `.hx-value`), и колонка телефонов
   выглядит сломанной лесенкой. Приём тот же, что у заголовка сделки. */
.phone-row .hx-value { margin-left: -7px; }

.channel-badges { display: flex; align-items: center; gap: 4px; margin-top: 4px; }
.channel-badge { font-size: 11px; line-height: 1; padding: 2px 6px; border-radius: 4px;
  background: var(--color-surface-sunken); color: var(--color-muted); font-weight: 600; }
.order-form .phone-channels {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
  margin-top: var(--space-1);
}
.order-form .phone-channels .phone-channels-label {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 400;
  width: 100%;
}
.order-form .phone-channels label {
  align-items: center;
  display: flex;
  flex-direction: row;
  font-size: var(--text-sm);
  font-weight: 500;
  gap: var(--space-1);
  margin: 0;
}
.order-form .phone-channels input[type="checkbox"] {
  accent-color: var(--color-primary);
  flex-shrink: 0;
  height: 14px;
  margin: 0;
  min-height: 0;
  padding: 0;
  width: 14px;
}
.order-form .phone-extra { margin-top: var(--space-2); }
.order-form .phone-extra summary {
  color: var(--color-muted);
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: 650;
}
.phone-extra-card {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
}
.order-form .field-hint {
  color: var(--color-muted);
  display: block;
  font-size: var(--text-xs);
  margin-top: var(--space-1);
}
.order-form .plan-upload-stub {
  align-items: center;
  border: 1px dashed var(--color-muted);
  border-radius: 6px;
  color: var(--color-muted);
  cursor: default;
  display: flex;
  font-size: var(--text-sm);
  justify-content: center;
  min-height: 56px;
  opacity: 0.7;
  padding: var(--space-4);
  text-align: center;
  user-select: none;
}

/* Дайджест поставщика (production): мобильный, одна колонка. */
.digest-zone {
  margin-bottom: var(--space-6);
}

.digest-zone-title {
  font-size: var(--text-lg);
  font-weight: 700;
  margin: 0 0 var(--space-3);
}

.digest-group-title {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 650;
  margin: var(--space-4) 0 var(--space-2);
}

.digest-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
  padding: var(--space-4);
}

.digest-card-head {
  align-items: flex-start;
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
}

.digest-card-title {
  flex: 1 1 auto;
  font-size: var(--text-md);
  font-weight: 700;
  min-width: 0;
}

.digest-card-head .status-badge {
  flex: 0 0 auto;
}

.digest-card-body {
  color: var(--color-muted);
  display: flex;
  flex-direction: column;
  font-size: var(--text-sm);
  gap: var(--space-1);
}

.digest-card-contact {
  color: var(--color-text);
  font-weight: 600;
}

.digest-card-address {
  color: var(--color-muted);
}

.digest-card-foot {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
  margin-top: var(--space-1);
}

.digest-card-total {
  color: var(--color-text);
  font-size: var(--text-md);
  font-weight: 700;
  white-space: nowrap;
}

.digest-card-body a {
  margin-left: var(--space-2);
}

.digest-card.is-hero {
  border: 2px solid var(--color-danger-text);
}

.digest-card.is-hero .digest-card-title {
  font-size: var(--text-lg);
}

.digest-card.is-new {
  border-left: 4px solid var(--color-warning-text);
}

.digest-card .row-action {
  min-height: 44px;
  padding: 9px var(--space-4);
}

/* Лёгкая карточка заказа поставщика (order_detail_supplier.html). */
.digest-money-tiles {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

.digest-money-tile {
  background: var(--color-neutral-bg);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
}

#copy-order {
  margin-top: var(--space-3);
  min-height: 44px;
  width: 100%;
}

.priority-panel form button,
.priority-panel form .primary-button {
  min-height: 44px;
}

/* Экран «Все заказы»: поиск + сегмент (мобайл-first) */
.overview-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.overview-search {
  display: flex;
  gap: var(--space-2);
}
.overview-search input[type="search"] {
  flex: 1;
}
.overview-segment {
  display: flex;
  gap: 4px;
  background: var(--color-neutral-bg);
  border-radius: var(--radius-md);
  padding: 3px;
}
.overview-segment a {
  flex: 1;
  text-align: center;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
  text-decoration: none;
}
.overview-segment a.active {
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-sm);
}
.overview-month-head {
  align-items: center;
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.overview-month-head .digest-zone-title {
  margin: 0;
}
.digest-card-money {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}
.digest-card-commission {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: 700;
  text-align: right;
}

/* ── Файлы лида (F-076; с F-110 живут в ленте, панели нет) ──────────────── */
.file-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-neutral-bg);
}

.file-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Тип файла подписью, а не иконкой-эмодзи: набор форматов узкий и опознаётся
   текстом надёжнее любого значка. */
.file-kind {
  color: var(--color-neutral-text);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.file-body {
  min-width: 0;
}

.file-name {
  display: block;
  overflow-wrap: anywhere;
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: 600;
}

/* Колонка карточки узкая (~260 px), поэтому метаданные и удаление живут одной
   переносимой строкой: отдельная колонка под кнопку ломала имя файла на три. */
.file-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  margin-top: 2px;
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.file-meta > span {
  white-space: nowrap;
}

.file-delete {
  flex: none;
  padding: 4px 6px;
  /* Прозрачная рамка, а не её отсутствие: на hover меняется цвет, и без неё
     строка дёргалась бы на пиксель. */
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-family: var(--font-base);
  font-size: var(--text-xs);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.file-delete:hover,
.file-delete:focus-visible {
  border-color: color-mix(in srgb, var(--color-danger-text) 40%, transparent);
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
}

.file-error {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
  font-size: var(--text-sm);
}

.file-error-close {
  flex: none;
  display: inline-flex;
  padding: 2px;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.file-upload-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

/* Два источника файла — одна группа, а не две одинаковые кнопки подряд: колонка
   узкая (~260 px), и без общей рамки пара читается как ошибка вёрстки. */
.file-pick-group {
  display: flex;
  flex: none;
}

.file-pick-group .file-pick:first-child {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.file-pick-group .file-pick:last-child {
  margin-left: -1px;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/* Наведённая половина поверх соседней, иначе её левая рамка режет подсветку. */
.file-pick-group .file-pick:hover,
.file-pick-group .file-pick:focus-within {
  z-index: 1;
}

.file-pick {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 6px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.file-pick:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.file-pick:focus-within {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Системный input прячем, но оставляем в потоке: он остаётся клавиатурной
   целью, а видимая кнопка — его подпись. */
.file-pick input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.file-hint,
.file-progress {
  color: var(--color-muted);
  font-size: var(--text-xs);
}

/* ---------------------------------------------------------------------------
   Страницы ошибок (403/404/500). Автономный слой: рендерятся без base.html,
   поэтому не опираются на .app-shell.
--------------------------------------------------------------------------- */

.error-body {
  display: grid;
  place-items: center;
  min-height: 100dvh;
  margin: 0;
  background: var(--color-page);
  font-family: var(--font-base);
  color: var(--color-text);
}

.error-page {
  max-width: 26rem;
  padding: var(--space-6);
}

.error-code {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(3.5rem, 12vw, 5.5rem);
  font-weight: var(--weight-medium);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--color-faint);
}

.error-name {
  margin: var(--space-3) 0 0;
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.error-hint {
  margin: var(--space-2) 0 0;
  color: var(--color-muted);
  font-size: var(--text-md);
  line-height: var(--leading-normal);
}

.error-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

/* F-026. История изменений полей. Своего визуального языка не заводит —
   только то, чего нет в общей таблице. */

/* Общий `table` держит min-width: 1120px под широкие списки лидов. Здесь колонок
   четыре, содержимое укладывается в 1014 — и полоса прокрутки вылезала на 1280
   при полупустом экране (пункт 5 приёмки вёрстки). 640px оставляет прокрутку там,
   где она честная: на 375. */
.history-table {
  min-width: 640px;
}

/* Дата и время — моноширинные цифры: колонку сравнивают по вертикали, разряды
   обязаны стоять друг под другом. Перенос запрещён: «08.08.2026» в две строки
   удваивает высоту каждой строки таблицы. */
.history-when {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Несколько полей одного сохранения читаются списком, а не абзацем. */
.history-change + .history-change {
  margin-top: var(--space-1);
}

/* В карточке на телефоне `.mobile-lead-header` — грид, и плашка времени иначе
   растягивается на всю ширину, читаясь как заголовок. То же со статусом заказа
   (F-219): растянутая плашка выглядит полосой, а не значком. */
.mobile-lead-card .history-when,
.mobile-lead-header .status-badge {
  justify-self: start;
}

/* ── Список лидов ─────────────────────────────────────────
   Пять колонок вместо восьми помещаются в 1049px рабочей области на 1280,
   поэтому прокрутки у таблицы больше нет. Название лида — треть ширины:
   именно по нему ищут строку глазами. */
.leads-table .cell-lead {
  width: 34%;
}

.leads-table .cell-task {
  color: var(--color-muted);
}

.leads-table .cell-when {
  white-space: nowrap;
}

.leads-table .cell-when .muted {
  white-space: nowrap;
}

/* ── Список заказов ───────────────────────────────────────
   Плотность (F-219, норма из `docs/reference/crm-ux-patterns.md`). Замер до
   правки на 1280×800: строка 126 px, потому что адрес доставки переносился на
   четыре строки, и в экран помещалось пять заказов. Норма — строка 44 px и
   четырнадцать в экран.

   Приём тот же, что у таблицы сделок (F-212), и работает только целиком:
   `table-layout: fixed` (иначе ширину колонки диктует самое длинное значение),
   одна строка текста в ячейке, обрезка многоточием. Ширины назначены
   заголовкам: при фиксированной раскладке браузер смотрит только на первый ряд.
   Адрес — единственная колонка без своей ширины: она забирает остаток и гаснет
   в многоточии первой, полный текст приходит из `title`. */
.orders-table {
  font-size: var(--text-sm);
  table-layout: fixed;
}

.orders-table th {
  height: 26px;
  overflow: hidden;
  padding-block: 2px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.orders-table td {
  height: var(--table-row-height);
  overflow: hidden;
  padding-block: 0;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Номер и название сделки — одна строка: название догоняет номер и гаснет
   первым, потому что строку ищут глазами по номеру. То же для клиента с
   телефоном. */
.orders-table .cell-order .lead-title,
.orders-table .cell-order .muted {
  display: inline;
}

.orders-table .cell-order .muted::before {
  content: " · ";
}

/* Ширины — по самому длинному реальному значению плюс паддинг, а не на глаз:
   «Подтверждён производством» значком статуса — 200 px, «15.08.2026» — 100 px.
   Колонка не может быть уже своей самой длинной подписи (грабля F-211). */
.orders-table .col-order {
  width: 215px;
}

.orders-table .col-client {
  width: 150px;
}

.orders-table .col-delivery {
  width: 105px;
}

.orders-table .col-status {
  width: 230px;
}

.orders-table .col-owner {
  width: 130px;
}

/* Поиск и статус заказов (F-219). Одна строка вместо двух у сделок: условий
   здесь два, а каждый ярус над списком стоит одной строки данных. Высота
   контролов — 30 px, как в панели сделок; на телефоне они растут до 44 px,
   иначе кнопка становится непопадаемой целью. */
.order-filters {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  padding: var(--space-2);
}

.order-filters :is(input, select, .secondary-button) {
  min-height: 30px;
  padding-block: var(--space-1);
}

.order-filters-search {
  flex: 1 1 260px;
  min-width: 0;
}

/* Ширина поля диктуется его самой длинной подписью: «Подтверждён
   производством» в 175px браузер обрезал бы многоточием. */
.order-filters-field {
  flex: 0 1 235px;
  min-width: 0;
}

/* ============================================================
   РАБОЧЕЕ МЕСТО МЕНЕДЖЕРА: список сделок и экран сотрудников
   (F-050, F-067). Цвета — только токены :root; тон стадии
   приходит переменной --stage-color от .stage-tone--*.
   ============================================================ */

/* --- Сохранённые виды: вкладки, самая левая — «все» ---------- */
.view-tabs {
  align-items: flex-end;
  border-bottom: 1px solid var(--color-border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-1);
}

.view-tab-wrap {
  align-items: center;
  display: inline-flex;
}

/* Счётчик внутри вкладки — цифра, а не плашка: с подложкой и паддингом он
   задавал высоту всего ряда вкладок, а ряд стоит одной строки задач. */
.view-tab .record-count {
  background: none;
  padding: 0;
}

.view-tab {
  border-bottom: 2px solid transparent;
  color: var(--color-muted);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  padding: var(--space-1) var(--space-3);
  text-decoration: none;
  transition: color var(--transition-fast), border-color var(--transition-fast);
}

/* Вкладки срезов — самое частое, что менеджер нажимает на телефоне: ими он
   переключает «На сегодня / Просроченные», каналы в инбоксе и режимы очереди.
   Замер 23.08.2026 на 375 px дал 26 px высоты при норме 44. Растёт только
   область нажатия: кегль, цвет и подчёркивание активной остаются как были. */
@media (hover: none) {
  .view-tab,
  .scope-option {
    /* `min-height` на строчном элементе не работает — `.scope-option` это
       ссылка, и без `inline-flex` она осталась бы прежней высоты. */
    align-items: center;
    display: inline-flex;
    min-height: 40px;
  }
}

.view-tab:hover {
  color: var(--color-text);
}

.view-tab.is-current {
  border-bottom-color: var(--color-primary);
  color: var(--color-text);
}

.view-tab-remove {
  background: none;
  border: 0;
  border-radius: var(--radius-control);
  color: var(--color-faint);
  cursor: pointer;
  display: inline-flex;
  padding: var(--space-1);
}

.view-tab-remove:hover {
  background: var(--state-hover);
  color: var(--color-danger-text);
}

/* --- Панель фильтров: две строки, и она не растёт (F-189) ----
   Замер до правки: 390px высоты на 1280 и 818px на 375; первая строка списка
   на экране 1280×800 начиналась с 662px, то есть за сгибом ноутбука. Съедали
   высоту три вещи: подписи над полями, восемь чипов стадий в ряд и четыре чипа
   задач. Стадии ушли в поповер (чип со счётчиком), задачи — в один select,
   подписи — в aria-label. Осталось две строки, ~110px. */
.deal-filters {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  display: grid;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
  padding: var(--space-2);
}

/* Контролы фильтра — 32 px, а не 40 (F-212): две строки панели стоили 101 px,
   и это была вторая по величине статья расхода до первой строки списка. */
.deal-filters :is(input, select, .secondary-button, .primary-action, .chip-button, .chip-select) {
  min-height: 30px;
  padding-block: var(--space-1);
}

.deal-filters .chip-check {
  min-height: 28px;
  padding-block: 2px;
}

.deal-filters-row {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.deal-filters-search {
  flex: 1 1 260px;
  min-width: 0;
}

/* Ширина поля диктуется его самой длинной подписью, а не сеткой: «Изменена: за
   всё время» в 190px браузер обрезал многоточием. */
.deal-filters-field {
  flex: 0 1 175px;
  min-width: 0;
}

.deal-filters-field--period {
  flex-basis: 210px;
}

.deal-filters-actions {
  display: flex;
  gap: var(--space-2);
  margin-left: auto;
}

.deal-filters-sep {
  background: var(--color-border);
  height: 20px;
  width: 1px;
}

/* Внутри поповера стадии стоят столбиком: подпись «Первичный контакт» плюс имя
   воронки в ряд по двое режется, а поповер шире 420px закрывает пол-списка. */
.stage-check-group {
  display: grid;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

/* Чип-переключатель. Нативный контрол не удаляется, а прячется: он остаётся
   в табуляции и озвучивается скринридером, кольцо фокуса рисует обёртка. */
.chip-check {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  line-height: 1;
  min-height: 32px;
  padding: var(--space-2) var(--space-3);
  position: relative;
  transition: background var(--transition-fast), border-color var(--transition-fast);
  user-select: none;
}

.chip-check input {
  height: 100%;
  left: 0;
  margin: 0;
  opacity: 0;
  position: absolute;
  top: 0;
  width: 100%;
}

.chip-check:hover {
  background: var(--state-hover);
}

.chip-check:has(input:checked) {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  font-weight: var(--weight-medium);
}

.chip-check:has(input:focus-visible) {
  box-shadow: var(--focus-ring);
  outline: none;
}

/* Стадия несёт свой тон и в фильтре — тот же язык, что на доске и в таблице.
   Цветом красится точка и рамка, не весь фон: красить фон целиком при восьми
   стадиях нельзя, это правило из docs/reference/layout-rules.md. */
.chip-check--stage:has(input:checked) {
  background: color-mix(in srgb, var(--stage-color) 12%, var(--color-surface));
  border-color: var(--stage-color);
}

.chip-check--won:has(input:checked) {
  background: var(--color-success-bg);
  border-color: var(--color-success-border);
  color: var(--color-success-text);
}

.chip-check--lost:has(input:checked) {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

.chip-check-hint {
  color: var(--color-muted);
  font-size: var(--text-2xs);
}

/* --- Чипы, раскрывающие поповер: «Стадии» и «Ещё фильтры» ----
   Это <summary> нативного <details>: клавиатура, скринридер и состояние
   «раскрыто» достаются бесплатно, JS-состояния на клиенте нет. Раскрытие шлёт
   `toggle`, а не `change`, поэтому htmx на него не реагирует.
   Число на чипе обязательно: свёрнутый фильтр, который сужает список молча, —
   ровно та поломка, которую мы уже ловили в F-151. */
.chip-button {
  align-items: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  line-height: 1;
  list-style: none;
  min-height: 32px;
  padding: var(--space-2) var(--space-3);
  transition: background var(--transition-fast), border-color var(--transition-fast);
  user-select: none;
}

.chip-button::-webkit-details-marker {
  display: none;
}

.chip-button:hover {
  background: var(--state-hover);
}

.chip-button:focus-visible {
  box-shadow: var(--focus-ring);
  outline: none;
}

/* «Выбрано» читается прямо из наличия счётчика, а не отдельным классом: счётчик
   приезжает внеполосным свопом, и второй источник правды разошёлся бы с ним. */
.chip-button:has(.chip-count) {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  font-weight: var(--weight-medium);
}

.chip-state:empty {
  display: none;
}

.chip-count {
  background: var(--color-primary);
  border-radius: var(--radius-pill);
  color: var(--color-on-primary);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  line-height: 1;
  padding: 3px 6px;
}

.chip-caret {
  color: var(--color-faint);
  transition: transform var(--transition-fast);
}

[open] > .chip-button .chip-caret {
  transform: rotate(180deg);
}

/* Задачи — нативный select ростом с чип: значение одно из четырёх, и четыре
   чипа занимали половину строки ради выбора, который делают раз в день. */
.chip-select {
  border-radius: var(--radius-pill);
  flex: none;
  font-size: var(--text-sm);
  min-height: 32px;
  padding: var(--space-1) 28px var(--space-1) var(--space-3);
  width: auto;
}

/* Тело поповера. Раскрывается ПОВЕРХ страницы (приём F-175): список под ним не
   двигается, и высота свёрнутой панели не зависит от числа стадий. Потолок
   высоты со своей прокруткой — иначе восемь стадий плюс шесть групп «ещё
   фильтров» вылезают за нижний край окна. */
.filter-pop {
  position: relative;
}

.filter-pop-body {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-overlay);
  left: 0;
  max-height: min(60vh, 460px);
  max-width: min(420px, calc(100vw - 2 * var(--space-4)));
  overflow-y: auto;
  padding: var(--space-3);
  position: absolute;
  top: calc(100% + var(--space-1));
  width: max-content;
  z-index: var(--z-dropdown);
}

/* Последний чип в строке: панель, выровненная по левому краю, вылезала бы за
   правую границу окна. */
.filter-pop--right .filter-pop-body {
  left: auto;
  right: 0;
}

.filter-pop--wide .filter-pop-body {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  max-width: min(720px, calc(100vw - 2 * var(--space-4)));
  width: min(720px, calc(100vw - 2 * var(--space-4)));
}

.filter-group {
  border: 0;
  margin: 0;
  min-width: 0;
  padding: 0;
}

.filter-group legend {
  margin-bottom: var(--space-2);
  padding: 0;
}

.filter-check-list {
  display: grid;
  gap: var(--space-1);
  max-height: 180px;
  overflow-y: auto;
}

.check-row {
  align-items: center;
  border-radius: var(--radius-control);
  cursor: pointer;
  display: flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  min-height: 28px;
  padding: var(--space-1) var(--space-2);
}

.check-row:hover {
  background: var(--state-hover);
}

.check-row input {
  flex: none;
  margin: 0;
}

.filter-dates {
  display: grid;
  gap: var(--space-2);
}

.filter-dates label {
  display: grid;
  gap: var(--space-1);
}

.filter-dates span {
  color: var(--label-color);
  font-size: var(--label-size);
}

/* --- Применённые условия ------------------------------------ */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.filter-chip {
  align-items: center;
  background: var(--color-neutral-bg);
  border: 1px solid var(--color-neutral-border);
  border-radius: var(--radius-pill);
  color: var(--color-neutral-text);
  display: inline-flex;
  font-size: var(--text-xs);
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  text-decoration: none;
}

.filter-chip:hover {
  background: var(--color-danger-bg);
  border-color: var(--color-danger-border);
  color: var(--color-danger-text);
}

.filter-chip--reset {
  background: none;
  border-style: dashed;
}

/* --- Полоса над результатом --------------------------------- */
.deal-result-bar {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
  min-height: 28px;
}

.deal-result-bar :is(.ghost-button, .per-page-option) {
  min-height: 26px;
  padding-block: 2px;
}

.deal-result-bar .record-count {
  background: none;
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-normal);
  padding: 0;
}

/* Выпадающая панель на <details>: состояние держит браузер, JS не нужен. */
.menu-drop {
  position: relative;
}

.menu-drop > summary {
  cursor: pointer;
  list-style: none;
}

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

.menu-drop-body {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  box-shadow: var(--elevation-overlay);
  display: grid;
  gap: var(--space-2);
  min-width: 240px;
  padding: var(--space-3);
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: var(--z-dropdown);
}

.menu-drop-title {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: 0;
}

/* Меню из нескольких смысловых кусков (колонки, размер страницы, сохранение
   вида). Линия между ними обязательна: без неё три формы подряд читаются как
   одна, и «Сохранить набор» выглядит кнопкой всего меню. */
.menu-drop-body--sections {
  min-width: 280px;
}

.menu-drop-body--sections > * + * {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-3);
}

.menu-drop-body--sections > form {
  display: grid;
  gap: var(--space-2);
}

.notification-button,
.notification-menu-head {
  align-items: center;
  display: flex;
  gap: var(--space-2);
}

.notification-button {
  background: transparent;
  border: 0;
  color: var(--color-text);
  min-height: 40px;
}

.notification-menu-body { max-height: min(420px, 70vh); min-width: min(360px, calc(100vw - 2 * var(--space-4))); }
/* Три ряда, а не два: между шапкой и списком встала строка настроек (F-236).
   Прокручивается по-прежнему только список — `minmax(0, 1fr)` на нём. */
/* `max-height`, а не `height`: с двумя уведомлениями панель на 420px — это
   экран пустоты под двумя строками. Прокрутка при длинном списке остаётся:
   её держит `minmax(0, 1fr)` на самом списке. */
.notification-menu-body:has(.notification-list) { grid-template-rows: auto auto minmax(0, 1fr); max-height: min(420px, 70vh); }
.notification-settings { align-items: center; border-bottom: 1px solid var(--color-border); display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); padding-bottom: var(--space-2); }
.notification-switch { align-items: center; color: var(--color-muted); display: flex; gap: var(--space-1); min-height: 32px; }
.notification-switch input:disabled + span { opacity: 0.5; }
.notification-push-status { color: var(--color-muted); flex-basis: 100%; font-size: var(--text-xs); margin: 0; }
/* Отказ браузера — не оттенок серого: его надо заметить, а не вычитать. */
.notification-push-status[data-push-state="denied"],
.notification-push-status[data-push-state="failed"],
.notification-push-status[data-push-state="unsupported"],
.notification-push-status[data-push-state="insecure"] { color: var(--color-danger-text); }
.notification-menu-head { justify-content: space-between; }
/* `align-content: start` — иначе две строки в панели фиксированной высоты
   растягиваются на все 420px и читаются как «список кончился». */
.notification-list { align-content: start; display: grid; gap: var(--space-1); list-style: none; margin: 0; min-height: 0; overflow-y: auto; padding: 0; }
.notification-item { align-items: center; display: flex; gap: var(--space-2); padding: var(--space-2); }
.notification-item--new { background: var(--color-surface); font-weight: var(--weight-semibold); }
.notification-item-icon { color: var(--color-muted); display: inline-flex; }
.notification-empty { margin: var(--space-2) 0; }

.field-block {
  display: grid;
  gap: var(--space-1);
}

.per-page {
  align-items: center;
  display: flex;
  gap: var(--space-1);
}

.per-page-option {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  color: var(--color-muted);
  font-size: var(--text-xs);
  padding: var(--space-1) var(--space-2);
  text-decoration: none;
}

.per-page-option.is-current {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  color: var(--color-primary);
  font-weight: var(--weight-medium);
}

/* --- Массовые действия -------------------------------------- */
.bulk-bar {
  align-items: end;
  background: var(--color-primary-soft);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-panel);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
  padding: var(--space-3);
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}

/* `display: flex` перебивает браузерный `hidden`, поэтому правило явное:
   панель массовых действий появляется только когда что-то отмечено. */
.bulk-bar[hidden] {
  display: none;
}

.bulk-count {
  align-self: center;
  font-size: var(--text-sm);
}

.bulk-field {
  display: grid;
  gap: var(--space-1);
}

.cell-pick {
  width: 36px;
}

.cell-pick input {
  cursor: pointer;
  height: 16px;
  margin: 0;
  width: 16px;
}

/* --- Таблица сделок ------------------------------------------
   Плотность (F-212). Замер до правки на 1280×800: строка 61–80 px и плавала от
   длины содержимого, в экран помещалось 7 лидов. Норма проекта из
   `docs/reference/crm-ux-patterns.md` — строка 44 px фиксированная и 14 строк
   в экран; арифметика простая: (800 − 180) / 44.

   Высота держится тремя вещами сразу, поодиночке ни одна не работает:
   `table-layout: fixed` (иначе ширину колонки диктует самое длинное значение),
   одна строка текста в ячейке и обрезка многоточием. Ширины назначены
   заголовкам: при фиксированной раскладке браузер смотрит только на первый
   ряд. Колонки без своей ширины делят остаток поровну. */
.deals-table {
  font-size: var(--text-sm);
  table-layout: fixed;
}

.deals-table th {
  height: 26px;
  overflow: hidden;
  padding-block: 2px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.deals-table td {
  height: var(--table-row-height);
  overflow: hidden;
  padding-block: 0;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Название сделки и имя клиента — одна строка: имя догоняет название и гаснет
   в многоточии первым, потому что ищут строку глазами по названию. */
.deals-table .cell-lead .lead-title,
.deals-table .cell-lead .muted {
  display: inline;
}

.deals-table .cell-lead .muted::before {
  content: " · ";
}

/* Ширины подобраны по самому длинному реальному значению колонки плюс паддинг,
   а не на глаз: «Квалифицирован» со значком стадии — 160 px, «15.08.2026 06:48»
   моноширинными цифрами — 135 px. Колонка не может быть уже своей самой длинной
   подписи — правило, из-за нарушения которого случился F-211. */
.deals-table .cell-pick {
  text-overflow: clip;
  width: 36px;
}

.deals-table .col-title {
  width: 24%;
}

.deals-table .col-task {
  width: 18%;
}

.deals-table .col-stage {
  width: 160px;
}

.deals-table .col-responsible,
.deals-table .col-source,
.deals-table .col-reason,
.deals-table .col-city {
  width: 120px;
}

.deals-table .col-amount {
  width: 120px;
}

.deals-table .col-phone {
  width: 140px;
}

.deals-table .col-updated,
.deals-table .col-created {
  width: 135px;
}

.col-sort {
  align-items: center;
  color: inherit;
  display: inline-flex;
  gap: var(--space-1);
  text-decoration: none;
}

.col-sort-caret {
  color: var(--color-faint);
  opacity: 0;
}

.col-sort:hover .col-sort-caret,
[aria-sort="ascending"] .col-sort-caret,
[aria-sort="descending"] .col-sort-caret {
  opacity: 1;
}

[aria-sort="ascending"] .col-sort-caret {
  transform: rotate(180deg);
}

[aria-sort="ascending"] .col-sort,
[aria-sort="descending"] .col-sort {
  color: var(--color-primary);
}

.stage-cell {
  align-items: center;
  display: inline-flex;
  gap: var(--space-2);
  white-space: nowrap;
}

.cell-amount {
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

.row-archived {
  opacity: var(--opacity-disabled);
}

/* Архивная строка не ссылка — и выглядеть ссылкой не должна. */
.lead-title--archived {
  color: var(--color-muted);
  cursor: default;
}

/* --- Страницы ------------------------------------------------ */
.pager {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  padding: var(--space-4) 0;
}

.pager-status {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.pager [aria-disabled="true"] {
  opacity: var(--opacity-disabled);
  pointer-events: none;
}

/* --- Экран сотрудников -------------------------------------- */
/* Колонка действий держится по содержимому и не переносится: `display: flex`
   прямо на <td> ломает раскладку таблицы, кнопки вылезали за границу строки. */
.staff-table .cell-actions {
  text-align: right;
  white-space: nowrap;
  width: 1%;
}

.staff-table .cell-actions-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
}

.staff-table .status-pill + .status-pill {
  margin-left: var(--space-1);
}

/* Карточки сотрудников — только на узком экране, таблица только на широком. */
.staff-cards {
  display: none;
}

.staff-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
}

.staff-card-head {
  align-items: start;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
}

.staff-card-head .muted {
  display: block;
}

.staff-card-meta {
  display: grid;
  gap: var(--space-2);
  margin: 0;
}

.staff-card-meta div {
  display: grid;
  gap: 2px;
}

.staff-card-meta dt {
  color: var(--label-color);
  font-size: var(--label-size);
}

.staff-card-meta dd {
  margin: 0;
}

.hint-note {
  align-items: start;
  color: var(--color-muted);
  display: flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  line-height: var(--leading-normal);
  margin: var(--space-3) 0 0;
}

.hint-note .icon {
  flex: none;
  margin-top: 2px;
}

.stack-form {
  display: grid;
  gap: var(--space-3);
}

.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* --- Инбокс непрочитанных (F-053) ----------------------------
   Экран отвечает на «где я нужен прямо сейчас», поэтому ведущая величина —
   не время сообщения, а время ожидания: моно, крупнее строки, первой колонкой.
   Просрочка помечается полосой у края строки, а не заливкой: залитая наполовину
   таблица перестаёт означать тревогу через минуту работы. */

/* Слот значка — flex-элемент пункта меню: `margin-left: auto` на инлайновом
   span схлопнулся бы в ноль, и цифра прилипла бы к слову. */
.nav-badge-slot {
  display: inline-flex;
  margin-left: auto;
}

/* `position: relative` обязателен: подпись для скринридера внутри — это
   `.visually-hidden`, то есть `position: absolute`. Без опоры её содержащим
   блоком становится страница, и на 375px невидимый узел растягивает документ
   до 420px — горизонтальная прокрутка всего экрана из-за того, чего не видно. */
.nav-badge {
  background: var(--color-danger-solid);
  border-radius: var(--radius-pill);
  color: var(--color-on-primary);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  line-height: 1;
  padding: 3px 6px;
  position: relative;
}

/* Вкладки каналов — те же `.view-tabs`/`.view-tab`, что на списке сделок:
   это ровно та же вещь (срез, живущий в адресе), и второго набора классов под
   неё заводить нечего. Здесь только раскладка полосы. */
.inbox-filters {
  border-bottom: 1px solid var(--color-border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}

.inbox-mine { margin-left: auto; }

/* F-230. Режим очереди — те же `.view-tab`, что и каналы, но своей строкой и
   выше их: это более крупный вопрос («что вообще показывать»), а канал —
   срез внутри ответа. Одна полоса на оба уровня превращала бы шесть вкладок
   в восемь равнозначных, и «Все диалоги» терялось бы среди каналов. */
.inbox-modes {
  gap: var(--space-1);
  margin-bottom: var(--space-2);
}

.inbox-search {
  align-items: center;
  display: flex;
  gap: var(--space-1);
  margin-bottom: var(--space-2);
}

.inbox-search input[type="search"] {
  flex: 1;
  min-width: 0;
}

/* Сброс — ссылка, а не третья кнопка: он отменяет действие, а не выполняет
   его, и рядом с «Найти» две одинаковые кнопки заставляли бы выбирать. */
.inbox-search-clear {
  color: var(--color-muted);
  flex-shrink: 0;
  font-size: var(--text-xs);
}

/* «Отвечено» стоит ровно там, где в непрочитанной строке кнопка-галочка, и
   занимает столько же места: иначе строки в режиме «Все» прыгали бы по
   ширине превью в зависимости от того, ответили на диалог или нет. */
.iq-answered {
  color: var(--color-faint);
  flex-shrink: 0;
  font-size: var(--text-2xs);
  white-space: nowrap;
}

/* --- Инбокс: рабочее место в две колонки ----------------------
   Очередь слева остаётся на экране, пока менеджер отвечает справа. Так
   устроены все четыре инбокса из разведки и все мессенджеры; список, который
   исчезает после клика по строке, был главной претензией к прошлой версии
   (docs/reference/inbox-ux.md).

   Высота считается от вьюпорта, как у карточки сделки: обе колонки
   прокручиваются внутри себя, страница целиком — нет. 168px — шапка (72) плюс
   отступы рабочей области (24 сверху и снизу) плюс запас на две строки
   заголовка. */
.inbox-workspace {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: minmax(300px, 360px) minmax(0, 1fr);
  /* Первая строка — слот сообщений; пустой он даёт ноль высоты, но остаётся
     строкой, иначе колонки переезжали бы в неё, когда сообщения нет. */
  grid-template-rows: auto minmax(0, 1fr);
  height: calc(100vh - 168px);
  height: calc(100dvh - 168px);
  min-height: 480px;
}

/* Сообщение после HTMX-действия — во всю ширину рабочей области, а не в одну
   из колонок: иначе оно съедает место очереди. */
.inbox-messages-slot { grid-column: 1 / -1; }

.inbox-queue {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-3) var(--space-3) var(--space-4);
  scrollbar-color: var(--color-border) transparent;
  scrollbar-width: thin;
}

.inbox-queue .inbox-filters {
  margin-bottom: var(--space-2);
  position: sticky;
  top: calc(var(--space-3) * -1);
  z-index: 1;
  background: var(--color-surface);
  padding-top: var(--space-1);
}

.inbox-pane {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-panel);
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

.inbox-pane-blank {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-2);
  justify-content: center;
  padding: var(--space-6);
}

.inbox-pane-blank p { margin: 0; max-width: 52ch; }
.inbox-pane-blank .muted { color: var(--color-muted); font-size: var(--text-sm); }

.inbox-pane-head { gap: var(--space-3); }

.inbox-pane-who {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
}

/* Кружок прижат к ПЕРВОЙ строке имени, а не к середине блока: длинное имя
   занимает три строки, и кружок, выровненный по центру, оказывался напротив
   середины фамилии — далеко от того, что он обозначает. */
.inbox-pane-avatar {
  align-self: flex-start;
  margin-top: 1px;
}

/* Знак канала не жмётся, когда имя длинное: он здесь — ответ на вопрос
   «куда уйдёт сообщение», и сжимать его в пользу имени неверно. */
.inbox-pane-channel { flex-shrink: 0; }

/* Имя и канал — одна строка, переносимая целиком. Знак остаётся при имени на
   любой ширине; `min-width: 0` нужен, чтобы длинное имя жалось, а не растягивало
   шапку и не выдавливало кнопки. */
.inbox-pane-name {
  align-items: center;
  column-gap: var(--space-2);
  display: flex;
  flex-wrap: wrap;
  min-width: 0;
}

.inbox-pane-who .muted {
  color: var(--color-muted);
  font-size: var(--text-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inbox-pane-head .panel-head-actions {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  margin-left: auto;
}

/* Кнопка «к очереди» нужна только там, где очередь ушла с экрана. */
.inbox-back { display: none; }

/* --- Строка очереди -------------------------------------------
   Три строки: кто и сколько ждёт, о чём сделка, что написал. Ссылка растянута
   псевдоэлементом, потому что внутри строки есть кнопка, а форма внутри ссылки
   невалидна. */
.inbox-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.iq-row {
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  cursor: pointer;
  display: grid;
  gap: 2px;
  /* minmax(0,1fr), а не 1fr: у грид-элемента min-width по умолчанию auto, и
     длинная реплика клиента распирала бы колонку, включая горизонтальную
     прокрутку списка (та же грабля, что у канбана — layout-rules.md). */
  grid-template-columns: minmax(0, 1fr);
  padding: var(--space-2) var(--space-3);
  position: relative;
  transition: background var(--transition-fast);
}

.iq-head,
.iq-line { min-width: 0; }

.iq-row:hover { background: var(--state-hover); }

.iq-row.is-current {
  background: var(--state-selected);
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
}

.iq-head {
  align-items: baseline;
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
}

.iq-open {
  color: var(--color-text);
  font-weight: var(--weight-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.iq-open::after {
  content: "";
  inset: 0;
  position: absolute;
}

.iq-open:hover { text-decoration: none; }
.iq-open:focus-visible { border-radius: var(--radius-sm); box-shadow: var(--focus-ring); outline: none; }

.iq-waiting {
  color: var(--color-muted);
  flex-shrink: 0;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.iq-row--overdue .iq-waiting {
  color: var(--color-danger-text);
  font-weight: var(--weight-semibold);
}

/* Три строки на диалог и ни одной лишней: на 900px высоты помещается десять
   штук, а очередь на день — это как раз десяток-полтора. */
.iq-line {
  align-items: center;
  display: flex;
  gap: var(--space-2);
  min-height: 22px;
}

.iq-deal,
.iq-preview {
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.iq-deal { color: var(--color-faint); flex: 1; font-size: var(--text-xs); }
.iq-preview { color: var(--color-muted); flex: 1; font-size: var(--text-sm); }

/* Метка черновика — как в списке чатов Telegram: видно, что ответ начат. */
.iq-draft { color: var(--color-warning-text); font-weight: var(--weight-medium); }

/* Неотправленное видно и из очереди: ответ, который не ушёл, — это работа,
   а не черновик, и он не должен обнаруживаться только внутри диалога. */
.iq-draft--failed { color: var(--color-danger-text); }

.iq-flag {
  align-items: center;
  color: var(--color-danger-text);
  display: flex;
  font-size: var(--text-2xs);
  gap: var(--space-05);
  white-space: nowrap;
}

/* «Разговор продолжился в другом месте» (F-145). Сознательно тише соседнего
   `.iq-flag`: тот кричит «забыли больше суток», а этот, наоборот, снимает
   тревогу — работа идёт, просто не здесь. Отсюда приглушённый цвет вместо
   красного и та же строка, что у названия сделки: это уточнение к диалогу,
   а не отдельное событие. Обрезается многоточием, чтобы длинное название
   канала не ломало три строки на диалог. */
.iq-moved {
  align-items: center;
  color: var(--color-muted);
  display: flex;
  flex-shrink: 1;
  font-size: var(--text-2xs);
  gap: var(--space-05);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.iq-count {
  background: var(--color-primary);
  border-radius: var(--radius-pill);
  color: var(--color-on-primary);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-semibold);
  line-height: 1;
  padding: 3px 7px;
}

/* Кнопка лежит поверх растянутой ссылки, иначе клик по ней открывал бы диалог. */
.iq-act {
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}

.iq-read {
  align-items: center;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  color: var(--color-muted);
  cursor: pointer;
  display: flex;
  height: 26px;
  justify-content: center;
  width: 30px;
}

.iq-read:hover {
  background: var(--color-success-bg);
  border-color: var(--color-success-border);
  color: var(--color-success-text);
}

.iq-read:focus-visible { box-shadow: var(--focus-ring); outline: none; }

.inbox-tail {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: var(--space-3) 0 0;
  padding: 0 var(--space-3);
}

.inbox-empty {
  color: var(--color-muted);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-3);
}

.inbox-empty h3 { color: var(--color-text); font-size: var(--text-md); margin: 0; }
.inbox-empty p { margin: 0; }
.inbox-empty .secondary-button { align-self: flex-start; }

/* --- Фильтр и охват ленты (F-025) ---------------------------
   Живут внутри прокручиваемой ленты и прилипают к её верху: блок
   перерисовывается опросом каждые 5 с, и счётчики обязаны оставаться живыми. */

.tl-controls {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: space-between;
  margin: 0 calc(var(--space-4) * -1);
  padding: var(--space-2) var(--space-4);
  position: sticky;
  /* Ноль, а не отрицательный отступ: при отрицательном верх панели уезжает за
     край прокрутки, и над кнопками видна полоска чужого сообщения. */
  top: 0;
  z-index: 1;
}

.tl-scope,
.tl-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-05);
}

.tl-scope-btn,
.tl-filter-btn {
  align-items: center;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  color: var(--color-muted);
  display: inline-flex;
  font-size: var(--text-2xs);
  gap: var(--space-1);
  line-height: 1;
  padding: 5px var(--space-2);
  text-decoration: none;
  transition: background var(--transition-fast), color var(--transition-fast);
}

/* На устройстве без мыши цель нажатия — палец, а не курсор. Замер на 375 px:
   фильтры ленты 23 px в высоту, ступени воронки 30 px при норме 44. Растёт
   только область нажатия, кегль и вид не меняются — на мыши всё как было.
   Запрос по `hover: none`, а не по ширине: узкое окно на ноутбуке остаётся
   мышиным, а планшет в 1024 px — пальцевым. */
@media (hover: none) {
  .tl-filter-btn { min-height: 36px; padding: 5px var(--space-3); }
  .stage-rail-step { min-height: 40px; }
}

.tl-scope-btn:hover,
.tl-filter-btn:hover {
  background: var(--state-hover);
  color: var(--color-text);
  text-decoration: none;
}

.tl-scope-btn.is-current,
.tl-filter-btn.is-current {
  background: var(--color-surface-sunken);
  border-color: var(--color-border);
  color: var(--color-text);
  font-weight: var(--weight-semibold);
}

.tl-filter-count {
  color: var(--color-faint);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.tl-filter-btn.is-current .tl-filter-count { color: var(--color-muted); }

.tl-filter-notice {
  color: var(--color-muted);
  font-size: var(--text-2xs);
  margin: 0 0 var(--space-2);
}

/* Подпись сделки в ленте по клиенту — разделитель разговоров, а не заголовок
   строки: ставится только там, где сделка сменилась. */
.tl-deal-mark {
  align-items: center;
  color: var(--color-faint);
  display: flex;
  font-size: var(--text-2xs);
  gap: var(--space-2);
  letter-spacing: 0.04em;
  margin: var(--space-2) 0 0;
  text-transform: uppercase;
}

.tl-deal-mark::after {
  background: var(--color-border);
  content: "";
  flex: 1;
  height: 1px;
}

/* --- Адаптив ------------------------------------------------- */

/* Ниже 1024px две колонки не помещаются: рабочая область на 768px — 736px, и
   очередь с перепиской делили бы её пополам, оставляя чату 380px. Поэтому
   переключаемся на одну колонку и показываем то, что менеджер выбрал: список
   или диалог. Признак «диалог открыт» приезжает с сервера классом is-open,
   клиентского состояния тут нет. */
@media (max-width: 1023px) {
  .inbox-workspace {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    height: auto;
    min-height: 0;
  }

  .inbox-workspace .inbox-queue {
    max-height: none;
    overflow-y: visible;
  }

  /* Прокрутки внутри колонки здесь нет, и sticky начал бы считаться от
     вьюпорта: панель каналов прилипала бы к верху экрана, уехав за край. */
  .inbox-queue .inbox-filters { position: static; }

  .inbox-workspace.is-open .inbox-queue { display: none; }
  .inbox-workspace:not(.is-open) .inbox-pane { display: none; }

  .inbox-workspace.is-open .inbox-pane {
    height: calc(100vh - 200px);
    height: calc(100dvh - 200px);
    min-height: 420px;
  }

  /* Возврат в очередь — единственный выход с открытого диалога на телефоне,
     и он же был самой мелкой целью экрана: замер 23.08.2026 дал 20 px. */
  .inbox-back {
    align-items: center;
    display: inline-flex;
    gap: var(--space-1);
    font-size: var(--text-sm);
    margin-left: calc(var(--space-2) * -1);
    min-height: 40px;
    padding: 0 var(--space-2);
  }

  /* Палец, а не курсор: 44px — минимум для касания (Apple HIG, Material). */
  .iq-read { height: 40px; width: 44px; }
  .iq-row { padding: var(--space-3); }
}

@media (max-width: 1200px) {
  .deals-table .col-created,
  .deals-table .col-source {
    display: none;
  }
}

/* Колонки фиксированной ширины сжиматься не умеют: на узком окне первым уходит
   ответственный — в списке заказов у менеджера он один и тот же во всех
   строках. Иначе адресу доставки не остаётся ничего и таблица едет вбок. */
@media (max-width: 1100px) {
  .orders-table .col-owner,
  .orders-table td:nth-child(6) {
    display: none;
  }
}

@media (max-width: 768px) {
  /* Таблица на телефоне не сжимается, а уступает место карточкам: десять
     колонок в горизонтальной прокрутке не читаются. */
  .table-scroll--deals,
  .bulk-bar {
    display: none;
  }

  .inbox-mine { margin-left: 0; }

  /* Шапка диалога на телефоне складывается: «назад» и кто это — первой
     строкой, действия — второй, иначе кнопка «Прочитано» уезжает за край. */
  .inbox-pane-head { flex-wrap: wrap; }
  .inbox-pane-head .panel-head-actions { margin-left: 0; width: 100%; }

  /* Внешний вид карточки живёт здесь, а не на 720px вместе с остальной
     раскладкой телефона: карточки включаются на 768, и на планшете они
     оставались без отступов и с подписями столбиком (F-219). */
  .mobile-lead-card {
    display: grid;
    gap: var(--space-2);
    padding: var(--space-3);
  }

  .mobile-lead-header {
    display: grid;
    gap: var(--space-1);
  }

  /* Подпись и значение в один ряд, а не столбиком (F-212): три пары подряд
     давали карточку в 285 px, и на экран телефона помещались две сделки.
     Подпись слева фиксированной колонкой — значения выстраиваются по одной
     вертикали и читаются сверху вниз одним движением глаз. */
  .mobile-lead-meta {
    display: grid;
    gap: var(--space-1);
    margin: 0;
  }

  .mobile-lead-meta div {
    align-items: baseline;
    display: grid;
    gap: var(--space-2);
    grid-template-columns: 9rem minmax(0, 1fr);
  }

  .mobile-lead-meta dd {
    margin: 0;
    min-width: 0;
  }

  /* Карточки списка включаются ровно там, где выключается таблица: общее
     правило .mobile-lead-list стоит на 720px, и между 721 и 768 не осталось бы
     ни таблицы, ни карточек. */
  .deal-bulk-form .mobile-lead-list {
    display: grid;
    gap: var(--space-3);
  }

  /* Заказы — тот же порог и та же причина (F-219): шесть колонок в
     горизонтальной прокрутке на 768 не читаются. */
  #orders-result .table-panel {
    display: none;
  }

  #orders-result .mobile-lead-list {
    display: grid;
    gap: var(--space-3);
  }

  .deal-filters-actions {
    margin-left: 0;
    width: 100%;
  }

  /* Разделитель между «мои/тип» и раскрывающимися чипами: на узком экране ряд
     переносится, и он повисает висячей чёрточкой в конце строки. */
  .deal-filters-sep {
    display: none;
  }

  /* Поповер, привязанный к своему чипу, на 375 не помещается: у «Ещё фильтров»
     левый край уезжал на −181px, то есть за экран. Здесь опорой служит вся
     панель фильтров, а не чип: поповер выходит под ней во всю её ширину.
     Ставится это сменой того, кто является содержащим блоком, — координат на
     клиенте по-прежнему никто не считает. */
  .deal-filters {
    position: relative;
  }

  .filter-pop {
    position: static;
  }

  .filter-pop-body,
  .filter-pop--wide .filter-pop-body {
    left: 0;
    max-width: none;
    right: 0;
    width: auto;
  }

  .deal-filters-actions > * {
    flex: 1 1 auto;
    justify-content: center;
  }

  .filter-check-list {
    max-height: none;
  }

  .menu-drop-body {
    left: 0;
    right: auto;
    width: min(280px, calc(100vw - var(--space-6)));
  }

  .table-scroll--staff {
    display: none;
  }

  .staff-cards {
    display: grid;
    gap: var(--space-3);
  }

  .cell-actions-row {
    justify-content: flex-start;
  }
}
/* ↑ Закрытие @media (max-width: 768px). Скобку потеряли, когда дописывали блок
   оплат ниже: всё, что шло после, оказывалось внутри медиазапроса и на
   десктопе не применялось вовсе — ни оплаты, ни долг сделки. Найдено
   браузером при вёрстке задач (F-034…F-038). */

/* ── Оплаты по заказу (F-094) ─────────────────────────────
   Менеджер смотрит сюда, разговаривая по телефону: главное — остаток, он
   набран моно и крупно, остальное намеренно тихое. Строка платежа — не
   карточка: панель уже карточка, вторая вложенность запрещена правилом
   области. Тип платежа продублирован словом, а не только цветом.
   Шрифт сумм — базовый, как у «Итого к оплате» рядом: три суммы одной панели
   обязаны выглядеть одним рядом чисел, а не двумя гарнитурами. Выравнивание
   держит tabular-nums. */
.money-line {
  font-variant-numeric: tabular-nums;
}

.money-remainder dd {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.money-remainder .order-price {
  color: var(--color-danger-text);
  font-variant-numeric: tabular-nums;
}

.money-remainder.is-settled .order-price {
  color: var(--color-success-text);
}

.status-pill.success {
  background: var(--color-success-bg);
  color: var(--color-success-text);
}

.payment-list {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: 0;
  padding: 0;
}

.payment-row {
  border-top: 1px solid var(--color-border);
  padding: var(--space-3) 0;
}

.payment-row:first-child {
  border-top: 0;
}

.payment-row__head {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: space-between;
}

.payment-row__sum {
  font-size: var(--text-md);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.payment-row--refund .payment-row__sum {
  color: var(--color-danger-text);
}

.payment-row__kind {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: 500;
}

.payment-row__meta {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin-top: var(--space-05);
}

.payment-row__comment {
  margin: var(--space-1) 0 0;
  overflow-wrap: anywhere;
}

.payment-row__delete {
  align-items: center;
  background: none;
  border: 0;
  color: var(--color-muted);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-sm);
  gap: var(--space-1);
  margin-top: var(--space-2);
  min-height: 32px;
  padding: 0;
}

.payment-row__delete:hover,
.payment-row__delete:focus-visible {
  color: var(--color-danger-text);
}

/* Отделение формы от списка платежей; раскладку полей даёт .form-stack. */
.payment-form {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
}

/* Долг клиента в карточке сделки: одна строка, читается за полсекунды. */
.deal-debt {
  align-items: baseline;
  display: flex;
  gap: var(--space-2);
  justify-content: space-between;
}

.deal-debt__value {
  color: var(--color-danger-text);
  font-size: var(--text-md);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

/* ─── Задачи и активности (F-034…F-038, переработано F-211/F-212) ────────────
   Список задач — не таблица: у строки две колонки смысла (что сделать и когда),
   а не восемь. На 375 px строка складывается в столбец, действия уезжают вниз.

   Две нормы из `docs/reference/crm-ux-patterns.md` держат этот блок.

   1. Один красный объект на строку, и он несёт число дней. Было три —
      слово «Просрочена», красная рамка и красная подложка; при пяти задачах
      экран становился светофором. Остался знак `.task-overdue`.
   2. Строка 56 px, одиннадцать штук в экран 1280×800. Было 118 px, потому что
      кнопки «Закрыть» и «Перенести» стояли третьей строкой у каждой задачи.
      Теперь они появляются по наведению и по фокусу с клавиатуры, а на
      сенсорном экране (`hover: none`) видны всегда — иначе до них не добраться.

   Ширину заголовку больше никто не отнимает: состояние ушло на строку
   подписи, поэтому `.task-title` переносится по словам, а не по слогам. */

.task-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.task-row {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  position: relative;
}

.task-row-main {
  align-items: flex-start;
  display: flex;
  gap: var(--space-3);
}

.task-type {
  color: var(--color-muted);
  flex: 0 0 auto;
  padding-top: 2px;
}

.task-body {
  flex: 1 1 auto;
  min-width: 0;
}

/* 🔴 `overflow-wrap: anywhere` здесь было и было причиной F-211: оно молча
   соглашалось на любую ширину и кололо «Проверить» на «Прове/рить». Перенос
   внутри слова разрешён только как аварийный выход, когда одно слово длиннее
   всей колонки, — за это отвечает `break-word`. Если заголовок снова начнёт
   ломаться по слогам, чинить надо колонку, а не эту строку. */
.task-title {
  display: -webkit-box;
  font-weight: var(--weight-semibold);
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  margin: 0;
  overflow: hidden;
  overflow-wrap: break-word;
  text-wrap: pretty;
}

.task-meta,
.task-result {
  color: var(--color-muted);
  font-size: var(--text-sm);
  margin: var(--space-05) 0 0;
  overflow-wrap: break-word;
}

/* Единственный красный объект строки. Иконка и число делают его понятным без
   цвета; полная фраза приходит скринридеру из `.visually-hidden`. */
.task-overdue {
  align-items: center;
  color: var(--color-danger-text);
  display: inline-flex;
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-semibold);
  gap: var(--space-1);
  white-space: nowrap;
}

/* Сделка, клиент и исполнитель — одна строка, а не три: общий класс .muted
   в проекте блочный, здесь он превратил бы подпись в столбик. */
.task-meta .task-meta-part {
  color: var(--color-muted);
  display: inline;
  font-size: inherit;
  margin: 0;
}

.task-result {
  color: var(--color-text);
}

.task-when {
  align-items: flex-end;
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  gap: var(--space-1);
}

.task-due {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.status-overdue {
  align-items: center;
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
  gap: var(--space-1);
}

.task-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ── Доска задач: строка 56 px, действия по требованию (F-212) ───────────────
   Замер до правки: 118 px на строку, 5 задач в экран 1280×800. Третью строку
   занимали кнопки «Закрыть» и «Перенести», видимые у каждой задачи сразу.

   Прятать их можно только так, чтобы они остались достижимы с клавиатуры:
   `display: none` и `visibility: hidden` убирают кнопку из порядка обхода, и
   до неё нельзя дойти табом — тогда `:focus-within` не сработает никогда.
   Поэтому прозрачность плюс `pointer-events`: кнопка остаётся в потоке фокуса
   и проявляется в тот момент, когда фокус на неё встаёт. */
/* Доска — список со строками, а не стопка карточек: восемь пикселей зазора
   между одиннадцатью задачами это ещё две строки работы, которых не видно.
   Разделитель 1 px, как в норме проекта; зебры нет — при 56 px она рябит. */
.task-list:not(.task-list--card) {
  gap: 0;
}

/* Список примыкает к ряду вкладок: его нижняя линия и есть верхняя граница
   списка. Своей рамки у списка нет намеренно — рамка внутри панели это
   карточка в карточке, брак из списка заказчика. */
#tasks-result .view-tabs {
  margin-bottom: 0;
}

/* 56 px — это вся коробка строки вместе с разделителем (`box-sizing: border-box`).
   Отступы намеренно меньше высоты: высоту задаёт `min-height`, иначе она
   плавала бы от того, есть ли у задачи результат или запись разговора. */
.task-list:not(.task-list--card) .task-row {
  border: 0;
  border-radius: 0;
  justify-content: center;
  min-height: 56px;
  padding-block: var(--space-1);
}

.task-list:not(.task-list--card) .task-row + .task-row {
  border-top: 1px solid var(--color-border);
}

.task-list:not(.task-list--card) .task-row:hover {
  background: var(--color-surface-sunken);
}

/* Две строки в 56 px помещаются только с явной высотой строки: наследуемые
   1,5 от корпуса давали 63 px и норму не выполняли. */
.task-list:not(.task-list--card) .task-title {
  line-height: 1.3;
}

.task-list:not(.task-list--card) .task-meta {
  line-height: 1.35;
}

/* На доске заголовок в одну строку: вторая занята сделкой и клиентом, а
   вместе они и есть те 56 px. Полный текст — в подсказке `title`. */
.task-list:not(.task-list--card) .task-title {
  -webkit-line-clamp: 1;
  line-clamp: 1;
}

.task-list:not(.task-list--card) .task-actions {
  background: var(--color-surface);
  border-radius: var(--radius-control);
  box-shadow: -16px 0 16px -8px var(--color-surface);
  opacity: 0;
  padding-left: var(--space-3);
  pointer-events: none;
  position: absolute;
  right: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  transition: opacity var(--transition-fast);
}

.task-list:not(.task-list--card) .task-row:hover .task-actions,
.task-list:not(.task-list--card) .task-row:focus-within .task-actions {
  opacity: 1;
  pointer-events: auto;
}

/* Пальцем навести нельзя. Там, где наведения не существует, действия стоят на
   месте и видны всегда — иначе задачу нельзя закрыть с телефона. */
@media (hover: none) {
  .task-list:not(.task-list--card) .task-actions {
    box-shadow: none;
    opacity: 1;
    padding-left: 0;
    pointer-events: auto;
    position: static;
    transform: none;
  }

  .task-list:not(.task-list--card) .task-row {
    min-height: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .task-list:not(.task-list--card) .task-actions {
    transition: none;
  }
}

/* Кнопка «Перенести» — часть ряда действий, а не секция: у инлайновой
   раскрывашки в проекте есть верхняя линия, здесь она читалась бы обрывком
   разделителя посреди строки. */
.task-actions .disclosure--inline {
  border-top: 0;
  margin: 0;
}

.task-actions .disclosure--inline .disclosure-summary {
  min-height: 36px;
}

/* Постановка задачи стоит первой в панели, и верхняя линия раскрывашки читалась
   бы здесь оборванным разделителем: отделять нечего, выше только заголовок. */
.task-new.disclosure--inline {
  border-top: 0;
  margin-top: 0;
}

/* Ширину селекта диктует самый длинный вариант, а `.form-grid` ставит только
   `max-width: --field-max` (480px). На 375 панель 341px, и «Сделка» вылезала за
   её край на 47px — названия сделок длинные. Замер 14.08.2026. */
.task-new .form-grid select,
.task-new .form-grid input {
  width: 100%;
}

/* Форма постановки задачи в колонке карточки (F-152). Замер до правки на 1280:
   колонка 249px, поле «Задача» шириной 182px с правым краем на 1292px — то есть
   на 62px за краем своей панели и за краем окна. Причина — два поля в ряд
   (.form-grid) там, где на поле остаётся ~110px, и растянутая по ширине рамка
   поля. Здесь одна колонка, подпись над полем, контрол ровно по месту.
   `min-width: 0` обязателен: у grid-элемента он по умолчанию `auto`, и поле не
   сжимается меньше своего содержимого, а вылезает наружу. */
/* .form-stack — общий класс той же болезни (19.08.2026, третий случай:
   форма платежа в рейле заказа). Правило из map/frontend.md исполнено:
   не четвёртый одноразовый, а общий; .task-form остаётся синонимом. */
.task-form,
.form-stack {
  display: grid;
  gap: var(--space-3);
}

.task-form-field,
.form-stack > div {
  display: grid;
  gap: var(--space-1);
  margin: 0;
  min-width: 0;
}

.task-form-field > label,
.form-stack label {
  color: var(--label-color);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
}

.task-form input,
.task-form select,
.form-stack input,
.form-stack select {
  max-width: 100%;
  min-width: 0;
  width: 100%;
}

.task-form .primary-button,
.form-stack .primary-button {
  justify-content: center;
  width: 100%;
}

/* Быстрая постановка задачи (F-205): форма открыта всегда и стоит над списком,
   поэтому её нужно отделить от задач — иначе поле ввода читается как часть
   первой строки списка. Линия, а не отступ: отступ в панели уже занят под
   разделение строк задач между собой. */
.task-quick {
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
}

/* Первый ярус формы — «что сделать» и «когда»; разделительной линии между ними
   не нужно, легенда «Срок» и так называет группу. Линия здесь читалась бы
   оборванным разделителем: выше только одно поле. */
.task-quick .preset-fieldset--first {
  border-top: 0;
  margin-top: 0;
  padding-top: 0;
}

/* Тип, исполнитель и точное время — под раскрывашкой: у всех трёх есть
   умолчание, и в быстрой постановке их не трогают. Верхняя линия достаётся от
   `.disclosure--inline` и работает здесь по назначению — отделяет
   необязательное от обязательного. */
.task-form-more-body {
  display: grid;
  gap: var(--space-3);
  padding-top: var(--space-3);
}

/* Подсказка про Enter: клавиатурный путь должен быть виден, а не угадываться
   (заказчик ставит задачи между звонками и от мыши отрывается). */
.task-form-hint {
  color: var(--color-muted);
  font-size: var(--text-xs);
  margin: 0;
}

/* Причина отказа формы, отправленной по HTMX (F-202). Область постоянная и
   пустая до первого отказа: `:empty` убирает и её саму, и зазор грида, иначе
   под кнопкой всё время висела бы пустая строка. Значок обязателен — цветом
   одним сообщать нельзя; токены опасности те же, что у остальных ошибок. */
.form-inline-errors {
  display: grid;
  gap: var(--space-2);
}

.form-inline-errors:empty {
  display: none;
}

/* Цвета не повторяются: разметка ставит `.form-error form-inline-error`, и
   токены опасности остаются в одном месте — здесь только раскладка под узкую
   колонку (значок слева, шрифт мельче, отступ меньше). */
.form-inline-error {
  align-items: flex-start;
  display: flex;
  font-size: var(--text-sm);
  gap: var(--space-2);
  line-height: 1.4;
  margin: 0;
  padding: var(--space-2);
}

/* Значок не сжимается и не уезжает вниз при переносе текста на три строки —
   в колонке карточки 250px это обычный случай, а не край. */
.form-inline-error svg {
  flex-shrink: 0;
  margin-top: 1px;
}

/* Пресеты срока и результата: касание ставится в два клика, календарь — запас. */
.preset-fieldset {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin: var(--space-3) 0 0;
  padding: var(--space-3) 0 0;
}

.preset-fieldset legend {
  color: var(--color-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  padding: 0;
  text-transform: uppercase;
}

.preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.preset-chip {
  align-items: center;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-full);
  cursor: pointer;
  display: inline-flex;
  font-size: var(--text-sm);
  gap: var(--space-1);
  min-height: 36px;
  /* 8px по бокам, а не 12: на 12px «Через 2 дня» и «Через неделю» вместе дают
     226px при 217px свободных в колонке карточки и уезжают на третий ряд. */
  padding: 0 var(--space-2);
  position: relative;
  /* Перенос подписи внутри чипа — брак по списку приёмки: «Через неделю» в две
     строки заказчик уже забраковывал. Не влезает — переносится ряд, не слово. */
  white-space: nowrap;
}

/* Кружок радио внутри чипа лишний: выбор и так виден заливкой и рамкой, а
   стоил он 17px ширины на чип — ровно те, из-за которых четыре срока не
   помещались в колонку карточки и вытягивались в столбик (F-152). Поле
   остаётся в дереве доступности и под фокусом: прозрачное, а не `display:none`,
   и обводку фокуса рисует `.preset-chip:has(input:focus-visible)` ниже. */
.preset-chip input {
  height: 100%;
  left: 0;
  margin: 0;
  opacity: 0;
  position: absolute;
  top: 0;
  width: 100%;
}

.preset-chip:hover {
  background: var(--state-hover);
}

.preset-chip:has(input:checked) {
  background: var(--color-primary-soft);
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.preset-chip:has(input:focus-visible) {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* В режиме высокой контрастности система подменяет фон и цвет рамки своими, и
   выбранный чип перестал бы отличаться от невыбранного: кружок радио, который
   раньше это показывал, здесь спрятан. Обводка системным цветом переживает
   подмену. */
@media (forced-colors: active) {
  .preset-chip:has(input:checked) {
    outline: 2px solid;
    outline-offset: -3px;
  }
}

.preset-exact {
  margin: var(--space-3) 0 0;
}

.preset-exact label {
  color: var(--color-muted);
  display: block;
  font-size: var(--text-sm);
}

.task-close-form {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  width: 100%;
}

.task-close-free {
  margin: var(--space-3) 0 0;
}

.task-close-free input {
  max-width: var(--field-max, 480px);
  width: 100%;
}

.task-close-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.field-hint {
  color: var(--color-muted);
  display: block;
  font-size: var(--text-xs);
  margin-top: var(--space-1);
}

/* Напоминания: строка «пора вернуться», а не всплывашка поверх работы. */
.reminder-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.reminder-list li {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.view-tabs-spacer {
  flex: 1 1 auto;
}

/* Сужение «чьи задачи» (F-151). Вкладки отвечают на «за какой срок», это — на
   «чьи»; два вопроса в одном ряду обязаны выглядеть по-разному, иначе читаются
   как один. Отсюда сегментер в рамке вместо пятой вкладки с подчёркиванием.
   Прежняя ссылка подписывалась тем, КУДА ведёт, — по такой подписи текущее
   состояние не прочитать, и заказчик не нашёл задачу, поставленную коллеге. */
.scope-switch {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  display: inline-flex;
  margin-bottom: var(--space-2);
  overflow: hidden;
}

.scope-option {
  color: var(--color-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  padding: var(--space-1) var(--space-3);
  text-decoration: none;
  transition: background var(--transition-fast), color var(--transition-fast);
  white-space: nowrap;
}

.scope-option + .scope-option {
  border-left: 1px solid var(--color-border);
}

.scope-option:hover {
  background: var(--state-hover);
  color: var(--color-text);
}

.scope-option.is-current {
  background: var(--state-selected);
  color: var(--color-primary);
  font-weight: var(--weight-semibold);
}

/* Строка «список сужен». Пустой экран человек читает как «функция сломана»:
   поэтому здесь не намёк цветом, а предложение с числом и выходом одним
   кликом. Показывается только когда сужение реально что-то прячет. */
.scope-notice {
  align-items: center;
  background: var(--color-primary-soft);
  border-radius: var(--radius-control);
  color: var(--color-text);
  display: flex;
  flex-wrap: wrap;
  font-size: var(--text-sm);
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
}

.scope-notice .icon {
  color: var(--color-primary);
  flex: none;
}

.nav-badge {
  background: var(--color-danger-solid);
  border-radius: var(--radius-full);
  color: var(--color-on-primary);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  margin-left: auto;
  min-width: 20px;
  padding: 2px 6px;
  text-align: center;
}

@media (max-width: 720px) {
  /* Переноса здесь быть не должно: с ним значок типа уезжал на отдельную
     строку над заголовком, а срок — на третью, и строка задачи на телефоне
     вырастала до 139 px. Ряд остаётся рядом, сжимается тело задачи. */
  .task-row-main {
    flex-wrap: nowrap;
  }

  .task-when {
    align-items: flex-end;
    flex: 0 0 auto;
  }

  .task-due {
    white-space: nowrap;
  }

  .task-actions > * {
    flex: 1 1 auto;
  }
}

/* ---------------------------------------------------------------------------
   Дубли контактов и телефон из переписки (F-055, F-144).
   Тон один — рабочий: экран разбора, а не тревоги. Цветом выделяется только
   то, по чему надо принять решение: строка с расхождением и предупреждение
   в карточке. Совпавшие значения остаются тихими.
   --------------------------------------------------------------------------- */

.contact-duplicate-note {
  align-items: flex-start;
  background: var(--color-warning-bg);
  border-radius: var(--radius-input);
  color: var(--color-warning-text);
  display: flex;
  font-size: var(--text-xs);
  gap: var(--space-2);
  line-height: 1.45;
  margin: var(--space-3) 0 0;
  padding: var(--space-2) var(--space-3);
}

.contact-duplicate-note a {
  color: inherit;
}

.ghost-button--inline {
  font-size: var(--text-2xs);
  height: auto;
  margin-left: var(--space-2);
  padding: var(--space-05) var(--space-2);
}

.dup-intro,
.merge-intro {
  margin: 0 0 var(--space-4);
  max-width: 68ch;
}

.dup-table td,
.merge-table td,
.merge-table th[scope="row"] {
  vertical-align: top;
}

.dup-actions {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Вторая карточка пары отделяется линией: без неё два имени в одной ячейке
   читаются как одна запись из двух строк. */
.dup-party + .dup-party {
  border-top: 1px dashed var(--color-border);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
}

.dup-party .muted {
  display: block;
  font-size: var(--text-xs);
}

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

/* Расхождение — единственное, что здесь требует внимания. Не рамка и не тень:
   заливка строки, чтобы глаз шёл по таблице, а не по украшениям. */
.merge-row--conflict > * {
  background: var(--color-warning-bg);
}

.merge-choice {
  align-items: baseline;
  cursor: pointer;
  display: flex;
  gap: var(--space-2);
}

.merge-fills {
  color: var(--color-text);
}

.merge-deals {
  border-top: 1px solid var(--color-border);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}

.merge-actions {
  align-items: center;
  border-top: 1px solid var(--color-border);
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}

.merge-error {
  align-items: center;
  color: var(--color-danger-text);
  display: flex;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
}

/* Отмена заказа (F-030). Панель узкая намеренно: это не рабочий инструмент, а
   аварийный выход, и он не должен спорить за место с составом и деньгами.
   Причина — textarea во всю ширину панели, кнопка под ней: причину пишут
   фразой, а кнопка рядом с полем в одну строку провоцирует клик до текста. */
.order-cancel-panel form,
.order-cancelled-note {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: flex-start;
}

.order-cancel-panel label {
  font-size: var(--text-2xs);
  color: var(--color-muted);
}

.order-cancel-panel textarea {
  width: 100%;
  resize: vertical;
}

.order-cancelled-note {
  border-left: 3px solid var(--color-danger-border);
}

/* Исход сделки в списке (F-127). Цветом — только «продано» и «отказ»: это
   вывод, а «в работе» и «неизвестно» — ещё не вывод, и красить их значило бы
   спорить с колонкой стадии за внимание. «Неизвестно» приглушено намеренно —
   это дырка в данных, а не результат. */
.cell-outcome--won { color: var(--color-success-text); }
.cell-outcome--lost { color: var(--color-danger-text); }
.cell-outcome--unknown { color: var(--color-muted); }
