/* Переходы между экранами, движение внутри экрана и фоновый слой картин.
   Часть CRM аренды. ПОРЯДОК ПОДКЛЮЧЕНИЯ ВАЖЕН: после 11-showcase.css и до
   17-mobile-input.css. Правила ниже перекрывают правила выше.

   Разметку слоёв и классы ставит web/js/21-transitions.js.

   Два правила, от которых зависит всё остальное в файле:

   1. КОНЕЧНОЕ СОСТОЯНИЕ — СОСТОЯНИЕ ПО УМОЛЧАНИЮ. Ни один элемент не спрятан
      «до анимации». 09-responsive.css гасит анимации через
      `animation: none !important` при prefers-reduced-motion, и элемент с
      `opacity: 0` в обычном правиле остался бы невидимым навсегда.
   2. ДВИЖЕНИЕ ВКЛЮЧАЕТСЯ АТРИБУТОМ `html[data-motion='on']`. Его ставит
      21-transitions.js по единственному источнику правды — выключателю
      атмосферы плюс prefers-reduced-motion (scMoving() из 20-showcase.js).
      Поэтому «выключить движение» гасит и то, что media-запрос не видит.

   Цвет — только токены из 01-tokens.css, ни одного нового значения цвета.
   Анимируются только transform, opacity, stroke-*, background-position;
   `transition: all` в файле не встречается. */

:root {
  /* Кривые — те же, что у витрины: один почерк на всю систему.
     Псевдонимы, а не копии значений: источник остаётся в 11-showcase.css. */
  --t-ease-out: var(--sc-ease-out); /* вход и появление */
  --t-ease-move: var(--sc-ease-move); /* переезд и уход */

  --t-exit: 140ms; /* уход старого содержимого */
  --t-enter: 320ms; /* приход панели, нижняя граница вилки 320–420 мс */
  --t-step: 50ms; /* шаг каскада, вилка 40–60 мс */
  --t-row: 20ms; /* шаг строк таблицы */
  --t-press: 120ms; /* нажатие кнопки */
  --t-hover: 180ms; /* подъём карточки под курсором */
  --t-sheet: 220ms; /* диалог и лист */
  --t-toast-in: 220ms;
  --t-toast-out: 160ms;
  --t-tick: 300ms; /* обновление цифры при смене месяца или объекта */
  --t-art: 600ms; /* кросс-фейд фоновой картины */
  --t-breath: 40s; /* «дыхание» фона */

  /* Каскад витрины приводится к общей вилке ТЗ (было 520/60 мс). Набег числа
     (--sc-count) не меняется: на него завязано свечение после посадки. */
  --sc-enter: 400ms;
  --sc-step: var(--t-step);
}

/* ============================================================
   1. Фоновые картины (ART-1)
   ============================================================ */

/* Слой лежит под canvas частиц: 21-transitions.js вставляет его первым
   элементом <body>, а 19-atmosphere.js свой canvas — вторым, и при равном
   z-index порядок в разметке решает спор. Вылет на 16px по кругу нужен
   параллаксу: сдвиг до 8px не должен обнажать край картинки. */
.art-layer {
  position: fixed;
  inset: -16px;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  /* Параллакс приходит переменными от курсора; по умолчанию сдвига нет. */
  transform: translate3d(var(--art-x, 0px), var(--art-y, 0px), 0);
  /* Плотность картинки. Вилка 0,35–0,6; больше — только там, где текста на
     самой земле нет. Зоны ставит 21-transitions.js. */
  --art-strength: 0.42;
}

/* Вход и витрина инвестора — самые убедительные экраны системы: по ним судят
   о проекте, и текста прямо на земле там почти нет. */
.art-layer[data-zone='rich'] {
  --art-strength: 0.5;
}

/* Экраны ввода: картина остаётся, но гасится — управляющий работает. */
.art-layer[data-zone='input'] {
  --art-strength: 0.35;
}

/* Потолок плотности для двух самых светлых картин серии. Зона говорит, какой
   плотности мы ХОТИМ, картина — какую она может ВЫДЕРЖАТЬ; min() берёт
   меньшее, поэтому поправка не может сделать экран светлее зоны.
   Мерилось по собранному кадру: у `constellation` есть точки-звёзды почти
   в белый, у `bokeh-squares` — крупные светлые квадраты; на плотности 0,5
   самый светлый пиксель давал --mist 4,28:1 и 4,58:1 против нужных 4,5:1.
   Остальные четыре картины тёмные, потолок им не нужен. */
.art-frame[data-art='constellation'] {
  --art-cap: 0.42;
}

.art-frame[data-art='bokeh-squares'] {
  --art-cap: 0.46;
}

.art-frame {
  position: absolute;
  inset: 0;
  display: block;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity var(--t-art) var(--t-ease-out);
  transform-origin: 50% 40%;
}

.art-frame--on {
  opacity: min(var(--art-strength), var(--art-cap, 1));
}

/* Пелена поверх картины: она держит контраст текста, поэтому рисуется всегда,
   независимо от движения и настроек. */
.art-layer:after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--art-veil);
}

/* Шесть картин серии. Пути собраны здесь, а не в разметке: адреса файлов —
   дело оформления. Таблица назначения — web/assets/art/README.md. */
.art-frame[data-art='login-night'] {
  background-image: url('/assets/art/login-soft.jpg');
}

/* Спокойный статичный материал Higgsfield только в панели входа.
   Форму не перекрывает; геометрия и шрифты остаются прежними. */
.login-art {
  background-image: url('/assets/art/login-soft.jpg');
  background-position: center;
  background-size: cover;
}

.art-frame[data-art='nebula'] {
  background-image: url('/assets/art/nebula.jpg');
}

.art-frame[data-art='aurora'] {
  background-image: url('/assets/art/aurora.jpg');
}

.art-frame[data-art='bokeh-squares'] {
  background-image: url('/assets/art/bokeh-squares.jpg');
}

.art-frame[data-art='dust-rays'] {
  background-image: url('/assets/art/dust-rays.jpg');
}

.art-frame[data-art='constellation'] {
  background-image: url('/assets/art/constellation.jpg');
}

/* Телефон получает версию 960px — она втрое легче.
   Почему media-запрос, а не image-set(): у нас две версии по ШИРИНЕ ЭКРАНА,
   а image-set() выбирает по плотности пикселей устройства. На телефоне с
   плотностью 3 он честно взял бы тяжёлую версию — то есть сделал ровно
   обратное задуманному. Media-запрос — правильный инструмент для этой пары. */
@media (max-width: 780px) {
  .art-frame[data-art='login-night'] {
    background-image: url('/assets/art/login-soft.jpg');
  }

  .art-frame[data-art='nebula'] {
    background-image: url('/assets/art/nebula-960.jpg');
  }

  .art-frame[data-art='aurora'] {
    background-image: url('/assets/art/aurora-960.jpg');
  }

  .art-frame[data-art='bokeh-squares'] {
    background-image: url('/assets/art/bokeh-squares-960.jpg');
  }

  .art-frame[data-art='dust-rays'] {
    background-image: url('/assets/art/dust-rays-960.jpg');
  }

  .art-frame[data-art='constellation'] {
    background-image: url('/assets/art/constellation-960.jpg');
  }
}

/* «Дыхание»: 1,00 → 1,04 за 40 секунд и обратно. Сорок секунд — не эффект,
   а отсутствие статики: заметить движение нельзя, заметить его отсутствие —
   можно. Только transform, слой уходит на композитор. */
@keyframes art-breath {
  from {
    transform: scale(1);
  }

  to {
    transform: scale(1.04);
  }
}

html[data-motion='on'] .art-frame--on {
  animation: art-breath var(--t-breath) ease-in-out infinite alternate;
}

/* Параллакс — только там, где есть настоящий курсор. Догоняющий переход
   вместо пружины: библиотек нет, а 400 мс на ease-out дают ту же мягкость без
   резинового возврата. */
html[data-motion='on'] .art-layer {
  transition: transform 400ms var(--t-ease-out);
}

/* ============================================================
   2. Переход между разделами
   ============================================================ */

/* Старое содержимое не уничтожается на месте, а переезжает в этот слой и
   уходит уже из него. Так новый экран успевает прийти, пока прежний гаснет,
   и ни один запрос не ждёт анимацию. Слой живёт в .main, а не в #content:
   иначе наблюдатели атмосферы приняли бы уходящую копию за новый экран. */
.main {
  position: relative;
}

.t-ghost {
  position: absolute;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
  opacity: 1;
}

.t-ghost--out {
  opacity: 0;
  transform: translateY(-8px);
  transition:
    opacity var(--t-exit) var(--t-ease-move),
    transform var(--t-exit) var(--t-ease-move);
}

/* Общая заготовка прихода. Смещение 14px: заметно как направление, но не как
   поездка. Оба свойства — transform и opacity, ни одного расчёта раскладки. */
@keyframes t-rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
}

@keyframes t-fade {
  from {
    opacity: 0;
  }
}

/* Отменяем прежний общий вход всей страницы: теперь приходят панели по очереди,
   а не блок целиком. Иначе страница гасла бы дважды. */
.page {
  animation: none;
}

/* Каскад: панели экрана приходят по очереди с шагом 50 мс. Индекс --ti
   ставит 21-transitions.js и ограничивает четвёркой — на пятой панели
   суммарная длительность перехода перешла бы 450 мс. Дальние панели делят
   последнюю задержку: они всё равно ниже первого экрана. */
html[data-motion='on'] .t-enter > * {
  animation: t-rise var(--t-enter) var(--t-ease-out) both;
  animation-delay: calc(var(--ti, 0) * var(--t-step));
}

/* Экраны ввода и обновление того же экрана: приход без каскада и без сдвига,
   200 мс на прозрачность. Управляющий вносит показания по 28 квартирам —
   здесь отклик дороже красоты, и поле не должно ехать под курсором. */
html[data-motion='on'] .t-enter-flat {
  animation: t-fade 200ms var(--t-ease-out) both;
}

/* Заголовок витрины поднимается вместе с её собственным каскадом. */
html[data-motion='on'] .sc-play > .page-heading {
  animation: t-rise var(--t-enter) var(--t-ease-out) both;
}

/* ============================================================
   3. Загрузка: полоса вместо кружка, скелеты вместо пустоты
   ============================================================ */

/* Тонкая полоса под шапкой. В потоке между .topbar и #content, отрицательный
   нижний отступ гасит её вклад в раскладку: появление полосы ничего не
   сдвигает. Дорожка видна всегда, пока идёт запрос, — даже при выключенном
   движении: это единственный знак того, что система работает. */
.t-bar {
  display: block;
  position: relative;
  height: 2px;
  margin-bottom: -2px;
  overflow: hidden;
  background: var(--violet-tint);
  opacity: 0;
  transition: opacity 120ms var(--t-ease-out);
}

.t-bar--on {
  opacity: 1;
}

.t-bar:after {
  content: '';
  position: absolute;
  inset: 0;
  width: 32%;
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, transparent, var(--violet), transparent);
  box-shadow: var(--glow-violet);
  transform: translateX(-110%);
}

@keyframes t-sweep {
  from {
    transform: translateX(-110%);
  }

  to {
    transform: translateX(420%);
  }
}

/* linear — постоянное движение: у бегунка нет ни начала, ни конца. */
html[data-motion='on'] .t-bar--on:after {
  animation: t-sweep 1.1s linear infinite;
}

/* При выключенном движении полоса остаётся видимой дорожкой без бегунка:
   знак работы системы сохраняется, движения нет. */
html[data-motion='off'] .t-bar--on {
  background: var(--violet-edge);
}

/* Скелеты. Ставятся, только если запрос идёт дольше 250 мс: на быстром ответе
   их мелькание было бы хуже пустоты. Держат высоту панелей, поэтому при
   появлении настоящего содержимого ничего не прыгает. */
.loading--slow {
  padding: 20px 0 0;
  text-align: left;
}

/* Слово «Загружаем данные…» остаётся для скринридера и для человека, но
   становится подписью над скелетами, а не единственным содержимым экрана. */
.loading--slow > .t-skeleton {
  margin-top: 18px;
}

.t-skeleton {
  display: grid;
  gap: 20px;
}

.t-skeleton-panel {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-1);
  padding: 22px;
  display: grid;
  gap: 14px;
}

.t-skeleton-line {
  height: 14px;
  border-radius: var(--radius-sm);
  background: var(--raised);
  background-image: linear-gradient(90deg, transparent, var(--state-hover), transparent);
  background-size: 220% 100%;
  background-position: 120% 0;
}

.t-skeleton-line:first-child {
  height: 20px;
  width: 42%;
}

.t-skeleton-line:last-child {
  width: 68%;
}

@keyframes t-shimmer {
  from {
    background-position: 120% 0;
  }

  to {
    background-position: -120% 0;
  }
}

html[data-motion='on'] .t-skeleton-line {
  animation: t-shimmer 1.4s linear infinite;
}

/* ============================================================
   4. Внутри экрана
   ============================================================ */

/* Строки таблицы приходят рядами: шаг 20 мс, первые двенадцать. Тринадцатая и
   дальше видны сразу — ждать конца каскада, чтобы прочитать длинный журнал,
   никто не будет. Задержки перечислены правилами, а не переменной: индекс
   строки в разметку не попадает, и ставить его скриптом на каждую строку
   значило бы тронуть таблицы, которые рисуют чужие модули. */
@keyframes t-row {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

html[data-motion='on'] .t-enter tbody tr:nth-child(-n + 12) {
  animation: t-row 220ms var(--t-ease-out) both;
}

html[data-motion='on'] .t-enter tbody tr:nth-child(1) {
  animation-delay: 0ms;
}

html[data-motion='on'] .t-enter tbody tr:nth-child(2) {
  animation-delay: calc(1 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(3) {
  animation-delay: calc(2 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(4) {
  animation-delay: calc(3 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(5) {
  animation-delay: calc(4 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(6) {
  animation-delay: calc(5 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(7) {
  animation-delay: calc(6 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(8) {
  animation-delay: calc(7 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(9) {
  animation-delay: calc(8 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(10) {
  animation-delay: calc(9 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(11) {
  animation-delay: calc(10 * var(--t-row));
}

html[data-motion='on'] .t-enter tbody tr:nth-child(12) {
  animation-delay: calc(11 * var(--t-row));
}

/* Карточки и панели под курсором: подъём на 2px и свечение кромки.
   Только там, где курсор настоящий: на телефоне :hover залипает после
   касания и плитка осталась бы поднятой. */
@media (hover: hover) and (pointer: fine) {
  .panel,
  .building,
  .unit-tile,
  .metric,
  .sc-object,
  .sc-flat,
  .workflow-step {
    transition:
      transform var(--t-hover) var(--t-ease-out),
      border-color var(--t-hover) ease,
      box-shadow var(--t-hover) ease;
  }

  .building:hover,
  .unit-tile:hover,
  .sc-flat:hover {
    transform: translateY(-2px);
    border-color: var(--violet-edge);
    box-shadow: var(--shadow-1), var(--glow-violet);
  }

  /* Панель — не орган управления: она приподнимается, но не светится
     фиолетовым, иначе читалась бы как нажимаемая. */
  .panel:hover,
  .sc-object:hover {
    transform: translateY(-2px);
    border-color: var(--border-strong);
  }
}

/* Нажатие: короткое сжатие на 120 мс. Transition, а не keyframes, — кнопку
   можно нажать дважды в секунду, и анимация должна догонять текущее
   состояние, а не начинать заново. */
html[data-motion='on'] button,
html[data-motion='on'] summary {
  transition: transform var(--t-press) var(--t-ease-out);
}

html[data-motion='on'] button:active:not(:disabled),
html[data-motion='on'] summary:active {
  transform: scale(0.98);
}

/* Просрочка дышит кромкой: 2,4 с — медленнее, чем взгляд успевает
   зацепиться, но достаточно, чтобы плашку нельзя было не заметить.
   Ореол лежит на отдельном слое и мигает прозрачностью, а не тенью: анимация
   box-shadow заставляла бы браузер перерисовывать плашку каждый кадр.
   Пульсирует свет, не цвет: цвет просрочки уже занят смыслом. */
@keyframes t-throb {
  0%,
  100% {
    opacity: 0;
  }

  50% {
    opacity: 1;
  }
}

html[data-motion='on'] .chip--overdue {
  position: relative;
}

html[data-motion='on'] .chip--overdue:after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  box-shadow: var(--glow-ember);
  pointer-events: none;
  opacity: 0;
  animation: t-throb 2.4s ease-in-out infinite;
}

/* Обновление цифры при смене месяца или объекта — «тик» 300 мс: короткая
   вспышка ореола у значения. Сам набег цифр остаётся за атмосферой: её
   накладка уже держит число, и вторая копия поверх дала бы две суммы.
   Единственное место в файле, где анимируется не transform/opacity: ореол
   идёт по базовой линии текста, и отдельным слоем его не поставить. Тем же
   приёмом сделано свечение после посадки числа (.sc-hero-value, sc-land):
   одна короткая строка, 300 мс, один элемент на экране. */
@keyframes t-tick {
  0% {
    text-shadow: none;
  }

  40% {
    text-shadow: var(--glow-window-text);
  }

  100% {
    text-shadow: none;
  }
}

html[data-motion='on'] .t-ticked {
  animation: t-tick var(--t-tick) var(--t-ease-out) both;
}

/* Окна квартир у сотрудника — та же сетка, что у инвестора (WindowGrid).
   Каскад зажигания берётся из 11-showcase.css: класс .sc-play ставит
   21-transitions.js и на рабочих экранах. */
.window-grid-group {
  margin-top: 22px;
}

.window-grid-group:first-of-type {
  margin-top: 6px;
}

.window-grid-group h3 {
  font-size: var(--text-h3);
  color: var(--pearl);
  margin: 0 0 4px;
}

.window-grid-group .sc-counts {
  margin: 0 0 12px;
}

/* На телефоне занятость дома не влезает в одну строку, и точка-разделитель
   при переносе оказывалась в начале следующей строки («· Жилец не записан»).
   Поэтому здесь факты идут столбиком, каждый своей строкой, без разделителя. */
@media (max-width: 780px) {
  .window-grid-group .sc-counts {
    flex-direction: column;
    gap: 2px;
  }

  .window-grid-group .sc-counts > span + span:before {
    content: none;
  }
}

/* ============================================================
   5. Диалоги, листы и тосты
   ============================================================ */

/* Лист поднимается и подрастает с 0,98 до 1. Не с нуля: из ничего в жизни
   ничего не появляется. Центрированное окно не привязано к кнопке, поэтому
   transform-origin остаётся в центре. */
@keyframes t-sheet-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.98);
  }
}

.detail-dialog {
  animation: t-sheet-in var(--t-sheet) var(--t-ease-out);
}

html[data-motion='off'] .detail-dialog {
  animation: none;
}

/* Подложка: размытие только на мониторе. На телефоне backdrop-filter
   пересчитывается каждый кадр поверх живого canvas и греет устройство
   управляющего — там затемнение без стекла. */
.detail-dialog::backdrop {
  background: var(--overlay);
  backdrop-filter: none;
}

@media (min-width: 781px) {
  .detail-dialog::backdrop {
    backdrop-filter: blur(6px);
  }
}

/* Тост. Въезд 220 мс, выезд 160 мс: асимметрия там, где решает система, —
   появление можно рассмотреть, исчезновение не должно задерживать взгляд.
   display больше не переключается: у выключенного через display элемента
   нет ни въезда, ни выезда. visibility убирает его и из дерева доступности. */
#toast {
  display: block;
  visibility: hidden;
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity var(--t-toast-out) var(--t-ease-out),
    transform var(--t-toast-out) var(--t-ease-out),
    visibility 0s linear var(--t-toast-out);
}

#toast.visible {
  visibility: visible;
  opacity: 1;
  transform: none;
  transition:
    opacity var(--t-toast-in) var(--t-ease-out),
    transform var(--t-toast-in) var(--t-ease-out),
    visibility 0s;
}

/* ============================================================
   6. Фокус, курсор, отсутствие прыжков
   ============================================================ */

/* Рамка фокуса остаётся: свечение вместо неё не даёт 3:1 к любой поверхности
   и на плотной таблице оплаты его не видно. Свечение добавляется К рамке —
   тем же токеном, которым светятся живые элементы. */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  box-shadow: var(--glow-violet);
}

/* Курсор-указатель — только на том, что действительно нажимается.
   Отключённая кнопка указателем не притворяется. */
button:not(:disabled),
summary,
[role='button'],
label.switch,
a[href] {
  cursor: pointer;
}

button:disabled {
  cursor: default;
}

/* Ничего не прыгает при загрузке: место под панели и под полосу загрузки
   зарезервировано заранее. Экран обхода показаний и оплаты — в первую
   очередь: там высота списка известна до ответа сервера. */
#content {
  min-height: 60vh;
}

/* ============================================================
   7. Движение выключено
   ============================================================ */

/* Выключатель движения гасит всё, что не видит media-запрос. Правила выше
   написаны так, что достаточно снять `data-motion='on'`: анимаций просто не
   возникает. Здесь остаются только переходы, которые живут вне этого атрибута
   (кросс-фейд фона, тост, нажатие) — их нужно погасить явно. */
html[data-motion='off'] .art-frame {
  transition: none;
}

html[data-motion='off'] .t-ghost--out {
  transition: none;
}

html[data-motion='off'] #toast,
html[data-motion='off'] #toast.visible {
  transition: visibility 0s;
}

/* Системная настройка — поверх всего. 09-responsive.css уже снимает все
   animation и transition через !important; здесь остаётся то, что он снять
   не может: параллакс фона задан не анимацией, а переменной. */
@media (prefers-reduced-motion: reduce) {
  .art-layer {
    transform: none;
  }
}
