/*
 * Buenos CRM — app shell (2026-06-24 nav redesign).
 *
 * Replaces the horizontal navbar + mega-menu dropdowns with a thin navy HEADER
 * (brand · club switcher · client search · BandejaGPT · profile) + a vertical
 * left SIDEBAR rendered from /api/menu (menu.js:renderSidebar), collapsible to an
 * icon rail. Refined-minimalist on purpose: airy padding, hairline borders, the
 * brand palette only (var(--bp-*), brand-tokens.css). Bootstrap 5 + Alpine, no
 * SPA. The legacy .navbar/.mega-menu rules in styles.css/crm-theme.css are now
 * inert (no .navbar element) — left in place, safe to delete in a later cleanup.
 */

:root {
  --app-header-h: 52px;
  --app-sidebar-w: 204px;
  --app-sidebar-rail-w: 60px;
}

/* ───────────────────────── Header ───────────────────────── */
.app-header {
  position: sticky;
  top: 0;
  z-index: 1030; /* below chat dock (1050) / improve FAB (1050) */
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--app-header-h);
  padding: 0 12px;
  background: var(--bp-blue);
  color: #fff;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
  font-family: var(--ff-body);
}
.app-brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: #fff;
  font-family: var(--ff-display);
  font-size: 1rem;
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;
}
.app-brand:hover { color: #fff; opacity: 0.92; }
.app-brand .bi { font-size: 1.1rem; }
/* Календарь сайдбара приходит готовым из Jinja (`base.html` +
   web/utils/sidebar_calendar.py), и резерв ему больше не нужен — сетка есть
   уже в первом кадре.
   Правило оставлено СТРАХОВКОЙ на один случай: браузер держит закешированный
   старый `base.html` без разметки календаря, и его собирает `menu.js`. Тогда
   без резерва повторяется прежнее: календарь занимает 16px вместо 200 и на
   КАЖДОМ переходе между разделами роняет меню под собой на 184px (CRM
   многостраничная, страница строится заново каждый раз).
   200px — высота месячной сетки, замерено на живой странице. */
#sidebar-calendar {
  min-height: 200px;
}

/* Слоты левой части шапки: держат место, пока переключатель клуба и поиск едут
   со своими запросами. Иначе всё правее них — поиск, BandejaGPT, «Посмотреть
   как…» — уезжает вбок, когда виджеты приходят.
   ⚠️ Включаются КЛАССОМ на <html>, который ставит скрипт в <head> по данным
   прошлого визита (`base.html`). Безусловный резерв тут вреден: у роли без
   клубов или без права `clients` виджета не будет никогда, и слот навсегда
   останется дырой — так и вышло в первой версии.
   У клуба `min-width`, а не фиксация: длинное имя должно раздвинуть слот, а не
   наехать на поиск. Только широкий экран — на узком у шапки своя раскладка. */
@media (min-width: 992px) {
  .app-header .header-slot {
    display: flex;
    align-items: center;
  }
  .has-club-switcher .app-header .header-slot--club {
    flex: 0 0 auto;
    min-width: 235px;
  }
  .has-client-search .app-header .header-slot--search {
    flex: 0 0 232px;
    width: 232px;
  }
}

/* На узких десктопах поиску отводится меньше: с полным слотом шапка не
   помещалась и выезжала за край экрана — весь документ получал
   горизонтальную прокрутку, страница ездила вбок на каждой ширине от 1024 до
   1350px. Уступает именно поиск: он единственный тут тянущийся, у остальных
   ширина по содержимому.
   Резерв остаётся ФИКСИРОВАННЫМ внутри своей ширины экрана — то есть он
   по-прежнему занят с первого кадра и сдвига не добавляет. */
@media (min-width: 992px) and (max-width: 1199.98px) {
  .has-client-search .app-header .header-slot--search {
    flex: 0 0 180px;
    width: 180px;
  }
  .app-header .app-search .input-group { min-width: 0; }
}

/* BandejaGPT раскрывает `menu.js` после `/api/menu`, а ссылку «Посмотреть как…»
   — Alpine после `/api/auth/me`. Приходя вразнобой, они толкали друг друга вбок
   на 130px, и это видно на каждом обновлении страницы.
   Право `ai_query` известно из прошлого визита (класс на <html> ставит скрипт в
   <head>), поэтому кнопку рисуем сразу — и место занято с первого кадра. Если
   право отобрали, её уберёт тот же `menu.js`: единственный случай, когда она
   мигнёт, и он разовый.

   Ссылке имперсонации своего правила НЕ даём намеренно: она последняя в строке,
   и раз BandejaGPT держит место, её появление уже ничего не двигает (замерено).
   А раннее раскрытие ей вредно — Alpine прячет её обратно, пока не подтянется
   его store, и вышло бы мигание на ровном месте.

   ⚠️ `!important` вынужденный: перебиваем `.d-none` Bootstrap с тем же
   `!important`. */
.has-bandeja #bandeja-btn.d-none {
  display: inline-flex !important;
}

/* Ширина переключателя клуба: чтобы чип утилизации и имя клуба, приезжающие
   РАЗНЫМИ запросами, не дёргали строку внутри самого тоггла.
   `min-width`, а не фиксация: длинное имя («Тверь - Центральный рынок») должно
   раздвинуть тоггл, а не наехать на поиск.
   ⚠️ Только для широкого экрана. Без медиа-запроса эти 235px душат шапку на
   всех узких ширинах: переполнение начиналось уже с ~1350px, на 992px доходило
   до 116px, а на 390px иконка поиска уезжала под ссылку профиля и переставала
   нажиматься вовсе. */
@media (min-width: 992px) {
  .app-header .header-left .dropdown-toggle {
    min-width: 235px;
    justify-content: flex-start;
  }
}

/* Слот под чип утилизации: держит ширину независимо от того, что внутри —
   значок-заглушка или сам процент. Они приходят в РАЗНОЕ время: клубы одним
   запросом, проценты вторым, поэтому без общего слота значок сменялся чипом и
   двигал название клуба вместе с поиском.
   `min-width` на самом чипе тут не работает: пока `x-show` держит его в
   `display: none`, он не занимает ничего. */
.app-header .header-left .club-chip-slot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  margin-right: 4px;
  flex: 0 0 48px;
}

.app-header .header-left .club-chip-slot .badge {
  min-width: 48px;
  text-align: center;
}

/* Слот под полосу выручки: 32px заняты с первого кадра, ещё до ответа API.
   Полоса живёт в base.html, то есть на КАЖДОЙ странице CRM, и её появление
   толкало вниз сайдбар и всё содержимое — системный сдвиг вёрстки.
   Фон и рамка здесь, а не на самой полосе: если данные не пришли (нет прав,
   ошибка), остаётся тонкий разделитель, а не пустая дыра. */
.revenue-bar-slot { display: none; }

/* Полоса — только у клубов, где план на месяц заведён. Без плана показывать
   нечего, а лента с одним «/ (план )» занимала строку зря. Признак известен
   заранее из прошлого визита (`base.html`, reserveHeaderSlots), поэтому место
   отводится ещё до отрисовки и полоса ничего не двигает; свежий ответ
   поправит флаг сам (`revenue-progress.js`). */
.has-revenue-plan .revenue-bar-slot {
  display: block;
  height: 32px;
  background: var(--bp-paper-2);
  border-bottom: 1px solid var(--bp-line);
}

.revenue-bar { display: flex; }

/* Цифры выручки: полоса показывается, когда `data` уже есть, а сумм в нём ещё
   нет — блок разрастался с 81 до 322px и прыгал левым краем на 241px, отъедая
   ширину у прогресс-дорожки рядом. Ширина замерена на живой странице.
   `min-width`, а не фиксация: длинные суммы должны раздвинуть блок, а не
   вылезти за него. Только широкий экран — на узком часть цифр скрыта
   (`d-none d-sm-inline`, `d-none d-md-inline`), и запас был бы лишним. */
@media (min-width: 992px) {
  .revenue-bar .revenue-stats {
    min-width: 322px;
    justify-content: flex-end;
  }
}

.app-header .header-spacer { flex: 1 1 auto; }
.app-header .header-left,
.app-header .header-right {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.app-header .header-left { flex: 0 1 auto; } /* club · search · BandejaGPT, by the logo */

/* Drawer / search toggles (mobile only) */
.app-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #fff;
  font-size: 1.15rem;
  cursor: pointer;
}
.app-icon-btn:hover { background: rgba(255, 255, 255, 0.12); }

/* Club switcher + search inherit a navy-friendly look (the old theme-blue
 * navbar rules used to do this; they no longer match .app-header). */
.app-header .nav-link,
.app-header .dropdown-toggle { color: #fff; }
.app-header .nav-link:hover { color: rgba(255, 255, 255, 0.85); }
.app-header .form-control,
.app-header .input-group-text {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.28);
  color: #fff;
}
.app-header .form-control::placeholder { color: rgba(255, 255, 255, 0.6); }
.app-header .badge.bg-primary {
  background: rgba(255, 255, 255, 0.92) !important;
  color: var(--bp-blue) !important;
}

/* BandejaGPT button — quiet accent pill, the one «sparkle» in the header. */
.app-bandeja {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  background: transparent;
  color: #fff;
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.app-bandeja:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
.app-bandeja .bi { color: var(--bp-green-on-dark); }

/* Profile dropdown */
.app-profile { display: flex; align-items: center; gap: 1px; }
/* Avatar/name → «Моя карточка» directly; the caret opens the dropdown. */
.app-profile-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 6px 3px 3px;
  border-radius: 999px;
  color: #fff;
  text-decoration: none;
}
.app-profile-link:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.app-profile-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 28px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.72rem;
  cursor: pointer;
}
.app-profile-caret:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.app-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.2);
  overflow: hidden;
  font-size: 0.85rem;
}
.app-profile-name { font-size: 0.85rem; white-space: nowrap; }

/* Wide-ish client search */
.app-search { display: flex; }
.app-search .input-group { min-width: 230px; }

/* ───────────────────────── Shell + sidebar ───────────────────────── */
.app-shell { display: flex; align-items: stretch; }
/* Сайдбар — БЕЛЫЙ лист на сером фоне страницы (2026-07-29). До этого он был
   var(--bp-paper), то есть того же цвета, что и контент: два поля разделяла
   только волосяная линия. Инверсия «белое меню / серая страница» повторяет
   раскладку YClients и сразу отделяет навигацию от данных. */
.app-sidebar {
  flex: 0 0 var(--app-sidebar-w);
  width: var(--app-sidebar-w);
  /* No inner scrollbar (product decision): the menu flows with the page — the
     whole page scrolls, never a scroll region inside the sidebar. */
  min-height: calc(100vh - var(--app-header-h));
  /* Нижний отступ с запасом под fixed-кнопку «Новая задача для IT…»
     (левый нижний угол): без него она стабильно накрывала последний пункт.
     64px = высота пилюли (38) + её отступ от низа (16) + воздух. */
  padding: 10px 8px 64px;
  background: var(--bp-sheet);
  border-right: 1px solid var(--bp-line);
  font-family: var(--ff-body);
}
.app-main { flex: 1 1 auto; min-width: 0; }

/* ── Обратные отсчёты (partials/_countdowns.html) ──
   Никакой анимации и мигания — блок висит на КАЖДОМ экране CRM, любое
   движение здесь отвлекает от работы.

   ⚠️ Оформление и место ВЫБИРАЮТСЯ, а не заданы кодом (миграция 642):
   «отключение через неделю» и «переезд кассы через полгода» требуют разной
   громкости. Базовая геометрия — здесь, пять скинов — ниже, `.countdown--*`.
   Сплошной чёрный лист по умолчанию убран: в белом сайдбаре он читался
   траурно и занимал много места (правка Дениса 06.09.2026).

   Резерв высоты (`.has-countdowns`) нужен только верхнему слоту — под меню
   блоку толкать нечего. */
.countdown {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin: 2px 4px 6px;
  padding: 7px 9px;
  border: 1px solid transparent;
  border-radius: var(--bp-radius-sm);
  font-family: var(--ff-body);
  text-decoration: none;
}
/* Известно из прошлого визита, сколько отсчётов включено, — место под них
   занято ещё до ответа API: блоки стоят НАД календарём и меню, и их
   появление сдвигало бы вниз весь сайдбар. 96px на блок — ЗАМЕР собранного
   блока на живой странице (подпись переносится на две строки, ниже цифра и
   дата), а не прикидка: неверный резерв — тот же сдвиг, только на разницу.
   Число блоков кладёт в `--countdown-count` скрипт в <head> (base.html). */
.countdown-list:empty { display: block; }
/* Резерв — только ВЕРХНЕМУ слоту: он один стоит над календарём и меню.
   `--countdown-count` считает именно верхние блоки (countdowns.js), поэтому
   при умолчании «под меню» резерв равен нулю и вёрстка не дёргается вовсе.
   78px на блок — замер собранного блока скина `brand`. */
.has-countdowns .countdown-list[data-countdowns="above"]:empty {
  min-height: calc(var(--countdown-count, 1) * 78px);
}
.countdown__label {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.countdown__clock {
  font-family: var(--ff-mono);
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.countdown__deadline { font-size: 0.68rem; }
/* Свёрнутая рельса показывает только это — в развёрнутом меню оно лишнее. */
.countdown__short { display: none; }

/* ── Пять заготовок оформления. Выбор хранится у отсчёта и приезжает
      классом из countdowns.js; всё — на фирменных токенах. ── */

/* brand — умолчание: фирменная зелёная плашка, спокойная и компактная. */
.countdown--brand {
  background: var(--bp-green-tint);
  border-color: var(--bp-green);
}
.countdown--brand .countdown__label { color: var(--bp-green-dark); }
.countdown--brand .countdown__clock { color: var(--bp-ink); }
.countdown--brand .countdown__deadline { color: var(--bp-ink-soft); }

/* soft — почти без фона, зелёная черта слева: для сроков «когда-нибудь». */
.countdown--soft {
  background: transparent;
  border-color: transparent;
  border-left: 3px solid var(--bp-green);
  border-radius: 0;
  padding-left: 8px;
}
.countdown--soft .countdown__label { color: var(--bp-ink-soft); }
.countdown--soft .countdown__clock { color: var(--bp-ink); }
.countdown--soft .countdown__deadline { color: var(--bp-muted); }

/* line — одна строка без плашки: самый тихий вариант, занимает минимум. */
.countdown--line {
  flex-direction: row;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 9px;
  background: transparent;
}
.countdown--line .countdown__label {
  color: var(--bp-muted);
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.72rem;
}
.countdown--line .countdown__clock {
  color: var(--bp-ink);
  font-size: 0.78rem;
}
.countdown--line .countdown__deadline { display: none; }

/* amber — янтарная: срок близко и это важно, но ещё не авария. */
.countdown--amber {
  background: var(--bp-warning-tint);
  border-color: var(--bp-warning-fill);
}
.countdown--amber .countdown__label { color: var(--bp-warning-deep); }
.countdown--amber .countdown__clock { color: var(--bp-ink); }
.countdown--amber .countdown__deadline { color: var(--bp-ink-soft); }

/* dark — прежний вид: тёмный лист с янтарной строкой. Оставлен вариантом
   для тех объявлений, которым и нужна максимальная громкость. */
.countdown--dark {
  background: var(--bp-ink);
  border-color: var(--bp-ink);
}
.countdown--dark .countdown__label { color: var(--bp-warning-fill); }
.countdown--dark .countdown__clock { color: var(--bp-sheet); }
.countdown--dark .countdown__deadline { color: var(--bp-muted); }

.countdown[href]:hover { border-color: var(--bp-green-dark); }

/* Последние сутки: до них блок обязан быть фоном, а не тревогой. Красным
   красим ЦИФРУ, а не всю плашку — иначе спокойные скины теряют смысл. */
.countdown.is-final { border-color: var(--bp-stamp-red); }
.countdown.is-final .countdown__clock { color: var(--bp-stamp-red); }
.countdown--dark.is-final .countdown__clock { color: var(--bp-warning-bg); }

/* ── Month calendar (above search) — ‹ › switch months, each day links to the
      calendar for that date (/timetable?date=…). ── */
/* Сайдбар белый, поэтому календарь и поиск «вдавлены» серой заливкой, а не
   выложены белыми карточками — иначе на белом они не читались бы как блоки. */
.sidebar-calendar {
  margin: 2px 4px 8px;
  padding: 8px 8px 6px;
  background: var(--bp-paper);
  border: 1px solid var(--bp-line);
  border-radius: 10px;
  font-family: var(--ff-body);
}
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.cal-title { font-size: 0.76rem; font-weight: 600; color: var(--bp-ink); }
.cal-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--bp-muted);
  font-size: 0.78rem;
  cursor: pointer;
}
.cal-nav:hover { background: rgba(4, 65, 132, 0.08); color: var(--bp-blue); }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.cal-wd { text-align: center; font-size: 0.58rem; font-weight: 600; color: var(--bp-muted); padding: 2px 0; }
.cal-empty { aspect-ratio: 1; }
.cal-day {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  border-radius: 6px;
  font-size: 0.72rem;
  color: var(--bp-ink-soft);
  text-decoration: none;
}
.cal-day:hover { background: rgba(4, 65, 132, 0.08); color: var(--bp-blue); }
.cal-day.is-today { font-weight: 700; color: var(--bp-blue); box-shadow: inset 0 0 0 1px var(--bp-blue); }
.cal-day.is-selected { background: var(--bp-blue); color: #fff; font-weight: 700; box-shadow: none; }

/* ── Menu search (top of sidebar) — filters items by label + description ── */
.sidebar-search {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 2px 4px 8px;
  padding: 6px 10px;
  background: var(--bp-paper);
  border: 1px solid var(--bp-line);
  border-radius: 9px;
}
.sidebar-search .bi { color: var(--bp-muted); font-size: 0.9rem; }
.sidebar-search-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  outline: none;
  background: transparent;
  font-family: var(--ff-body);
  font-size: 0.82rem;
  color: var(--bp-ink);
}
.sidebar-search-input::placeholder { color: var(--bp-muted); }
.sidebar-search-results { padding: 0 0 8px; }
.sidebar-search-results .sidebar-search-desc { display: block; font-size: 0.72rem; color: var(--bp-muted); margin-top: 1px; }
.sidebar-search-results .sidebar-search-sec {
  display: block;
  font-size: 0.64rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bp-blue);
  opacity: 0.75;
  margin-top: 2px;
}
.sidebar-search-results mark { background: var(--bp-warning-bg); color: inherit; padding: 0 1px; border-radius: 2px; }
.sidebar-search-empty { padding: 12px 10px; color: var(--bp-muted); font-size: 0.8rem; }

/* ── Accordion sections: the active section is expanded, the rest collapse to a
      header. Click a header to open it (single-open — setupSidebarAccordion).
      Three tiers: SECTION header (uppercase, accent icon, chevron) > item
      (readable) > group caption (tiny, muted, own icon). ── */
.sidebar-section { position: relative; margin-bottom: 1px; }

/* Section header — shared look for the <a> link and the <button> toggle. */
.sidebar-section-head {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 10px;
  border: 0;
  background: transparent;
  color: var(--bp-ink-soft);
  font-family: var(--ff-body);
  font-size: 0.92rem;
  font-weight: 600;
  letter-spacing: 0;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.sidebar-section-toggle { width: 100%; margin-top: 4px; border-radius: 8px; }
.sidebar-section-toggle:hover { background: rgba(4, 65, 132, 0.05); }
/* Раскрывающийся заголовок — кнопка, а не ссылка (просьба Паши 30.07), поэтому
   активное состояние живёт на ней, а не на .sidebar-section-headrow. */
.sidebar-section-toggle.active { background: var(--bp-blue-tint); color: var(--bp-blue); }
.sidebar-section-toggle.active .sidebar-section-icon { color: var(--bp-blue); }

/* Link-header row: label links to the section's page, a separate chevron button
   expands the rest (click — never hover). */
.sidebar-section-headrow {
  display: flex;
  align-items: stretch;
  margin-top: 4px;
  border-radius: 8px;
}
.sidebar-section-headrow:hover { background: rgba(4, 65, 132, 0.05); }
.sidebar-section-headrow.active { background: var(--bp-blue-tint); }
.sidebar-section-headrow.active .sidebar-section-head,
.sidebar-section-headrow.active .sidebar-section-icon { color: var(--bp-blue); }

.sidebar-section-icon { flex: 0 0 18px; width: 18px; text-align: center; font-size: 1rem; color: var(--bp-blue); }
.sidebar-section-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

.sidebar-chevron-btn {
  display: flex;
  align-items: center;
  padding: 0 8px;
  border: 0;
  background: transparent;
  color: var(--bp-muted);
  cursor: pointer;
}
.sidebar-chevron-btn:hover { color: var(--bp-ink); }
.sidebar-chevron { font-size: 0.7rem; color: var(--bp-muted); transition: transform 0.18s ease; }
.sidebar-section-toggle .sidebar-chevron { margin-left: auto; }
.sidebar-section:not(.is-open) .sidebar-chevron { transform: rotate(-90deg); }
.sidebar-section:not(.is-open) .sidebar-section-body { display: none; }
.sidebar-section-body { padding: 2px 0 8px; }

/* Третий уровень (.sidebar-group-label — подзаголовки «Сегодня» / «Каталог» /
   «Аналитика» внутри секции) УДАЛЁН 2026-07-29 вместе с перекладкой структуры:
   меню теперь ровно двухуровневое, как в YClients. Каждый подзаголовок стоил
   строки экрана, а группировку из трёх слов читатель всё равно не удерживал.
   Разбиение по смыслу переехало на страницы-хабы (/settings, /reports). */

.sidebar-link {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 7px 10px 7px 14px;
  border-radius: 8px;
  color: var(--bp-ink-soft);
  font-size: 0.85rem;
  line-height: 1.45;
  text-decoration: none;
  transition: background 0.12s ease, color 0.12s ease;
}
.sidebar-link:hover { background: rgba(4, 65, 132, 0.06); color: var(--bp-ink); }
.sidebar-link.active { background: var(--bp-blue-tint); color: var(--bp-blue); font-weight: 600; }
.sidebar-link.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--bp-blue);
}
/* Иконок у пунктов НАВИГАЦИИ нет — их не рендерит buildSidebarHTML, отдельного
   правила не нужно. ❌ Не глушить их здесь через `.sidebar-link .bi`: тот же
   класс носят результаты поиска по меню и кнопка BandejaGPT в мобильном
   drawer, и слепое `display:none` погасило бы иконки и там. */
.sidebar-link .bi { flex: 0 0 18px; width: 18px; text-align: center; font-size: 1rem; line-height: 1.25; opacity: 0.85; }
/* Full labels — wrap to as many lines as needed. We NEVER truncate with «…»
   (CLAUDE.md rule); a cut-off label reads as a bug. */
.sidebar-link .nav-label { white-space: normal; overflow-wrap: anywhere; }
.sidebar-divider { height: 1px; margin: 8px 10px; background: var(--bp-line); }

/* Staggered reveal (one tasteful page-load motion). */
@media (prefers-reduced-motion: no-preference) {
  #app-sidebar-nav { animation: navShellFade 0.22s ease both; }
}
@keyframes navShellFade { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }

/* ───────────────────── Свёрнутый сайдбар (рельса 60px) ─────────────────────
   ☰ в шапке переключает body.sidebar-railed (base.html:navShell.toggleNav).
   Свёрнутый сайдбар показывает ТОЛЬКО иконки секций; подписи и подпункты
   всплывают флайаутом по наведению/фокусу. Подписи в рельсе нет вовсе — это не
   обрезка многоточием, правило CLAUDE.md не нарушается.

   ⚠️ Иконка секции здесь — единственный различитель разделов: подбирая новую,
   проверяй глиф на спутываемость с соседними.

   Только десктоп: ниже 992px сайдбара нет вовсе, там мобильный drawer. */
@media (min-width: 992px) {
  /* В режиме переноса брони рельса снимается: «прожектор» журнала подсвечивает
     месячный календарь сайдбара, а в рельсе он спрятан — пользователь видел бы
     подсветку пустого места. Поэтому все правила ниже скоупятся
     :not(.tt-reschedule-active). */
  body.sidebar-railed:not(.tt-reschedule-active) .app-sidebar {
    flex-basis: var(--app-sidebar-rail-w);
    width: var(--app-sidebar-rail-w);
    padding-left: 4px;
    padding-right: 4px;
    overflow: visible; /* флайаут выходит за пределы рельсы */
    /* Поднимаем ВЕСЬ сайдбар, а не только флайаут: z-index на самом флайауте
       не сработает — его предок несёт заполняющую transform-анимацию и потому
       создаёт собственный stacking context.
       990 — выше любого контента (замеренный максимум 85), но НИЖЕ залипшей
       шапки: сайдбар не липкий, и при скролле его иконки въезжают в полосу
       шапки. Значение выше 1030 перекрывало саму кнопку ☰, и развернуть меню
       было нельзя, не прокрутившись наверх. Заодно ниже дропдаунов Bootstrap
       (1000), чат-дока и модалок. Подробности — web/README.md. */
    position: relative;
    z-index: 990;
  }
  /* Календарь и поиск в рельсу не влезают по смыслу — прячем целиком. */
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-calendar,
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-search,
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-search-results { display: none; }

  /* Отсчёт в рельсе ужимается до значка и числа дней («9д»): прятать его
     целиком нельзя — свёрнутое меню это рабочий режим, а не «без объявлений».
     Полный текст возвращается вместе с меню. */
  /* Резерв в рельсе — своим числом: 51px на блок вместо 96px (ЗАМЕР
     собранного блока в рельсе). Иначе у того, кто свернул меню и включил
     отсчёт, место было бы отведено под полный блок, и меню всё равно
     дёргалось бы — только в другую сторону. */
  body.sidebar-railed:not(.tt-reschedule-active) .countdown {
    align-items: center;
    gap: 1px;
    padding: 6px 2px;
    margin-left: 0;
    margin-right: 0;
    min-height: 51px;
  }
  /* `.has-countdowns` живёт на <html>, а `.sidebar-railed` — на <body>:
     порядок в селекторе именно такой, иначе правило не совпадает ни с чем. */
  .has-countdowns body.sidebar-railed:not(.tt-reschedule-active) .countdown-list[data-countdowns="above"]:empty {
    min-height: calc(var(--countdown-count, 1) * 51px);
  }
  body.sidebar-railed:not(.tt-reschedule-active) .countdown [data-countdown-title],
  body.sidebar-railed:not(.tt-reschedule-active) .countdown__clock,
  body.sidebar-railed:not(.tt-reschedule-active) .countdown__deadline { display: none; }
  body.sidebar-railed:not(.tt-reschedule-active) .countdown__short {
    display: block;
    color: var(--bp-sheet);
    font-family: var(--ff-mono);
    font-size: 0.8rem;
    font-weight: 700;
  }

  /* В рельсе остаётся ТОЛЬКО иконка: подпись и шеврон гасим. Шеврон живёт
     внутри кнопки-заголовка (`.sidebar-chevron`) — отдельной кнопки-шеврона
     больше нет, и правило на неё в рельсе ничего не гасило: шевроны торчали
     рядом с иконками и сбивали их с центра (30.07). */
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section-name,
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-chevron { display: none; }
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section-head { justify-content: center; padding: 9px 0; }
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section-icon { flex: 0 0 auto; font-size: 1.15rem; }

  /* Тело секции превращается во флайаут-карточку справа от рельсы. */
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section { position: relative; }
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section-body {
    display: none;
    position: absolute;
    top: 0;
    left: calc(var(--app-sidebar-rail-w) - 6px);
    z-index: 1035;
    min-width: 210px;
    padding: 6px;
    background: var(--bp-sheet);
    border: 1px solid var(--bp-line);
    border-radius: var(--bp-radius);
    box-shadow: var(--bp-shadow-pop);
  }
  /* Показываем флайаут по наведению И по фокусу с клавиатуры (Tab). Раскрытость
     аккордеона (.is-open) в рельсе роли не играет — флайаут ведёт себя одинаково
     у всех секций, поэтому :not(.is-open) тут не нужен. */
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section:hover .sidebar-section-body,
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section:focus-within .sidebar-section-body {
    display: block;
  }
  /* Заголовок секции — во флайауте, иначе в рельсе его негде прочитать. */
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-section-body::before {
    content: attr(data-section-label);
    display: block;
    padding: 4px 10px 6px;
    border-bottom: 1px solid var(--bp-line);
    margin-bottom: 4px;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--bp-ink);
  }
  body.sidebar-railed:not(.tt-reschedule-active) .sidebar-divider { margin-left: 6px; margin-right: 6px; }
}

@media (prefers-reduced-motion: no-preference) {
  .app-sidebar { transition: width 0.18s ease, flex-basis 0.18s ease; }
}

/* ───────────────────────── Mobile drawer ───────────────────────── */
.app-drawer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1040;
  background: rgba(13, 18, 30, 0.45);
}
.app-drawer {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1045;
  width: 270px;
  max-width: 84vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bp-paper);
  box-shadow: 2px 0 16px rgba(0, 0, 0, 0.18);
  transform: translateX(-100%);
  transition: transform 0.2s ease;
  overflow-y: auto;
  font-family: var(--ff-body);
}
.app-drawer.is-open { transform: translateX(0); }
.app-drawer-head {
  display: flex;
  align-items: center;
  height: var(--app-header-h);
  padding: 0 12px;
  background: var(--bp-blue);
  color: #fff;
}
/* Поиск по меню в drawer'е: на мобиле сайдбара с его полем нет, а без поиска
   один журнал из шестидесяти трёх ищется прокруткой всего дерева. */
.app-drawer-search {
  padding: 8px 8px 0;
  border-bottom: 1px solid var(--bp-line);
}
.app-drawer-nav { padding: 8px; flex: 1 1 auto; }
.app-drawer-foot {
  padding: 10px 8px;
  border-top: 1px solid var(--bp-line);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* Пункт-кнопка в подвале drawer'а выглядит как ссылка меню рядом с ним */
.app-drawer-improve {
  width: 100%;
  border: 0;
  background: transparent;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}

/* ── «Новая задача для IT…» — плавающая кнопка в левом нижнем углу ──
   Была синим кружком 56×56 с лампочкой: значок не объяснял, что за ним, и
   его просто не нажимали. Подписанная пилюля объясняет себя сама.

   Заливка брендовым синим, а не «белый лист с рамкой»: кнопка висит НАД
   сайдбаром, а он тоже белый — светлая пилюля на нём терялась и читалась как
   часть меню. Контраст здесь важнее скромности: кнопку нужно найти глазами
   на любом экране, не выискивая. */
.app-fab-improve {
  position: fixed;
  bottom: 16px;
  left: 16px;
  z-index: 1050;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 38px;
  padding: 0 16px;
  border: 1px solid var(--bp-blue);
  border-radius: 999px;
  background: var(--bp-blue);
  color: var(--bp-on-accent);
  font-family: var(--ff-body);
  font-size: 0.86rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  box-shadow: var(--bp-shadow-pop);
  cursor: pointer;
}
.app-fab-improve:hover {
  background: var(--bp-blue-ink);
  border-color: var(--bp-blue-ink);
  color: var(--bp-on-accent);
}
.app-fab-improve .bi { font-size: 1rem; }

/* Responsive switch: sidebar on desktop, drawer on mobile. */
@media (max-width: 991.98px) {
  .app-sidebar { display: none; }
  .app-search {
    display: none;
    position: absolute;
    top: var(--app-header-h);
    left: 0;
    right: 0;
    padding: 8px 12px;
    background: var(--bp-blue);
    z-index: 1029;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.18);
  }
  .app-search.is-open { display: flex; }
  .app-search .input-group { width: 100%; }
}
@media (min-width: 992px) {
  .app-drawer,
  .app-drawer-backdrop { display: none !important; }
}

/* ───────────────────── Страницы-хабы (/settings, /reports) ─────────────────
   Секция с placement="hub" рендерится сеткой карточек: карточка = MenuGroup,
   внутри — компактные текстовые ссылки на экраны. Раскладка снята с экрана
   настроек YClients (docs/yclients/admin-ui-menu.md).

   Отличие от .guide-card («Путеводитель»): там карточка = ОДИН экран с
   описанием — витрина для новичка. Здесь карточка = ГРУППА экранов, а описание
   ушло в title: на хаб приходят, уже зная, что ищут, и нужна плотность. */
.hub-page { max-width: 1100px; }
.hub-hero-title {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--ff-display);
  color: var(--bp-ink);
}
.hub-hero-title .bi { color: var(--bp-blue); font-size: 0.9em; }
.hub-hero-sub { color: var(--bp-ink-soft); max-width: 62ch; margin-bottom: 0; }

.hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 14px;
  align-items: start;
}
/* На телефоне minmax(300px, …) шире контента страницы — карточки уезжали
   вбок; одна колонка. Правило живёт рядом с определением, а не в mobile.css:
   каркас навигации адаптирует этот файл. */
@media (max-width: 767.98px) {
  .hub-grid { grid-template-columns: 1fr; }
  /* Пункт хаба — основной способ навигации с телефона, а 6px паддинга дают
     ссылку в 26px: палец промахивается по соседнюю строку. */
  .hub-card-list a { padding: 10px 8px; }
  .hub-card-list li + li { margin-top: 4px; }
  /* Группа-аккордеон (menu.js рисует `<details>` только на телефоне): на
     десктопе карточка остаётся `<section>`, поэтому раскрытие видно лишь тут. */
  .hub-card > summary { list-style: none; cursor: pointer; }
  .hub-card > summary::-webkit-details-marker { display: none; }
  .hub-card > summary .hub-card-title { margin-bottom: 0; }
  .hub-card[open] > summary .hub-card-title { margin-bottom: 10px; }
  .hub-card > summary .hub-card-title::after {
    content: "\f282"; /* bi-chevron-down */
    margin-left: auto;
    font-family: bootstrap-icons;
    font-size: 0.8rem;
    color: var(--bp-muted);
  }
  .hub-card[open] > summary .hub-card-title::after { content: "\f286"; }
  .hub-search { margin-bottom: 12px; }
}
.hub-card {
  padding: 16px 18px;
  background: var(--bp-sheet);
  border: 1px solid var(--bp-line);
  border-radius: var(--bp-radius-lg);
  box-shadow: var(--bp-shadow-card);
}
.hub-card-title {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0 0 10px;
  font-family: var(--ff-body);
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--bp-ink);
}
.hub-card-title .bi { color: var(--bp-blue); font-size: 1rem; }
.hub-card-list { list-style: none; margin: 0; padding: 0; }
.hub-card-list li + li { margin-top: 2px; }
.hub-card-list a {
  display: block;
  padding: 6px 8px;
  margin: 0 -8px;
  border-radius: var(--bp-radius-sm);
  color: var(--bp-ink-soft);
  font-size: 0.87rem;
  line-height: 1.45;
  text-decoration: none;
  /* Полное имя переносом — никаких «…» (правило CLAUDE.md). */
  overflow-wrap: anywhere;
}
.hub-card-list a:hover { background: var(--bp-blue-tint); color: var(--bp-blue); }

/* Плиточный хаб (`hub_tiles`): описание ВИДНО, а не спрятано в title.
   Плотный список выше остаётся дефолтом — там экран ищут по знакомому имени.
   Плитки нужны там, где название само не объясняет, зачем экран: «Расхождения
   оплат» и «Ракетки партнёра» мимо глаз и пролистывали. */
.hub-card-sub {
  margin: -4px 0 12px;
  color: var(--bp-ink-soft);
  font-size: 0.82rem;
  line-height: 1.45;
}
/* Карточка плиточного хаба несёт больше текста — колонки шире, чтобы описание
   не резалось в две буквы на строку. */
.hub-grid--tiles { grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); }
@media (max-width: 767.98px) {
  .hub-grid--tiles { grid-template-columns: 1fr; }
}
.hub-tiles { display: grid; gap: 8px; }
.hub-tile {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--bp-line);
  border-radius: var(--bp-radius-sm);
  background: var(--bp-paper);
  text-decoration: none;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.hub-tile:hover {
  border-color: var(--bp-blue);
  box-shadow: var(--bp-shadow-hover);
}
.hub-tile-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--bp-radius-sm);
  background: var(--bp-blue-tint);
  color: var(--bp-blue);
}
.hub-tile-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.hub-tile-title {
  color: var(--bp-ink);
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.35;
  /* Полное имя переносом — никаких «…» (правило CLAUDE.md). */
  overflow-wrap: anywhere;
}
.hub-tile-desc {
  color: var(--bp-ink-soft);
  font-size: 0.8rem;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
/* Зелёный тинт + ЧЕРНИЛЬНЫЙ текст (~16:1). Оба «брендовых» варианта проваливают
   AA: «тинт + --bp-green-dark» даёт 2.44:1, «--bp-green + белый» — 2.06:1,
   потому что в CRM обе зелёные переменные подменяются светлым вариантом
   (body:not(.theme-paper) в brand-tokens.css, а CRM-body — theme-blue).
   Зелёный остаётся сигналом, читаемость даёт текст. */
.hub-badge {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--bp-green-tint);
  color: var(--bp-ink);
  font-size: 0.68rem;
  font-weight: 600;
  vertical-align: middle;
}
.hub-empty { color: var(--bp-muted); padding: 24px 0; }
