/* ============================================================================
   Движение «Обзора дня» и оболочки пульта — «Стекло» (12.09.2026).
   ----------------------------------------------------------------------------
   Отдельный файл, а не строки в `app.css`, по трём причинам.

   1. `app.css` и `overview.js` лежат в CRLF, а весь остальной пульт — в LF.
      Правка CRLF-файла из LF-инструмента даёт дифф на семь тысяч строк, в
      котором ревью не увидит ни одной настоящей.
   2. Движение — это система, а не украшение отдельного экрана. Половина
      правил ниже (`.g-gloss`, `gDeal`, `gDot`, `gDraw`, `gFlash`, `gShake`)
      ОБЩАЯ: ею пользуются и «Обзор», и «Заказы» (`orders.css`). Одна форма
      движения на весь продукт читается как система; шесть разных — как набор
      эффектов.
   3. Экран «Инбокс» уже перенесён (`inbox.css`, раздел «движение») и служит
      образцом. Здесь тот же язык и те же длительности, а не вторая система.

   ГРАНИЦЫ (чек-лист макета, `design/pult-glass-2026-09/README.md`,
   «Решения (12.09) — движение и фирменные детали обязательны»):
     · в кадре живут только `transform`/`opacity` и независимые `translate`,
       `rotate`, `scale`; `filter` и `box-shadow` не анимируются никогда.
       Единственное исключение — `stroke-dashoffset` рисующейся линии, оно
       названо в чек-листе прямо;
     · микроанимации ≤ 300 мс, декоративные циклы ≥ 6 с;
     · `prefers-reduced-motion: reduce` гасит всё — блок в конце файла;
     · на 390 px параллакса и расхождения веера нет: палец наводится не так,
       как мышь, и «наведение» на нём означает нажатие.

   ЧТО ЗДЕСЬ НЕ ЖИВЁТ. Всё, что нужно ставить классом по данным (зажечь цифру
   ТОЛЬКО при смене показания, раздать колоду ТОЛЬКО на входе в экран,
   подвинуть подчёркивание таба), живёт в `overview-motion.js`: css не знает,
   изменилось ли число, а мигать на каждом опросе раз в тридцать секунд —
   значит через час перестать что-либо означать.
   ========================================================================= */

:root {
  /* Бегущий блик по стеклу под курсором. Отдельная роль: статический блик
     кромки (`--sheen`) уже занят градиентной карточкой, а этот слой ЕДЕТ.
     Имя без экранного префикса намеренно: слоем пользуются оба экрана. */
  --g-gloss: linear-gradient(
    100deg,
    transparent 18%,
    rgba(255, 255, 255, 0.1) 50%,
    transparent 82%
  );
  /* Вспышка новой строки: подсветка уходит в прозрачность за 1,8 с. */
  --g-flash: linear-gradient(90deg, var(--accent-soft-2), transparent 78%);
  /* Плашка скелетона НА градиентной карточке. Своя, а не `--ink-780`: та
     считана от фона панели, а здесь под плашкой лежит плотный градиент
     данных, одинаковый во всех трёх темах, — поэтому и значение одно. */
  --g-skel: rgba(255, 255, 255, 0.16);
  --g-skel-hi: rgba(255, 255, 255, 0.26);
}

[data-theme="light"] {
  --g-gloss: linear-gradient(
    100deg,
    transparent 18%,
    rgba(255, 255, 255, 0.7) 50%,
    transparent 82%
  );
}

[data-theme="neutral"] {
  --g-gloss: linear-gradient(
    100deg,
    transparent 18%,
    rgba(255, 255, 255, 0.58) 50%,
    transparent 82%
  );
}

/* ================================================ ОБЩИЕ КАДРЫ ДВИЖЕНИЯ
   Пять кадровых наборов на оба экрана. Названия без экранного префикса —
   это язык системы, а не эффекты «Обзора». */

/* Блик проезжает по стеклу слева направо. */
@keyframes gSheen {
  from {
    translate: -120% 0;
  }
  to {
    translate: 120% 0;
  }
}

/* Карта «раздаётся» из колоды. Позу карточки веера держат `rotate` и поля,
   поэтому в кадрах живут только `translate`, `scale` и `opacity` — три
   независимых свойства, которые складываются с наклоном сами. */
@keyframes gDeal {
  from {
    opacity: 0;
    translate: 0 18px;
    scale: 0.94;
  }
}

/* Точка цифры-матрицы зажигается. Шаг очереди даёт `--i` (ставит js). */
@keyframes gDot {
  from {
    opacity: 0;
    scale: 0.4;
  }
}

/* Линия дорисовывается слева направо. `stroke-dashoffset` — единственное
   свойство вне transform/opacity, которое чек-лист макета разрешает прямо. */
@keyframes gDraw {
  from {
    stroke-dashoffset: var(--g-dash, 320);
  }
}

@keyframes gFade {
  from {
    opacity: 0;
  }
}

/* Вспышка новой строки: появилась — и погасла, не оставшись подсветкой. */
@keyframes gFlash {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* Строка списка приезжает снизу. 20 мс шага ставит `cascade()` из app.js. */
@keyframes gRow {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* Одна тряска поля на отказе сети — и больше ничего: повторяющаяся анимация
   ошибки читается как поломка интерфейса, а не как отказ сервера. */
@keyframes gShake {
  0%,
  100% {
    transform: translateX(0);
  }
  20% {
    transform: translateX(-5px);
  }
  45% {
    transform: translateX(4px);
  }
  70% {
    transform: translateX(-2px);
  }
}

/* ======================================================= БЕГУЩИЙ БЛИК
   Слой-элемент, а не псевдоэлемент: у `.gcard` оба псевдо уже заняты
   «дымками» и статическим бликом кромки, а у `.glass` блик обязан
   ОБРЕЗАТЬСЯ по карточке, не заставляя саму карточку прятать переполнение
   (в «персоне» за край намеренно выходит свечение под знаком EVA).

   🔴 `z-index: -1` внутри контекста наложения кладёт слой НАД фоном
   карточки и ПОД её содержимым. Без этого белая полоса проезжала бы поверх
   текста и на полсекунды съедала его контраст. Контекст у `.gcard` уже
   есть (`isolation: isolate` в app.css), у `.glass` заводится ниже. */
.g-gloss {
  position: absolute;
  inset: 0;
  z-index: -1;
  display: block;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}

.g-gloss::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--g-gloss);
  translate: -120% 0;
}

#tab-overview .glass,
#tab-orders .glass {
  isolation: isolate;
}

.gcard:hover > .g-gloss::after,
.glass:hover > .g-gloss::after {
  animation: gSheen 620ms var(--ease-out) backwards;
}

/* Подъём под курсором. Отдельным свойством `translate`, а не `transform`:
   у карточек веера `transform` держит позу, и общее правило её стирало бы. */
#tab-overview .gcard,
#tab-overview .glass {
  transition: translate var(--dur-2) var(--ease);
}

#tab-overview .gcard:hover,
#tab-overview .glass:hover {
  translate: 0 -2px;
}

/* ================================================= ХЕРО: ЗНАК EVA ЖИВОЙ
   Знак плывёт вверх-вниз за 6,4 с и медленно поворачивает дуги за 22 с.
   Оба цикла длиннее шести секунд — знак «дышит», а не мигает.

   🔴 Плывут ШАР И ДУГИ, а тень на полу — нет: тень, которая ездит вместе с
   предметом, означает, что поднялся пол. Поворот и подъём — два независимых
   свойства (`rotate` и `translate`), поэтому обе анимации живут на одном
   узле и не стирают друг друга. */
@keyframes ovFloat {
  from {
    translate: 0 -4px;
  }
  to {
    translate: 0 4px;
  }
}

@keyframes ovSpin {
  to {
    rotate: 360deg;
  }
}

.eva-orb .shell-ball {
  animation: ovFloat 6.4s var(--ease) infinite alternate;
}

.eva-orb .arcs {
  animation:
    ovFloat 6.4s var(--ease) infinite alternate,
    ovSpin 22s linear infinite;
}

/* Объёмный блок из трёх шаров в карточке «EVA сейчас» дышит. Поворота у
   него нет намеренно: блик на шарах нарисован сверху слева, и поворот
   означал бы, что вокруг карточки ездит источник света. */
@keyframes ovBreathe {
  from {
    scale: 1;
  }
  to {
    scale: 1.045;
  }
}

#tab-overview .ov-now-head .blob {
  animation: ovBreathe 7.2s var(--ease) infinite alternate;
}

/* ==================================================== ВЕЕР СОБЫТИЙ ДНЯ
   Три последних события смены «в руке». Карты раздаются из колоды при входе
   на экран и при смене состава событий, расходятся под курсором, наведённая
   выходит вперёд.

   🔴 Поза карточки переехала с `transform` на ПОЛЯ и `rotate`. Иначе
   раздача (она двигает `translate` и `scale`) стирала бы сдвиг веера, и
   все три карты сошлись бы в стопку в середине анимации. Математика та же,
   что была в app.css: `left: 50%` плюс тот же сдвиг, только полем. */
#tab-overview .evafan-card {
  transform: none;
  transition:
    rotate var(--dur-2) var(--ease),
    translate var(--dur-2) var(--ease);
}

#tab-overview .evafan-card--l {
  margin-left: calc(var(--fan-shift) * -1 - var(--fan-w) * 0.5);
  rotate: calc(var(--fan-tilt) * -1);
}

#tab-overview .evafan-card--c {
  margin-left: calc(var(--fan-w) * -0.5);
}

#tab-overview .evafan-card--r {
  margin-left: calc(var(--fan-shift) - var(--fan-w) * 0.5);
  rotate: var(--fan-tilt);
}

/* Расхождение под курсором — ровно ±3°, как в чек-листе. */
#tab-overview .evafan:hover .evafan-card--l {
  rotate: calc(var(--fan-tilt) * -1 - 3deg);
}

#tab-overview .evafan:hover .evafan-card--r {
  rotate: calc(var(--fan-tilt) + 3deg);
}

/* Наведённая карта выходит вперёд: и глубиной, и подъёмом. */
#tab-overview .evafan-card:hover {
  z-index: 4;
  translate: 0 -4px;
}

/* Раздача. Класс ставит `overview-motion.js` — только на входе в экран и
   при смене состава событий, а не на каждом такте опроса. */
#tab-overview .evafan.is-dealt .evafan-card {
  animation: gDeal var(--dur-3) var(--ease-out) backwards;
}

#tab-overview .evafan.is-dealt .evafan-card--c {
  animation-delay: 0ms;
}

#tab-overview .evafan.is-dealt .evafan-card--l {
  animation-delay: 80ms;
}

#tab-overview .evafan.is-dealt .evafan-card--r {
  animation-delay: 160ms;
}

/* Вспышка НОВОГО события. Карточка уже прячет переполнение, поэтому слой
   ложится прямо на неё. */
#tab-overview .evafan-card.is-new::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background: var(--g-flash);
  animation: gFlash 1.8s var(--ease) both;
}

/* ================================================= ЦИФРЫ ИЗ ТОЧЕК ВОЛНОЙ
   Каждая точка получает свой шаг задержки (`--i` ставит js через CSSOM:
   CSP `default-src 'self'` инлайновых стилей не пускает). Зажигаем при
   появлении и при СМЕНЕ значения — неподвижная цифра в пульте означает
   «ничего не изменилось», и это правда.

   Правило ОДНО на все экраны (аудит B §5.4): раньше пять одинаковых копий
   под `#tab-*` жили в `access.css`, `orders.css`, `settings.css`,
   `status.css` и здесь, шестая — в `inbox.css`; пять мест, где однажды
   поправят четыре. Экран с другим кадром («Журнал», `auDot`) держит свою
   копию под своим идентификатором: она весит больше и побеждает. */
.dotnum.is-lit svg circle {
  transform-box: fill-box;
  transform-origin: center;
  animation: gDot 300ms var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 9ms);
}

/* ================================================ СПАРКЛАЙНЫ И ВОЛНА
   Линия дорисовывается слева направо, маркеры появляются после неё.
   Пунктирная база не рисуется: она отвечает на вопрос «относительно чего»
   и обязана стоять до того, как поедет волна. Тоже одно правило на все
   экраны: класс `is-drawn` ставит js каждого из них — и только на приезд
   данных. Спарклайны с другой длиной штриха (`#ov-wave`, `#set-wave`,
   `.kb-tile`) держат свои правила. */
.is-drawn .ovspark-line {
  --g-dash: 320;
  stroke-dasharray: 320;
  animation: gDraw 620ms var(--ease-out) backwards;
}

.is-drawn .ovspark-dot {
  animation: gFade 240ms var(--ease) 560ms backwards;
}

/* Волна расхода шире спарклайна (viewBox 220): своя длина штриха. */
#ov-wave.is-drawn .wave-line {
  --g-dash: 460;
  stroke-dasharray: 460;
  animation: gDraw 680ms var(--ease-out) backwards;
}

#ov-wave.is-drawn .wave-mark,
#ov-wave.is-drawn .wave-dot {
  animation: gFade 240ms var(--ease) 620ms backwards;
}

/* ============================================ ШКАЛА «БЕЗ ЧЕЛОВЕКА»
   Дуга заполняется при появлении и при смене доли. Длина самой дуги —
   данные (её ставит overview.js через `stroke-dasharray`), поэтому кадр
   двигает только смещение штриха. 276.5 — длина полуокружности r=88. */
#ov-gauge.is-filled .gauge-arc,
#ov-gauge.is-filled .gauge-glow {
  --g-dash: 276.5;
  animation: gDraw 720ms var(--ease-out) backwards;
}

/* ============================================ СТОЛБИКИ ПЯТИ СУТОК
   Растут снизу. Высоту задают данные (инлайн-стиль из js), кадр двигает
   только масштаб по Y — раскладка при этом не пересчитывается. */
@keyframes ovBar {
  from {
    scale: 1 0;
  }
}

#ov-bars.is-grown .stem {
  transform-origin: bottom center;
  animation: ovBar var(--dur-3) var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 40ms);
}

#ov-bars.is-grown .val,
#ov-bars.is-grown .lbl {
  animation: gFade var(--dur-2) var(--ease) 120ms backwards;
}

/* Значение прошлых суток проступает под курсором.

   🔴 Раньше его не было вовсе: число стояло только у сегодняшнего столбика, и
   «сколько было позавчера» нельзя было узнать ни глазами, ни программой чтения
   с экрана (дефект 5 ручного QA). Теперь число есть у каждого, молчит, пока не
   спросили, и названо в подсказке столбика вместе с датой. Сегодняшнее видно
   всегда — вопрос у карточки по-прежнему один. */
#ov-bars figure:not(.on) .val {
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
}

#ov-bars figure:not(.on):hover .val,
#ov-bars figure:not(.on):focus-within .val {
  opacity: 1;
}

/* Въезд значения — только у сегодняшнего: остальные обязаны молчать с первого
   кадра, а не вспыхнуть на въезде экрана и погаснуть. */
#ov-bars.is-grown figure:not(.on) .val {
  animation: none;
}

/* ================================================ ТРИ КАРТОЧКИ ДНЯ
   Раздаются из колоды при КАЖДОМ входе на экран. Класса для этого не нужно:
   спрятанная вкладка лежит в `display: none`, и браузер запускает анимацию
   заново ровно в тот момент, когда экран показали. Шаг — 80 мс, как у
   веера: это одна и та же раздача, просто карты легли сеткой. */
#tab-overview .ov-trio .gcard {
  animation: gDeal var(--dur-3) var(--ease-out) backwards;
}

#tab-overview .ov-trio .gcard:nth-child(2) {
  animation-delay: 80ms;
}

#tab-overview .ov-trio .gcard:nth-child(3) {
  animation-delay: 160ms;
}

/* ============================ СРЕДНЯЯ КОЛОНКА ВРОВЕНЬ С СОСЕДЯМИ
   Единственные правила РАСКЛАДКИ в этом файле — и они здесь потому, что
   разбираются с раскладкой конкретного экрана, а не с общей сеткой `.ov`
   (`app.css`).

   У роли «менеджер» денег в ответе нет (`spend: null`), и вместо расхода
   встаёт «Долгие ответы». Она ниже расхода — в ней нет волны по часам, —
   а обойма средней колонки прижата к верху строки (`align-items: start` у
   `.ov`): под «EVA сейчас» оставалась дыра 330×240 px, треть колонки
   (ревизия 13.09, дефект 16).

   🔴 Тянется обойма ОБЕИХ ролей, а не только менеджерская. Пока правило
   висело на `.is-lean`, у владельца дыры не было лишь потому, что самой
   высокой в строке была его же колонка. Стоило правой колонке подрасти
   (паспорт персоны, 13.09) — и под «Расходом» открылись те же 45 px пустоты.
   Высоту строки задаёт самый высокий сосед, и обойма обязана держаться его
   низа независимо от того, кто сегодня выше.

   Остаток внутри второй карточки забирает её нижний прибор: у менеджера —
   легенда каналов, у владельца — волна по часам с осью. Пустое место при
   этом оказывается между подписью и прибором, а не под карточкой.

   Класс `is-lean` ставит `overview.js` по ФАКТУ (есть ли `spend` в ответе), а
   не по роли, известной браузеру: роль в браузере — подсказка, вырезает
   сервер. Здесь он больше ничего не решает, но остаётся признаком состояния
   для смоука и для чтения разметки. */
#tab-overview .ov-stack {
  align-self: stretch;
  grid-template-rows: auto minmax(0, 1fr);
}

#tab-overview #ov-speed-card .legend,
#tab-overview #ov-spend-card .wave {
  margin-top: auto;
}

/* ========================= ПАСПОРТ ПЕРСОНЫ: НИЖНЯЯ ТРЕТЬ ПРАВОЙ КОЛОНКИ
   Второе (и последнее) место в этом файле, где живёт РАСКЛАДКА, и по той же
   причине, что и предыдущее: она разбирается с конкретным экраном, а не с
   общей сеткой `.ov` (`app.css`).

   🔴 Что было. `.persona` — колонка флексом, и всю свободную высоту забирал
   `.figure-wrap` (`flex: 1`). Знак EVA вставал в его СЕРЕДИНУ, четыре чипа
   висят абсолютно и прижаты к его ВЕРХУ — значит весь излишек высоты
   превращался в пустую полосу под знаком: ~140 px на 1440 и 1920, нижняя
   треть колонки (ревизия прода 13.09, отчёт 4 §В п.4).

   🔴 Что стало. Излишек забирает паспорт, а не воздух: у знака остаётся
   ровно его блок, а три строки фактов расходятся по остатку
   (`space-evenly`). Полосы нет ни при каком росте соседней колонки: чем выше
   строка сетки, тем шире шаг между фактами, а не тем больше пустоты.

   Высоту блока знака держит `min-height` из `app.css` (13rem): чипы в нём
   абсолютные, и без неё они вылезли бы за карточку. */
#tab-overview .persona .figure-wrap {
  flex: 0 1 auto;
}

#tab-overview .ov-facts {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  margin: var(--s4) 0 0;
  /* Полоса не растёт бесконечно: на очень высокой строке сетки шаг между
     фактами упёрся бы в «три строки по краям пустого прямоугольника». */
  max-height: 16rem;
}

#tab-overview .ov-facts > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  min-height: 2rem;
  padding: var(--s2) 0;
  border-top: 1px solid var(--line);
}

#tab-overview .ov-facts dt {
  color: var(--mute);
  font-size: var(--t-xs);
}

#tab-overview .ov-facts dd {
  margin: 0;
  font-size: var(--t-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* Строки паспорта приезжают каскадом при входе на экран — тем же шагом
   80 мс, что три карточки дня и веер событий: это одна раздача. Класса не
   нужно, спрятанная вкладка лежит в `display: none`. */
#tab-overview .ov-facts > div {
  animation: gRow var(--dur-2) var(--ease-out) backwards;
}

#tab-overview .ov-facts > div:nth-child(2) {
  animation-delay: 80ms;
}

#tab-overview .ov-facts > div:nth-child(3) {
  animation-delay: 160ms;
}

/* ======================================================= ПИЛЮЛЯ И КНОПКИ */

/* Пилюля ряда столбиков: состояние переключается плавно, шеврон
   разворачивается. 150 мс — как у чипов «Заказов». */
#ov-now-period {
  transition:
    background 150ms var(--ease),
    opacity 150ms var(--ease);
}

#ov-now-period svg {
  transition: rotate 150ms var(--ease);
}

#ov-now-period.is-alt svg {
  rotate: 180deg;
}

/* Нажатие любой кнопки экрана — короткое «приседание». */
#tab-overview button:active:not(:disabled) {
  transform: scale(0.98);
}

#tab-overview a.g-arrow:active {
  transform: scale(0.94);
}

/* Первичная кнопка экрана — «Спросить в тест-чате»: под курсором по ней
   проезжает блик. Слой на `z-index: -1` внутри своего контекста наложения,
   то есть НАД фоном кнопки и ПОД её текстом. */
#tab-overview .askbar-pill[type="submit"] {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

#tab-overview .askbar-pill[type="submit"]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--g-gloss);
  translate: -120% 0;
}

/* Блик — приглашение нажать, и погашенной кнопке он не полагается: пока поле
   пустое, «Спросить в тест-чате» выключена, и проезжающий по ней блик звал бы
   в запертую дверь. */
#tab-overview .askbar-pill[type="submit"]:hover:not(:disabled)::before {
  animation: gSheen 620ms var(--ease-out) backwards;
}

/* ========================================================== СКЕЛЕТОН
   До первого ответа `/api/overview` на месте каждой величины стоит плашка
   с пробегающим бликом, а не пустота: пустой экран читается как поломка.

   🔴 Держится на `:empty`, а не на классе. Ровно потому, что классу нужен
   был бы js в `overview.js` (CRLF, не трогаем), а `:empty` знает ровно то
   же самое: «сюда ещё ничего не положили». Как только `put()` положит
   первый узел — правило перестаёт совпадать само. */
#ov-sum:empty,
#ov-spend:empty,
#ov-unread:empty,
#ov-waiting:empty,
#ov-reply:empty,
#ov-gauge:empty,
#ov-legend:empty,
#ov-bars:empty,
#ov-events:empty,
#ov-wave:empty,
#ov-unread-spark:empty,
#ov-waiting-spark:empty {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-sm);
}

#ov-sum:empty,
#ov-spend:empty,
#ov-unread:empty,
#ov-waiting:empty,
#ov-reply:empty,
#ov-gauge:empty,
#ov-legend:empty,
#ov-wave:empty,
#ov-unread-spark:empty,
#ov-waiting-spark:empty {
  background: var(--g-skel);
}

/* Скелетоны на стекле считаются от фона панели, а не от градиента. */
#ov-bars:empty,
#ov-events:empty {
  background: var(--ink-780);
}

#ov-sum:empty {
  height: var(--dotnum-hero);
}

#ov-spend:empty {
  height: var(--dotnum-md);
}

#ov-unread:empty,
#ov-waiting:empty,
#ov-reply:empty {
  height: var(--dotnum-sm);
}

#ov-gauge:empty {
  min-height: 6.5rem;
}

#ov-legend:empty {
  height: 6.8rem;
}

#ov-events:empty {
  height: 8.6rem;
}

#ov-wave:empty {
  height: 4.5rem;
}

#ov-unread-spark:empty,
#ov-waiting-spark:empty {
  height: 2.4rem;
}

#ov-sum:empty::after,
#ov-spend:empty::after,
#ov-unread:empty::after,
#ov-waiting:empty::after,
#ov-reply:empty::after,
#ov-gauge:empty::after,
#ov-legend:empty::after,
#ov-bars:empty::after,
#ov-events:empty::after,
#ov-wave:empty::after,
#ov-unread-spark:empty::after,
#ov-waiting-spark:empty::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--g-skel-hi), transparent);
  transform: translateX(-100%);
  animation: shimmer 1.4s var(--ease) infinite;
}

/* На стекле пробег считается от токена панели: белая полоса по светлой
   теме была бы невидимой. */
#ov-bars:empty::after,
#ov-events:empty::after {
  background: linear-gradient(90deg, transparent, var(--shimmer), transparent);
}

/* ========================================================== ОБОЛОЧКА
   Три детали живут не на экране, а вокруг него, и потому описаны здесь, а
   не в `orders.css`: `app.css` в CRLF и на правку закрыт, а второй общий
   файл стилей ради трёх правил — это ещё одно место, где их будут искать. */

/* ---------------------------------------- подчёркивание активного таба
   Едет между вкладками, а не вспыхивает на новом месте. Позицию и ширину
   ставит `overview-motion.js` двумя переменными; в кадре — только
   `transform`, поэтому раскладка шапки не пересчитывается.

   🔴 Только ВЕРХНИЙ таб-бар. На телефоне тот же `.tabs` становится нижней
   панелью в `position: fixed` (app.css, адаптив), и безусловный `relative`
   отсюда перебил бы его по порядку файлов — панель уехала бы в поток. */
@media (min-width: 56.0625rem) {
  .tabs {
    position: relative;
  }

  .tabs::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 3px;
    width: 1px;
    height: 2px;
    border-radius: 2px;
    background: var(--accent);
    pointer-events: none;
    opacity: var(--nav-on, 0);
    transform: translateX(var(--nav-x, 0)) scaleX(var(--nav-w, 0));
    transform-origin: left center;
    transition:
      transform var(--dur-2) var(--ease-out),
      opacity var(--dur-1) var(--ease);
  }
}

/* ------------------------------------------------- приезд экрана
   Раздел въезжает целиком за 200 мс, а панели внутри него — каскадом
   (`enterSection` в app.js). Класса не нужно: спрятанная вкладка лежит в
   `display: none`, и анимация запускается ровно в момент показа. */
@keyframes ovTabIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

main .tab:not([hidden]) {
  animation: ovTabIn 200ms var(--ease-out) backwards;
}

/* ------------------------------------------- пульс бейджа непрочитанных
   Тот же мягкий цикл 6,4 с, что у бейджа строки в «Инбоксе»: это фоновый
   признак «вас ждут», а не сигнал тревоги. */
@keyframes ovBadge {
  0%,
  70%,
  100% {
    opacity: 0;
    transform: scale(0.9);
  }
  78% {
    opacity: 0.55;
    transform: scale(1);
  }
  92% {
    opacity: 0;
    transform: scale(1.22);
  }
}

/* 🔴 Только на широком экране. На телефоне `.nav-badge` уже
   `position: absolute` (app.css, адаптив), и безусловный `relative` отсюда
   перебил бы его по порядку файлов — бейдж уехал бы из угла вкладки в
   строку. */
@media (min-width: 56.0625rem) {
  .nav-badge {
    position: relative;
  }
}

.nav-badge::after {
  content: "";
  position: absolute;
  inset: -0.25rem;
  border-radius: var(--r-pill);
  pointer-events: none;
  background: var(--bad-soft);
  opacity: 0;
  animation: ovBadge 6.4s var(--ease) infinite;
}

/* ------------------------------------------------------- смена темы
   Фон и текст переезжают за 300 мс вместо мгновенной подмены палитры.

   🔴 Класс висит ровно на время перехода и ставится из js в тот же кадр,
   что и сам `data-theme` (MutationObserver срабатывает микрозадачей, до
   пересчёта стилей). Постоянный переход на всём документе означал бы, что
   каждое наведение в пульте длится треть секунды.

   В списке только цветовые свойства: ни одно из них не трогает раскладку. */
:root.theme-shift,
:root.theme-shift * {
  transition-property: background-color, color, border-color, outline-color, fill, stroke;
  transition-duration: 300ms;
  transition-timing-function: var(--ease);
}

/* ==================================================== АДАПТИВ ЭКРАНА */

/* 390 px: параллакса и расхождения веера нет — палец наводится не так, как
   мышь, и «наведение» на нём означает нажатие. */
@media (max-width: 26rem) {
  #tab-overview .evafan:hover .evafan-card--l {
    rotate: calc(var(--fan-tilt) * -1);
  }

  #tab-overview .evafan:hover .evafan-card--r {
    rotate: var(--fan-tilt);
  }

  #tab-overview .evafan-card:hover {
    translate: none;
  }

  #tab-overview .gcard:hover,
  #tab-overview .glass:hover {
    translate: none;
  }

}

/* Паспорт персоны в одну колонку (там же, где `.ov` складывается в столбик,
   `app.css`) идёт сразу под чипами обычным списком: раздвигать три строки по
   высоте нечем — правая колонка здесь не тянется по соседке, а стоит под
   всем остальным. Пустой полосы в этом режиме не появляется и появиться не
   может. */
@media (max-width: 56rem) {
  #tab-overview .ov-facts {
    display: block;
    max-height: none;
    margin-top: var(--s5);
  }
}

/* ================================================== ДВИЖЕНИЕ ВЫКЛЮЧЕНО
   Четвёртый уровень выключателя живёт в js (`REDUCED` из app.js): каскад
   строк и зажигание цифр ставятся классами, и css их не достанет.

   Общий блок app.css уже сокращает длительности до 0,001 мс. Здесь именно
   `animation: none` — чтобы «выключено» можно было ПРОВЕРИТЬ: смоук читает
   `animationName` и ждёт `none`, а не «идёт, но быстро». */
@media (prefers-reduced-motion: reduce) {
  .eva-orb .shell-ball,
  .eva-orb .arcs,
  .nav-badge::after,
  main .tab:not([hidden]),
  #tab-overview .ov-now-head .blob,
  #tab-overview .ov-trio .gcard,
  #tab-overview .ov-facts > div,
  #tab-overview .evafan.is-dealt .evafan-card,
  #tab-overview .evafan-card.is-new::after,
  .dotnum.is-lit svg circle,
  .is-drawn .ovspark-line,
  .is-drawn .ovspark-dot,
  #ov-wave.is-drawn .wave-line,
  #ov-wave.is-drawn .wave-mark,
  #ov-wave.is-drawn .wave-dot,
  #ov-gauge.is-filled .gauge-arc,
  #ov-gauge.is-filled .gauge-glow,
  #ov-bars.is-grown .stem,
  #ov-bars.is-grown .val,
  #ov-bars.is-grown .lbl,
  #ov-sum:empty::after,
  #ov-spend:empty::after,
  #ov-unread:empty::after,
  #ov-waiting:empty::after,
  #ov-reply:empty::after,
  #ov-gauge:empty::after,
  #ov-legend:empty::after,
  #ov-bars:empty::after,
  #ov-events:empty::after,
  #ov-wave:empty::after,
  #ov-unread-spark:empty::after,
  #ov-waiting-spark:empty::after,
  .gcard:hover > .g-gloss::after,
  .glass:hover > .g-gloss::after,
  #tab-overview .askbar-pill[type="submit"]:hover::before {
    animation: none !important;
  }

  /* Линия обязана остаться НАРИСОВАННОЙ, а не спрятанной штрихом. */
  .is-drawn .ovspark-line,
  #ov-wave.is-drawn .wave-line {
    stroke-dasharray: none;
  }

  #tab-overview .evafan-card.is-new::after {
    opacity: 0;
  }

  /* Блик и подъём — это не движение ради движения, а ответ на наведение;
     но при выключенных анимациях бегущей полосы быть не должно вовсе. */
  .g-gloss {
    display: none;
  }

  #tab-overview .gcard:hover,
  #tab-overview .glass:hover,
  #tab-overview .evafan-card:hover {
    translate: none;
  }
}
