/* ============================================================
   Orgship · Общие UI-компоненты
   Header, кнопки, side-panel, состояния, hover-аура
   ============================================================ */

/* ============================================================
   Доступность · видимый фокус с клавиатуры (focus-visible)
   Единое кольцо на всех интерактивных элементах. Мышиный :focus
   кольцо НЕ показывает (только :focus-visible), поэтому ховер мышью
   остаётся чистым. Перекрывает места, где стоит outline:none без
   замены (codex/mission/legal/ops инпуты и кнопки).
   ============================================================ */
:focus-visible {
  outline: 2px solid var(--accent-cyan);
  outline-offset: 2px;
  /* НЕ задаём border-radius у самого элемента (это «квадратило» бы
     пилюли при фокусе). Современные браузеры огибают outline по форме
     элемента сами. Для прямоугольных кнопок без радиуса добавляем
     лёгкий радиус ниже. */
}
/* Лёгкий радиус кольца только для непилюльных кнопок/ссылок-карточек. */
.fs-tabs button:focus-visible,
.viz-group button:focus-visible,
.colony-card:focus-visible,
.codex-card:focus-visible,
.feedback-card:focus-visible {
  border-radius: 8px;
}
/* Инпуты с собственным box-shadow-кольцом фокуса — даём им то же
   видимое кольцо для клавиатуры (без двойного border-radius-конфликта). */
.search-box input:focus-visible,
.codex-search input:focus-visible,
.ops-search input:focus-visible,
.mission-unlock .mu-form input:focus-visible {
  outline: 2px solid var(--accent-cyan);
  outline-offset: 2px;
}

/* ============================================================
   Доступность · prefers-reduced-motion
   Гасит анимации/переходы/плавный скролл для пользователей с
   вестибулярными расстройствами. !important перебивает inline
   animation-delay/transition в компонентах.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  /* Карточки Кодекса/Обратной связи/Колонии используют *-rise с
     `both` + opacity:0 в keyframes. При почти нулевой длительности
     анимация всё равно отрабатывает до конечного кадра, но на всякий
     случай гарантируем видимость (защита от хрупкого both+delay). */
  .codex-card, .codex-hero,
  .feedback-card, .feedback-hero,
  .colony-card, .colony-hero {
    opacity: 1 !important;
  }
}

/* ---------- App layout ---------- */
.app {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* Фон убран — он скрывал cosmos-layer. Базовый цвет тянется с body. */
  background: transparent;
}

/* ---------- Header ---------- */
.app-header {
  position: sticky;
  top: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 24px;
  padding: 14px 28px;
  background: linear-gradient(180deg, rgba(10,14,26,0.95) 0%, rgba(10,14,26,0.65) 100%);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--bg-line-soft);
}

.app-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}

.app-brand-logo {
  width: 56px;
  height: 56px;
  object-fit: contain;
  filter:
    drop-shadow(0 0 14px rgba(125, 211, 255, 0.70))
    drop-shadow(0 0 4px rgba(125, 211, 255, 0.40));
  user-select: none;
  flex-shrink: 0;
}

.app-brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.1;
}
.app-brand-text strong {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  letter-spacing: 0.04em;
  color: var(--text-primary);
}
.app-brand-text span {
  font-size: var(--fs-xs);
  letter-spacing: 0.12em;
  color: var(--text-muted);
  text-transform: uppercase;
}

/* ---------- View switcher ---------- */
/* R-224 (2026-09-17): на 1280/1440/1560px 12 кнопок + шапка справа не
   помещались в вьюпорт (scrollWidth 1617 при 1280/1560, PATCH_2026-09-11_KPI.md
   → мега-ревью G1-2/E4-08) — `.app-header-right` уезжал за край и не был
   достижим (в бою `overflow-x:hidden` глобален). Вместо того чтобы шапка
   росла вместе с числом вкладок, `.view-switcher` теперь сам сжимаемый
   flex-item (`min-width:0`) со своим внутренним горизонтальным скроллом —
   `.app-brand` и `.app-header-right` (flex-shrink:0) остаются полностью
   видимыми на любой ширине ≥ разумного минимума. На мобильных ширинах это
   же поведение уже задавал mobile.css — здесь оно становится базовым для
   ВСЕХ ширин, mobile.css переопределяет частности (order, паддинг, маску). */
.view-switcher {
  display: inline-flex;
  flex: 1 1 auto;
  min-width: 0;
  background: var(--bg-surface);
  border: 1px solid var(--bg-line-soft);
  border-radius: var(--r-pill);
  padding: 4px;
  margin-left: 12px;
  overflow-x: auto;
  /* Полоса прокрутки СКРЫТА на всех ширинах (2026-09-28, Андрей: «серый
     ползунок сверху — некрасиво»). До этого был тонкий видимый скроллбар
     как подсказка о спрятанных вкладках. Теперь подсказку дают затухание
     по краям (маска ниже) и прокрутка колесом мыши: main.js переводит
     вертикальное колесо в горизонтальный сдвиг, когда вкладки не влезли. */
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* Затухание по краям — знак «здесь есть ещё», когда вкладки не
     помещаются целиком и часть доступна только скроллом (тот же приём,
     что и на мобильном, mobile.css:235-238). Прозрачно, когда всё влезло
     (маска на пустоту эффекта не даёт). */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10px, #000 calc(100% - 10px), transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 10px, #000 calc(100% - 10px), transparent 100%);
}

.view-switcher::-webkit-scrollbar { display: none; }

.view-switcher button {
  position: relative;
  padding: 7px 18px;
  font-size: var(--fs-sm);
  font-weight: 500;
  border-radius: var(--r-pill);
  color: var(--text-muted);
  flex-shrink: 0;
  white-space: nowrap;
  transition: color var(--t-normal), background var(--t-normal);
}

.view-switcher button.active {
  background: linear-gradient(135deg, rgba(63,224,197,0.18), rgba(79,123,196,0.18));
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--bg-line);
}

.view-switcher button:hover {
  color: var(--text-primary);
}

/* ---------- Header right ---------- */
.app-header-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}

.kpi-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px;
  background: var(--bg-surface);
  border: 1px solid var(--bg-line-soft);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  color: var(--text-secondary);
}

/* ---------- Sync indicator (header) ---------- */
.sync-indicator {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px 6px 10px;
  background: var(--bg-surface);
  border: 1px solid var(--bg-line-soft);
  border-radius: var(--r-pill);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  transition: border-color var(--t-normal), background var(--t-normal), color var(--t-normal);
}
.sync-indicator:hover {
  border-color: rgba(125, 211, 255, 0.45);
  background: rgba(125, 211, 255, 0.06);
  color: var(--text-primary);
}
.sync-indicator .sync-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
  box-shadow: 0 0 0 0 transparent;
  flex-shrink: 0;
  transition: background var(--t-normal), box-shadow var(--t-normal);
}
.sync-indicator .sync-dot.live {
  /* R-234 (2026-09-17, мега-ревью G2-1.5): было буквально #3FE0C5 —
     точное совпадение с --accent-cyan мимо токена, смена акцента в
     tokens.css сюда бы не докатилась. */
  background: var(--accent-cyan);
  box-shadow: 0 0 8px rgba(63, 224, 197, 0.7);
  animation: sync-pulse 2.6s ease-in-out infinite;
}
.sync-indicator .sync-dot.cache {
  background: var(--status-risk); /* R-234: было буквально #FFB547 */
}
.sync-indicator .sync-dot.local {
  background: var(--text-muted);
}
/* Конвейер данных под наблюдением (Meta.sync_status=warn из DataPipeline) */
.sync-indicator .sync-dot.warn {
  background: var(--status-risk); /* R-234: было буквально #FFB547 */
  box-shadow: 0 0 8px rgba(255, 181, 71, 0.75);
  animation: sync-pulse 1.6s ease-in-out infinite;
}
/* Конвейер данных ОТКАЗАЛ (Meta.sync_status=error). Громче предупреждения:
   до 01.09.2026 это состояние не проверялось вовсе и полный отказ выглядел
   зеленее, чем лёгкий warn. */
.sync-indicator .sync-dot.err {
  background: var(--status-behind); /* R-234: было буквально #FF5470 */
  box-shadow: 0 0 10px rgba(255, 84, 112, 0.85);
  animation: sync-pulse 1.2s ease-in-out infinite;
}
.sync-indicator.syncing {
  pointer-events: none;
  opacity: 0.85;
}
.sync-indicator.syncing .sync-dot {
  animation: sync-spin 0.9s linear infinite;
  background: #7BD3FF;
  box-shadow: 0 0 10px rgba(125, 211, 255, 0.7);
}
@keyframes sync-pulse {
  0%, 100% { transform: scale(1);   opacity: 1; }
  50%      { transform: scale(1.4); opacity: 0.55; }
}
@keyframes sync-spin {
  0%   { transform: scale(1.1); }
  50%  { transform: scale(0.65); }
  100% { transform: scale(1.1); }
}
.sync-indicator .sync-label {
  white-space: nowrap;
}

/* ---------- Полоса «данные не свежие» (core/sync-indicator.js) ----------
   Появляется под шапкой, когда на экране НЕ живой ответ сервера, а снимок.
   Полоса, а не тост: тост исчезает через несколько секунд, а неправда на
   экране остаётся. Разметку строит JS — index.html тонкая оболочка. */
.stale-bar {
  position: sticky;
  top: 0;
  z-index: calc(var(--z-overlay) - 1);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 28px;
  background: linear-gradient(180deg, rgba(255, 84, 112, 0.20) 0%, rgba(255, 84, 112, 0.10) 100%);
  border-bottom: 1px solid rgba(255, 84, 112, 0.45);
  color: var(--text-primary);
  font-size: 13px;
  line-height: 1.45;
  backdrop-filter: blur(8px);
}
.stale-bar[hidden] { display: none; }
.stale-bar-icon {
  flex-shrink: 0;
  font-size: 15px;
  color: var(--status-behind); /* R-234: было буквально #FF5470 */
}
.stale-bar-text { flex: 1; min-width: 0; }
/* Не «данные подождут», а «работать нельзя»: другой цвет, чтобы полосу
   не приняли за привычное предупреждение о свежести. */
.stale-bar--blocked { background: color-mix(in srgb, var(--danger, #e5484d) 16%, transparent); border-color: var(--danger, #e5484d); }
.stale-bar--blocked .stale-bar-icon::before { content: ""; }

.stale-bar-btn {
  flex-shrink: 0;
  padding: 6px 14px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(255, 84, 112, 0.55);
  background: rgba(255, 84, 112, 0.14);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.stale-bar-btn:hover:not(:disabled) {
  background: rgba(255, 84, 112, 0.26);
  border-color: rgba(255, 84, 112, 0.8);
}
.stale-bar-btn:disabled { opacity: 0.6; cursor: default; }

@media (max-width: 720px) {
  .stale-bar {
    flex-wrap: wrap;
    padding: 9px 14px;
    font-size: 12px;
    gap: 8px;
  }
  .stale-bar-text { flex: 1 1 100%; order: 2; }
  .stale-bar-btn  { order: 3; }
}
.kpi-pill strong {
  font-family: var(--font-mono);
  color: var(--text-primary);
  font-weight: 600;
}
.kpi-pill .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent-cyan);
  box-shadow: 0 0 6px var(--accent-cyan);
}

/* ---------- Search ---------- */
.search-box {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  background: var(--bg-surface);
  border: 1px solid var(--bg-line-soft);
  border-radius: var(--r-md);
  min-width: 220px;
  transition: border var(--t-normal), box-shadow var(--t-normal);
}

.search-box:focus-within {
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(63, 224, 197, 0.15);
}

.search-box input {
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text-primary);
  font-size: var(--fs-sm);
  width: 100%;
  font-family: var(--font-body);
}
.search-box input::placeholder { color: var(--text-muted); }

.search-box .search-icon {
  color: var(--text-muted);
  flex-shrink: 0;
}

/* Выдача поиска по сайту (core/search.js, 23.09.2026). Живёт ВНУТРИ
   `.search-box` абсолютом — поэтому у коробки `position: relative` и
   нигде нет `overflow: hidden`. Прижата к ПРАВОМУ краю: поле стоит в
   правой части шапки, и выпадать списку есть куда только влево. */
.search-box--site { position: relative; }
/* Затемнение под поиском — только на телефоне (mobile.css). */
.search-shade { display: none; }
.search-pop {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: min(460px, calc(100vw - 24px));
  max-height: min(70vh, 560px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 4px 0;
  background: var(--bg-surface);
  border: 1px solid var(--bg-line);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-card);
  z-index: var(--z-overlay);
  text-align: left;
}
.search-pop[hidden], .search-list[hidden] { display: none; }
.search-grp + .search-grp { border-top: 1px solid var(--bg-line-soft); }
.search-grp-h {
  padding: 8px 12px 3px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.search-opt {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  color: var(--text-primary);
  font-size: var(--fs-sm);
  cursor: pointer;
}
.search-opt.is-active { background: var(--bg-line-soft); }
.search-ava {
  width: 24px; height: 24px; flex-shrink: 0;
  border-radius: 50%;
  object-fit: cover; object-position: center 22%;
}
.search-ico { width: 24px; flex-shrink: 0; text-align: center; font-size: 15px; line-height: 1; }
.search-txt { display: flex; flex-direction: column; min-width: 0; }
.search-title, .search-sub {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.search-sub { color: var(--text-muted); font-size: var(--fs-xs); }
.search-note, .search-foot {
  padding: 6px 12px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.search-note[hidden], .search-foot[hidden] { display: none; }
.search-foot { border-top: 1px solid var(--bg-line-soft); }
.search-foot--empty { border-top: 0; padding: 12px; font-size: var(--fs-sm); }

/* ============================================================
   ШАПКА НА НОУТБУКЕ: ВСЕ ВКЛАДКИ ВИДНЫ, БЕЗ ПРОКРУТКИ (R3B-06, 17.09.2026)

   R-224 убрал переполнение вьюпорта, сделав `.view-switcher` сжимаемым
   скроллером, — и переполнение ушло, а жалоба осталась: на 1440 из
   одиннадцати вкладок за коробкой скроллера оказывались ЧЕТЫРЕ, включая
   «Панель», на 1560 — три, на 1280 — шесть. Вход в пульт надо было
   сначала найти прокруткой узкой полосы.

   Полоса вкладок нужна целиком: это единственная навигация сайта.
   Значит место ей уступает всё остальное, и уступает ПО ПОРЯДКУ
   ВАЖНОСТИ — подпись бренда (логотип остаётся), поле поиска (сворачивается
   в значок и раскрывается по фокусу, Ctrl+K работает как работал),
   подпись «Mission 2026» у пилюли (число остаётся), отступы и padding
   кнопок. Ни одна кнопка и ни один элемент не прячется: спрятанное —
   это то, чем не пользуются.

   Замер до правки (1280 / 1440 / 1560): коробка 473 / 633 / 753 px при
   нужных 1063 — обрезано 6 / 4 / 3. Ниже 769 px действует mobile.css:
   там полоса переезжает второй строкой и скроллится пальцем осознанно.
   ============================================================ */
@media (min-width: 769px) and (max-width: 1699px) {
  .app-header { gap: 12px; padding-left: 16px; padding-right: 16px; }

  /* Логотип остаётся — по нему узнают сайт; подпись повторяет то, что
     и так написано во вкладке браузера. */
  .app-brand-text { display: none; }
  .app-brand-logo { width: 42px; height: 42px; }

  .view-switcher { margin-left: 0; padding: 3px; }
  .view-switcher button { padding: 7px 11px; font-size: 13px; }

  .app-header-right { gap: 8px; }

  /* ПОИСК — ЗНАЧКОМ. Поле схлопывается до иконки и раскрывается по
     фокусу: клик по значку попадает в сам `input` (он растянут по
     коробке и прозрачен), поэтому ни обработчика, ни второй кнопки не
     заводим. Ctrl+K фокусирует его как прежде — раскрытие идёт от
     `:focus-within`, а не от клика.

     `overflow: hidden` здесь НЕЛЬЗЯ: список подсказок поиска живёт
     внутри `.search-box` абсолютом (`core/search.js`), и он был бы
     обрезан. Поэтому схлопывается не коробка с содержимым, а ширина
     коробки при растянутом поле. */
  .search-box {
    position: relative;
    min-width: 0;
    width: 38px;
    padding: 7px 10px;
    transition: width var(--t-normal), border var(--t-normal), box-shadow var(--t-normal);
  }
  .search-box .search-icon { position: relative; z-index: 1; pointer-events: none; }
  .search-box input {
    position: absolute;
    inset: 0;
    width: 100%;
    padding: 0 10px 0 32px;
    opacity: 0;
    transition: opacity var(--t-normal);
  }
  .search-box:focus-within { width: 260px; }
  .search-box:focus-within input { opacity: 1; }

  /* Пилюля Mission: число выполнения остаётся, подпись года — нет.
     Тот же приём, что на мобильном (mobile.css), и по той же причине. */
  .kpi-pill { padding: 6px 10px; gap: 6px; }
  .kpi-pill > span:nth-of-type(2) { display: none; }
}

/* Узкий ноутбук (1024–1150): того же набора уступок не хватает —
   добираем на padding кнопок и на компактном индикаторе синхронизации.
   Одиннадцать вкладок на 1024 помещаются целиком. */
@media (min-width: 769px) and (max-width: 1150px) {
  .app-header { padding-left: 10px; padding-right: 10px; gap: 8px; }
  .view-switcher button { padding: 7px 6px; font-size: 11.5px; }
  .app-brand-logo { width: 34px; height: 34px; }
  .sync-indicator { padding: 5px 9px; font-size: 11px; gap: 6px; }
  .sync-indicator .sync-label { font-size: 11px; }
}

/* ---------- Main area ---------- */
.app-main {
  flex: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 32px 24px 80px;
}

.view {
  width: 100%;
  max-width: 1400px;
  display: none;
}
.view.active { display: block; }

/* ---------- Side panel ---------- */
.side-panel {
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  /* dvh — фактическая высота вьюпорта. В iOS Safari 100vh считается
     по БОЛЬШОМУ вьюпорту (адресная строка спрятана), поэтому пока она
     видна, низ панели лежит на ~90px ниже экрана и кнопка в конце
     формы недостижима никакой прокруткой. Строкой выше — фолбэк. */
  height: 100dvh;
  width: 440px;
  max-width: 92vw;
  background: linear-gradient(180deg, var(--bg-surface) 0%, var(--bg-dark) 100%);
  border-left: 1px solid var(--bg-line);
  box-shadow: var(--shadow-elevated);
  z-index: var(--z-panel);
  transform: translateX(110%);
  transition: transform var(--t-slow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.side-panel.open { transform: translateX(0); }

.side-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 22px;
  border-bottom: 1px solid var(--bg-line-soft);
}

.side-panel-close {
  width: 32px; height: 32px;
  display: inline-flex;
  align-items: center; justify-content: center;
  border-radius: 50%;
  color: var(--text-muted);
  transition: background var(--t-fast), color var(--t-fast);
}
.side-panel-close:hover {
  background: var(--bg-line-soft);
  color: var(--text-primary);
}

.side-panel-body {
  flex: 1;
  overflow-y: auto;
  padding: 24px 22px 32px;
}

.side-panel-overlay {
  position: fixed;
  inset: 0;
  background: rgba(5, 8, 16, 0.55);
  backdrop-filter: blur(2px);
  z-index: calc(var(--z-panel) - 1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-slow);
}
.side-panel-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

/* ---------- Universal: hover aura ---------- */
.clickable {
  cursor: pointer;
  transition: filter var(--t-normal), transform var(--t-normal);
}
.clickable:hover {
  filter: drop-shadow(0 0 8px var(--accent-color, var(--accent-cyan))) drop-shadow(0 0 18px rgba(63, 224, 197, 0.5));
}

/* ---------- Small atoms ---------- */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  background: rgba(63, 224, 197, 0.12);
  color: var(--accent-cyan);
  border-radius: var(--r-pill);
  font-size: var(--fs-xs);
  letter-spacing: 0.03em;
  border: 1px solid rgba(63, 224, 197, 0.25);
}

.tag.warn  { background: rgba(255,181,71,0.10); color: var(--status-risk); border-color: rgba(255,181,71,0.3); }
.tag.alert { background: rgba(255,84,112,0.10); color: var(--status-behind); border-color: rgba(255,84,112,0.3); }
.tag.muted { background: rgba(122,138,168,0.10); color: var(--text-muted); border-color: var(--bg-line-soft); }

/* ---------- Section title ---------- */
.section-title {
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 22px 0 10px;
}

/* ---------- Loader ---------- */
.loader {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-dark);
  z-index: var(--z-modal);
}
.loader-dot {
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--accent-cyan);
  margin: 0 6px;
  animation: pulse 1.2s infinite ease-in-out;
  box-shadow: 0 0 12px var(--accent-cyan);
}
.loader-dot:nth-child(2) { animation-delay: 0.15s; }
.loader-dot:nth-child(3) { animation-delay: 0.3s; }
@keyframes pulse {
  0%, 100% { transform: scale(0.6); opacity: 0.3; }
  50% { transform: scale(1); opacity: 1; }
}

/* ---------- Toast (notification) ---------- */
.toast {
  position: fixed;
  bottom: 28px;
  right: 28px;
  padding: 12px 18px;
  background: var(--bg-surface);
  border: 1px solid var(--bg-line);
  border-radius: var(--r-md);
  color: var(--text-primary);
  font-size: var(--fs-sm);
  z-index: var(--z-toast);
  box-shadow: var(--shadow-card);
  transform: translateY(20px);
  opacity: 0;
  transition: transform var(--t-normal), opacity var(--t-normal);
  /* Сообщение об успехе НЕ должно перехватывать клики.

     Оно висит в правом нижнем углу — там же, где кнопка отправки в
     боковой панели. Две с половиной секунды после каждого сохранения
     кнопка под ним переставала нажиматься, причём молча: человек жмёт,
     ничего не происходит, он жмёт ещё раз. Поймано браузерным смоуком
     2026-08-21 («toast intercepts pointer events»). */
  pointer-events: none;
}
.toast.show { transform: translateY(0); opacity: 1; }


/* ============================================================
   ЗНАЧОК «?» И ОБЛАЧКО ПОДСКАЗКИ — ОБЩИЙ СЛОЙ (17.09.2026)

   Оба рисует `q()` из `src/core/ui/tip.js`, то есть ЛЮБАЯ вкладка, а
   стиль до сих пор лежал только в CSS трёх из них (tasks, admin, kpi)
   тремя одинаковыми копиями. На «Неделе», в OKR, Кодексе и «Заявках»
   значок приезжал голой кнопкой 7×16 px (замерено в форме недельной
   цели при собственном пороге проекта 44 px), а облачко — абзацем
   текста в конце страницы: `.org-tip` живёт в <body>, иначе его
   обрезает прокрутка панели, и без стиля оно ничем не оформлено.

   Место правила — здесь: ui.css грузится всегда, и это единственный
   слой, который гарантированно рядом с элементом, который может
   появиться где угодно. Зона нажатия под палец (::before, inset -14px)
   стоит в mobile.css — тоже всегда загруженном.

   Три копии в tasks.css / admin.css / kpi.css остались: на них стоят
   сторожа соседних пакетов («значок оформлен в CSS самой вкладки»).
   Значения там те же; снимать их надо вместе с этими сторожами, одной
   правкой, а не по одной.
   ============================================================ */
.tk-q {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; margin-left: 6px; padding: 0;
  vertical-align: -3px; cursor: help;
  border: 1px solid var(--bg-line); border-radius: 50%;
  background: transparent; color: var(--text-muted);
  font: 600 10.5px/1 var(--font-body); flex: 0 0 auto;
}
.tk-q:hover, .tk-q:focus-visible {
  border-color: var(--brand-primary-soft); color: var(--text-primary);
  background: rgba(122, 138, 168, .12); outline: none;
}
.org-tip {
  position: fixed; z-index: var(--z-top); max-width: 320px;
  padding: 9px 11px; border-radius: 8px;
  border: 1px solid var(--bg-line);
  background: var(--bg-surface-soft);
  color: var(--text-secondary);
  font-size: 12.5px; line-height: 1.5;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
  pointer-events: none;
  opacity: 0; transform: translateY(2px);
  transition: opacity .12s ease, transform .12s ease;
}
.org-tip.show { opacity: 1; transform: none; }


/* ============================================================
   «НЕ СОХРАНИЛОСЬ» В ФОРМЕ — ОБЩИЙ СЛОЙ (17.09.2026)

   Коробку `.sp-err` рисует `showFormError` из `src/core/api-write.js`,
   то есть КАЖДАЯ форма записи на сайте: «Неделя», OKR, Кодекс,
   «Обратная связь», задачи. Имя у класса досталось от «Недели»
   (первой формой записи была недельная цель), а правило до 17.09
   лежало в `sprint.css` — файле ОДНОЙ вкладки. Итог: «Обратной связи»
   пришлось завести свою копию тех же семи свойств (`.fbe-form .sp-err`
   в feedback.css), а остальные вкладки показывали отказ сервера
   неразмеченным абзацем. Это единственное место, где человек узнаёт,
   что правка НЕ сохранилась, — показывать его как обычный текст
   дороже всего.

   Место правила — здесь, по той же причине, что у `.org-tip` выше:
   ui.css грузится всегда, а элемент может появиться на любой вкладке.
   ============================================================ */
.sp-err {
  margin: 12px 0 0; padding: 10px 12px; border-radius: var(--r-md);
  border: 1px solid var(--status-behind); background: rgba(255, 84, 112, .08);
  color: var(--text-primary); font-size: var(--fs-sm); line-height: 1.5;
}
.sp-err ul { margin: 6px 0 0; padding-left: 18px; }
.sp-err li { margin: 2px 0; }
.sp-err b { color: var(--status-behind); }


/* ============================================================
   Замок прокрутки под полноэкранным слоем.
   Класс вешает JS при открытии боковой панели, модалки, ops-drawer
   и полноэкранного экрана миссии. Без него свайп по нескроллящейся
   части оверлея (шапке, отступам, затемнению) прокручивает страницу
   ПОД ним, и после закрытия человек оказывается в другом месте
   списка, не понимая, что произошло.
   overscroll-behavior на самих скроллерах — в mobile.css §0.
   ============================================================ */
html.overlay-open,
html.overlay-open body {
  overflow: hidden;
  overscroll-behavior: none;
}


/* ============================================================
   Скелет загрузки (2026-09-01).

   До этого патча лоадер (#loader) — непрозрачная заглушка во весь
   экран с z-index модалки — снимался ПОСЛЕДНЕЙ строкой boot: после
   загрузки данных и после init всех десяти вкладок. Всё это время
   человек смотрел в чёрное и не знал, работает ли сайт вообще.

   Теперь лоадер уходит сразу после сборки оболочки: видны шапка,
   поиск и кнопки вкладок (они уже нажимаются), а на месте содержимого
   — эти серые полосы. Секции вкладок в это время скрыты: показывать
   полупустую вкладку и дорисовывать её на глазах хуже, чем честно
   сказать «грузим».
   ============================================================ */
.app-main.booting > .view { display: none !important; }

#boot-skeleton {
  width: 100%;
  max-width: 1400px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
#boot-skeleton .sk-row {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
}
#boot-skeleton .sk {
  border-radius: var(--r-md);
  background: linear-gradient(
    100deg,
    var(--bg-surface) 0%,
    var(--bg-line-soft) 40%,
    var(--bg-surface) 80%
  );
  background-size: 300% 100%;
  animation: sk-shimmer 1.6s linear infinite;
}
#boot-skeleton .sk-pill  { height: 34px; width: 132px; border-radius: var(--r-pill); }
#boot-skeleton .sk-hero  { height: 220px; }
#boot-skeleton .sk-card  { height: 96px; flex: 1 1 220px; }
#boot-skeleton .sk-line  { height: 14px; max-width: 62%; }
#boot-skeleton .sk-line.short { max-width: 38%; }

@keyframes sk-shimmer {
  from { background-position: 150% 0; }
  to   { background-position: -150% 0; }
}

/* Бегущий блик — ровно тот случай, ради которого существует
   prefers-reduced-motion. Полосы остаются, движение уходит. */
@media (prefers-reduced-motion: reduce) {
  #boot-skeleton .sk { animation: none; }
}

/* ============================================================
   Групповые чаты Telegram (src/core/ui/tg-chats.js)

   Переехали сюда из tasks.css (2026-09-17, мега-ревью «новое»): блок
   вынесен в общий модуль и его подключают и на вкладке «Панель»
   (Задачник → Уведомления теперь не единственный потребитель), а
   tasks.css на «Панели» не грузится — общий CSS должен лежать в
   файле, который грузится всегда.
   ============================================================ */
.tkg { margin-top: 18px; }
.tkg-list { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }

.tkg-chat {
  border: 1px solid var(--bg-line); border-radius: 12px; padding: 12px 14px;
  background: var(--bg-surface); display: flex; flex-direction: column; gap: 7px; align-items: flex-start;
}
/* Заявка от бота выделена рамкой, а не только словом: она обязана
   найтись взглядом раньше остального списка. */
.tkg-chat.is-wait { border-color: var(--status-risk); background: rgba(255, 181, 71, .06); }

.tkg-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
.tkg-name { font-size: 14px; color: var(--text-primary); font-weight: 500; }
.tkg-kind { font-size: 11.5px; color: var(--text-muted); }

.tkg-pill { font-size: 11px; border-radius: 999px; padding: 2px 9px; border: 1px solid transparent; }
.tkg-pill.on   { color: var(--status-on-track); border-color: rgba(63, 224, 197, .35); }
.tkg-pill.wait { color: var(--status-risk);     border-color: rgba(255, 181, 71, .45); }
/* «Вся компания» — предупреждение, а не ярлык: у чата без фильтров с
   пометкой «все поводы» события уходят по всей организации. */
.tkg-pill.wide { color: var(--status-behind);   border-color: rgba(255, 84, 112, .45); }

.tkg-meta { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--text-muted); }
.tkg-id { font-family: var(--font-mono, monospace); font-size: 11.5px; }
.tkg-purpose { margin: 0; font-size: 12.5px; color: var(--text-secondary); }

.tkg-wait-box { margin-top: 12px; padding: 12px 14px; border: 1px dashed var(--status-risk); border-radius: 12px; }
.tkg-wait-h { margin: 0 0 4px; font-size: 13.5px; color: var(--status-risk); font-weight: 500; }
.tkg-wait-box .tkg-list { margin-top: 10px; }

.tkg-empty { margin-top: 12px; padding: 14px; border: 1px dashed var(--bg-line); border-radius: 12px; }
.tkg-empty p { margin: 0 0 6px; font-size: 13.5px; }
.tkg-empty .muted { color: var(--text-muted); font-size: 13px; }
.tkg-empty code, .tkg code { font-family: var(--font-mono, monospace); color: var(--accent-cyan); font-size: 12px; }

.tkg-form {
  margin-top: 14px; padding: 14px; border: 1px solid var(--brand-primary-soft); border-radius: 12px;
  display: flex; flex-direction: column; gap: 12px;
}
.tkg-field { display: flex; flex-direction: column; gap: 5px; border: 0; padding: 0; margin: 0; }
.tkg-label { font-size: 12.5px; color: var(--text-muted); }
.tkg-form input[type="text"], .tkg-form select {
  background: var(--bg-surface); color: var(--text-primary);
  border: 1px solid var(--bg-line); border-radius: 8px; padding: 7px 10px; font-size: 13px;
}
.tkg-check, .tkg-radio, .tkg-ev { display: flex; gap: 8px; align-items: center; font-size: 13px; cursor: pointer; }
.tkg-evs { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 4px; }
.tkg-ev input:disabled + span { opacity: .45; }
.tkg-filters { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }

/* Предупреждение живёт В ФОРМЕ и видно всегда, а не всплывает по
   наведению: молчаливая настройка «всем всё» — это утечка, которая
   выглядит как удобство, и заметить её в чате некому. */
.tkg-warn {
  margin: 0; padding: 10px 12px; border-radius: 10px; font-size: 12.5px; line-height: 1.5;
  color: var(--text-secondary); border: 1px solid rgba(255, 181, 71, .45); background: rgba(255, 181, 71, .07);
}
.tkg-acts { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.tkg-del { color: var(--status-behind); margin-left: auto; }
.tkg-confirm { display: inline-flex; gap: 8px; align-items: center; font-size: 12.5px; color: var(--text-muted); margin-left: auto; }

@media (max-width: 720px) {
  .tkg-filters { grid-template-columns: 1fr; }
  .tkg-del, .tkg-confirm { margin-left: 0; }
}

/* ============================================================
   ГРАФИКИ core/charts.js (line / bars / donut) — общий слой.

   Переехали сюда из mission.css (2026-09-17, мега-ревью, находка
   «новое-2»): `core/charts.js` зовут ДВЕ вкладки — Mission Map и OKR
   (`Charts.line`/`Charts.donut` в features/okr/index.js), — а `VIEW_CSS`
   в registry.js даёт OKR только okr.css и sprint.css. Пока база лежала
   в mission.css, человек, открывший OKR напрямую (не заходя на Mission
   Map), видел график без `fill:none` на линии и донат с легендой под
   кругом — ровно тот же класс бага, что и у `.tkg*` в tasks.css.
   Общее у двух вкладок — в общем файле, тема вкладки — в её файле.
   ============================================================ */
/* ---------- Inline SVG line chart ---------- */
.chart-wrap { margin: 6px 0 4px; }
/* background/border здесь НЕТ намеренно: «стекло» v3.1 задаёт их в
   mission.css (там же, дважды — R-230 оставил победившую копию), и на
   Mission Map она применяется поверх этой базы, потому что mission.css
   грузится позже. На OKR графику достаётся только база — это и есть
   разделение «общее / тема вкладки». */
.chart-svg {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--r-md);
}
.ch-guide  { stroke: var(--bg-line-soft); stroke-width: 0.6; }
.ch-line   { fill: none; stroke-width: 2; vector-effect: non-scaling-stroke; }
.ch-line.dashed { stroke-dasharray: 5 5; opacity: 0.75; }
.ch-dot    { /* точки факта */ }
.ch-xlabel { fill: var(--text-muted); font-size: 11px; font-family: var(--font-mono); }
.ch-ylabel { fill: var(--text-muted); font-size: 9.5px; font-family: var(--font-mono); opacity: 0.85; }
.ch-barval { fill: var(--text-secondary); font-size: 9px; font-family: var(--font-mono); }
.chart-legend { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 6px; }
.ch-leg { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-secondary); }
.ch-leg i { width: 12px; height: 3px; border-radius: 2px; display: inline-block; }

/* ---------- Donut ---------- */
.donut-wrap { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.donut-svg { width: 150px; height: 150px; flex: 0 0 auto; }
.donut-center { fill: var(--text-primary); font-family: var(--font-mono); font-size: 15px; }
.donut-center-sub { fill: var(--text-muted); font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; }
.donut-legend { display: flex; flex-direction: column; gap: 6px; flex: 1 1 160px; }
.donut-leg-row { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); }
.donut-leg-dot { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.donut-leg-name { color: var(--text-secondary); flex: 1 1 auto; }
.donut-leg-val { font-family: var(--font-mono); color: var(--text-primary); }
