/* ============================================================
   Система движения портала Nagano 2026.
   Подключается ПОСЛЕ styles.css — поэтому может переопределять.
   Источник правды по движению: docs/MOTION.md.

   Главное правило: анимация украшает, но никогда не прячет содержимое.
   Всё, что стартует с opacity: 0, обязано иметь путь показаться без
   анимации — через медиазапрос «меньше движения», через печать и через
   страховку в components.js.
   ============================================================ */

:root {
  /* Длительности. Три на весь портал. */
  --t-fast: .18s;   /* отклик: наведение, нажатие, раскрытие меню */
  --t-base: .3s;    /* короткое перемещение, уход, зум обложки */
  --t-slow: .55s;   /* вход элемента: появление, переход, каскад */

  /* Кривые. Три на весь портал. */
  --e-out: cubic-bezier(.22,.61,.36,1); /* вход: быстрый старт, мягкая остановка */
  --e-in: ease-in;                      /* уход */
  --e-std: ease;                        /* отклик */

  /* Шаги */
  /* Каскад появления считает JS (задержка зависит от размера группы),
     поэтому значения продублированы в components.js: REVEAL_STEP_MS,
     HAIKU_STEP_MS, HAIKU_LINE_MS. Менять — здесь и там одновременно. */
  --stagger: 80ms;  /* каскад блоков: 40–120 мс, группа из 3–8 элементов */
  --hk-step: .6s;   /* хокку: шаг чтения по строкам, не каскад (см. MOTION.md) */

  /* styles.css завёл собственный токен на подъём поверхности при наведении.
     Рецепт подъёма (сдвиг, тень, рамка) остаётся его, скорость берём отсюда —
     чтобы длительностей на портале было ровно три, а не четыре. */
}

/* ============================================================
   1. Появление при прокрутке (.reveal)
   Класс вешает revealOnScroll() в components.js — и только на четыре
   типа блоков, а не на всё подряд. Разметка про это ничего не знает.
   ============================================================ */
.reveal { opacity: 0; transform: translateY(16px); }
.reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity var(--t-slow) var(--e-out), transform var(--t-slow) var(--e-out);
}

/* ============================================================
   2. Отклик на наведение
   Сам подъём карточки задан в styles.css через --lift / --lift-dur,
   и скорость мы уже перевели на --t-fast токеном выше.
   Здесь остаётся обложка в карточке списка репортажей: она тянулась
   .4s — вдвое дольше самой карточки, и было видно, как картинка
   доезжает уже после того, как карточка встала на место.
   ============================================================ */
.rl-cover img { transition: transform var(--t-base) var(--e-out); }

/* ============================================================
   3. Заголовок главной
   Вход шёл на ease вместо кривой входа. Строк две — это не группа
   из 3–8, поэтому каскада здесь нет и не должно быть.
   ============================================================ */

/* ============================================================
   4. Хокку — вступление к материалу
   Лучший момент портала, характер не меняем: строка выходит из
   размытия и приподнимается, следующая идёт за ней.
   Две правки:
   · шаг 900 мс → 600 мс. Это осознанно НЕ каскад (40–120 мс), а темп
     чтения: строки должны читаться по одной. Но при четырёхстрочном
     переводе 900 мс давали 3,6 с до последней строки — это уже не
     чтение, а ожидание. 600 мс держат построчность и укладывают даже
     четыре строки в 2,4 с.
   · высоту блока держат сами строки: они в DOM с самого начала, просто
     прозрачные, а сдвиг идёт через transform и раскладку не трогает.
     Поэтому никакого min-height под три строки — от него блок дёргался
     бы на четырёхстрочном переводе.
   ============================================================ */
.haiku { min-height: 0; }
.haiku.is-in .hk-line {
  transition-property: opacity, filter, transform;
  transition-duration: var(--t-slow);
  transition-timing-function: var(--e-out);
  transition-delay: calc(var(--i) * var(--hk-step));
}
.hk-original { transition: opacity var(--t-slow) var(--e-std); }
.hk-kigo { transition: opacity var(--t-slow) var(--e-std) var(--t-fast); }

/* ============================================================
   5. Переход в материал и обратно (.view-enter)
   Было: контейнер едет reportEnter .42s И его прямые дети едут тем же
   reportEnter .5s с задержкой 60 мс. Одно и то же содержимое двигалось
   дважды за полсекунды — статья «плыла внутри себя».
   Оставлено одно движение — контейнера.
   ============================================================ */
.view-enter { animation: reportEnter var(--t-slow) var(--e-out) both; }
.view-enter > * { animation: none; }

/* ============================================================
   7. Доступность: «меньше движения»
   styles.css глушит все анимации и переходы разом. Такой выключатель
   опасен: то, что стартует невидимым и проявляется анимацией, остаётся
   невидимым навсегда — так уже пропадал заголовок главной.
   Ниже — полный список того, что обязано показаться само. Список
   намеренно дублирует styles.css: motion.css должен закрывать это
   правило в одиночку, даже если styles.css перепишут.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  .hero-line,
  .reveal, .reveal.is-in,
  .haiku .hk-line,
  .view-enter, .view-enter > *,
  .report-full-cover {
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
  }
  .hk-original, .hk-kigo { opacity: .85 !important; filter: none !important; }

}

/* ============================================================
   8. Печать
   На бумаге нет ни прокрутки, ни наблюдателя пересечений. Всё, что
   ждёт своей анимации, печатается видимым.
   ============================================================ */
@media print {
  .hero-line,
  .reveal, .reveal.is-in,
  .haiku .hk-line,
  .hk-original, .hk-kigo,
  .view-enter, .view-enter > *,
  .report-full-cover {
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
  }
}

/* ── Переход между разделами ───────────────────────────────────────────────
   Подменяется только область контента: шапка, футер и плеер — те же узлы.
   Логика — блок «Мягкая навигация» в core.js, спецификация — MOTION.md.

   Порядок, который убирает белый провал между страницами:
     1. новый раздел готовится РЯДОМ со старым, скрытно (.sn-prep) — старый
        всё это время виден и на месте, данные и рендер идут в тени;
     2. старый уходит (.sn-leave, --t-fast · --e-in);
     3. в кадре, где ничего не видно, старый удаляется, новый встаёт на его
        место, прокрутка в начало;
     4. новый приходит (.sn-enter, --t-base · --e-out).
   Удерживать высоту не нужно: к моменту подмены новый раздел уже отрисован
   и сам задаёт высоту документа. Без слайдов вбок, без размытия. */
@keyframes snLeave {
  to { opacity: 0; transform: translateY(-8px); }
}
@keyframes snEnter {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* Подготовка: вне потока, без размера и без отрисовки — документ не
   удлиняется, полоса прокрутки не меняется. Вложенные входы (каскад героя,
   разворот) стоят на паузе и продолжатся уже на экране. */
main.sn-prep {
  position: absolute; left: 0; right: 0; top: 0;
  height: 0; overflow: hidden; visibility: hidden; pointer-events: none;
}
main.sn-prep .hero .container > *,
main.sn-prep .view-enter { animation-play-state: paused; }
main.sn-prep .reveal { transition: none; }

#main-content.sn-enter {
  animation: snEnter var(--t-base) var(--e-out) both;
  will-change: opacity, transform;
}
/* Уход объявлен последним: если два перехода наложились, уход побеждает. */
main.sn-leave {
  animation: snLeave var(--t-fast) var(--e-in) forwards;
  will-change: opacity, transform;
  pointer-events: none;
}

#main-content:focus,
#main-content:focus-visible { outline: none; }

/* Вложенные входы на время перехода выключены: разворот репортажа
   (.view-enter), каскад первого экрана, появление при прокрутке. Иначе
   содержимое двигалось бы дважды — само и вместе с main. По окончании
   перехода core.js (settleMain) доводит их до конечного состояния. */
#main-content.sn-enter .view-enter { animation: none; }
#main-content.sn-enter .reveal { transition: none; }

/* Полоса загрузки. Появляется только если ответ идёт дольше 400 мс: иначе
   на переходах из кэша она мигала бы на каждый клик и стала бы шумом.
   Это индикатор хода, ему можно линейное движение — но здесь и его нет:
   одно смещение до 80 % и добивка до 100 % при снятии. */
.sn-bar {
  position: fixed; inset: 0 0 auto 0; height: 2px; z-index: 120;
  background: var(--c-primary); transform: scaleX(0); transform-origin: 0 50%;
  transition: transform var(--t-slow) var(--e-out), opacity var(--t-base) var(--e-in);
  pointer-events: none;
}
.sn-bar.is-on { transform: scaleX(.8); }
.sn-bar.is-done { transform: scaleX(1); opacity: 0; }

/* Объявление заголовка раздела для скринридера: визуально скрыто, из потока
   вынуто, поэтому раскладку не двигает. */
.sn-status {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* «Меньше движения»: подмена и всё функциональное работает, движения нет.
   styles.css глушит анимации разом — значит страховка snReveal там не идёт,
   и показать .sn-prep обязан JS (settleMain) — CSS тут не нужен. Полоса загрузки остаётся как
   статичный индикатор. */
@media (prefers-reduced-motion: reduce) {

  #main-content.sn-enter,
  main.sn-leave {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    transition: none !important;
    will-change: auto;
  }
  .sn-bar { transition: none; }
}
/* Печать: ничего не скрыто, служебные элементы не печатаются. */
@media print {

  #main-content.sn-enter,
  main.sn-leave {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
    min-height: 0 !important;
  }
  .sn-bar, .sn-status { display: none !important; }
}

/* ── Отклик на нажатие ─────────────────────────────────────────────────────
   Единственный момент, когда интерфейс может подтвердить «я услышал» до того,
   как появится результат. Раньше отвечали только .btn и кнопки плеера. */
.btn:active,
.section-link:active { transform: translateY(1px); }

.chip.filter:active,
.lang-opt:active { transform: scale(.96); }

.nav-toggle:active,
.mz-in:active, .mz-out:active,
.upd-close:active, .av-close:active,
.lb-close:active, .lb-prev:active, .lb-next:active { transform: scale(.92); }

.btn, .section-link, .chip.filter, .lang-opt,
.nav-toggle, .mz-in, .mz-out, .upd-close, .av-close,
.lb-close, .lb-prev, .lb-next {
  transition: transform var(--t-fast) var(--e-std),
              background-color var(--t-fast) var(--e-std),
              border-color var(--t-fast) var(--e-std),
              color var(--t-fast) var(--e-std);
}

/* ── Первый экран ──────────────────────────────────────────────────────────
   Пять объектов первого экрана появлялись одновременно, и глаз выбирал по
   размеру. Каскад задаёт порядок чтения: что за турнир → кто мы → когда →
   сколько осталось → куда нажать. Вход выражен только через keyframes, без
   невидимого базового состояния — иначе он переживёт глушение анимаций. */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
.hero .container > .chip,
.hero .container > .display,
.hero .container > .hero-date,
.hero .container > .countdown,
.hero .container > .btn { animation: heroRise var(--t-slow) var(--e-out) both; }
.hero .container > .display    { animation-delay: 80ms; }
.hero .container > .hero-date  { animation-delay: 160ms; }
.hero .container > .countdown  { animation-delay: 240ms; }
.hero .container > .btn        { animation-delay: 320ms; }
/* Страховка: если анимации не идут (перекрытая вкладка, фоновая загрузка),
   через полторы секунды первый экран показывается принудительно. Правило
   проекта: движение украшает, но никогда не прячет содержимое. */
/* Страховка покрывает и вложенные элементы: заголовок лежит внутри h1,
   и прежний селектор «прямые дети» до него не доставал. */
.hero .container.is-shown > *,
.hero .container.is-shown > * * { animation: none !important; opacity: 1 !important; transform: none !important; }

/* ── Смена языка ───────────────────────────────────────────────────────────
   Меняется весь экран разом; лёгкое приглушение показывает, что это одно
   событие, а не мигание. С переходом между разделами не спорит: уход и
   приход там заданы keyframes, а анимация в каскаде сильнее transition;
   вдобавок core.js снимает этот класс со старого main в начале ухода. */
#main-content.locale-swap { opacity: .55; transition: opacity var(--t-fast) var(--e-in); }

/* ── Ошибка пароля ─────────────────────────────────────────────────────────
   Сообщение переигрывает вход на каждой попытке, поле встряхивается один раз:
   без этого вторая неверная попытка выглядела как «не нажалось». */
@keyframes errIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@keyframes fieldShake {
  0%, 100% { transform: none; }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}
.err.is-shown { animation: errIn var(--t-fast) var(--e-out) both; }
.field.is-wrong input { animation: fieldShake .25s var(--e-std) 1; }

@media (prefers-reduced-motion: reduce) {
  .hero .container > .chip,
  .hero .container > .display,
  .hero .container > .hero-date,
  .hero .container > .countdown,
  .hero .container > .btn,
  .err.is-shown, .field.is-wrong input,
  #main-content.locale-swap {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
}
@media print {
  .hero .container > * { animation: none !important; opacity: 1 !important; transform: none !important; }
}

/* ── Ширина списка репортажей ──────────────────────────────────────────────
   Список материалов жил в узкой колонке для чтения и выглядел уже остальных
   страниц. Список — это перечень, ему нужна полная ширина; сам материал
   остаётся узким, потому что его читают. Правило временно живёт здесь,
   его место — в styles.css рядом с .report-grid. */
body[data-page="report"] .container.narrow:has(#report-list:not([hidden])) {
  max-width: 1200px;
}
body[data-page="report"] .report-list-card { grid-template-columns: 240px 1fr; }
@media (max-width: 640px) {
  body[data-page="report"] .report-list-card { grid-template-columns: 1fr; }
}

/* ── Живые результаты 13–14 ноября ─────────────────────────────────────────
   Подсветка изменившейся строки: вход быстрый, уход долгий — сигнал должен
   пережить момент, когда взгляд в другой части экрана. Светится только своя
   строка и подиум, иначе из четырнадцати строк светятся все. */
@keyframes rowUpdated {
  0%   { background-color: rgb(var(--c-primary-rgb) / 0); }
  25%  { background-color: rgb(var(--c-primary-rgb) / .18); }
  100% { background-color: rgb(var(--c-primary-rgb) / 0); }
}
table.results tr.is-updated { animation: rowUpdated 1.2s var(--e-out) 1; }

@media (prefers-reduced-motion: reduce) {
  table.results tr.is-updated { animation: none; }
  table.results tr { transform: none !important; transition: none !important; }
}
@media print {
  table.results tr { transform: none !important; }
}

/* ── Пасхалка: форель бросается на камеру ──────────────────────────────────
   Знак федерации в шапке кликабелен отдельно от надписи. Сцена короткая:
   кадр наезжает, экран гаснет, дальше переход на сайт федерации. */
.logo-mark { cursor: pointer; }

.trout-strike {
  position: fixed; inset: 0; z-index: 200; overflow: hidden;
  background: #000; opacity: 0;
  transition: opacity var(--t-fast) var(--e-out);
}
.trout-strike.is-on { opacity: 1; }
.ts-video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; transform: scale(1);
  transition: transform 1.1s cubic-bezier(.6, 0, .9, .4);
}
.trout-strike.is-on .ts-video { transform: scale(3.4); }
.ts-black {
  position: absolute; inset: 0; background: #000; opacity: 0;
  transition: opacity var(--t-slow) var(--e-in);
}
.trout-strike.is-black .ts-black { opacity: 1; }

@media (prefers-reduced-motion: reduce) { .trout-strike { display: none; } }
@media print { .trout-strike { display: none !important; } }

/* ── textEffect: перенос TextEffect (motion-primitives) на ваниль ───────────
   Разметку делает textEffect() в components.js: сегменты .te-seg с --i,
   у контейнера --te-n, --te-step, --te-delay. Пресеты: blur | fade | slide |
   scale. Вход — каскадом от первого сегмента, уход — обратной волной. */
.te { --te-step: 40ms; --te-delay: 0ms; }
.te-seg {
  display: inline-block; white-space: pre;
  opacity: 0;
  transition:
    opacity var(--t-base) var(--e-out),
    transform var(--t-base) var(--e-out),
    filter var(--t-base) var(--e-out);
  transition-delay: calc(var(--te-delay) + var(--i) * var(--te-step));
}
.te-line .te-seg { display: block; white-space: pre-wrap; }
.te-blur  .te-seg { filter: blur(8px); transform: translateY(4px); }
.te-slide .te-seg { transform: translateY(14px); }
.te-scale .te-seg { transform: scale(.6); }
.te-fade  .te-seg { transform: none; }

.te.te-in .te-seg { opacity: 1; filter: none; transform: none; }

/* уход: короче входа, от последнего сегмента к первому */
.te.te-out .te-seg {
  opacity: 0; filter: blur(6px); transform: translateY(-8px);
  transition-duration: var(--t-fast);
  transition-timing-function: var(--e-in);
  transition-delay: calc((var(--te-n) - var(--i)) * 18ms);
}

/* хокку поверх эффекта: контейнер строки уже виден, каскад внутри строки */
.haiku .hk-line.te { opacity: 1; filter: none; transform: none; transition: none; }

@media (prefers-reduced-motion: reduce) {
  .te-seg, .te.te-out .te-seg { opacity: 1 !important; filter: none !important; transform: none !important; transition: none !important; }
}
@media print {
  .te-seg { opacity: 1 !important; filter: none !important; transform: none !important; }
}

/* ── Уведомление об обновлении: «дым» ─────────────────────────────────────
   Строка вспыхивает из ниоткуда (быстрый врыв из размытия, BURST), READ_MS
   медленно плывёт на зрителя (рост масштаба) и растворяется как дым (размытие,
   подъём, прозрачность). Тайминги — SMOKE в core.js; здесь одна keyframes-
   анимация на строку, фазы разложены по процентам от общей длины 5,52 с:
   420 мс врыв · 4000 мс чтение · 1100 мс растворение.
   Без плашек: читаемость держит дымный ореол (radial-gradient) и свечение
   текста. Только transform / opacity / filter; слой не ловит клики. */
.smoke-news {
  position: fixed; inset: 0; z-index: 96; overflow: hidden; pointer-events: none;
  background: rgb(var(--c-deep-rgb) / .05);
  opacity: 0; transition: opacity var(--t-base) var(--e-out);
}
.smoke-news.is-in { opacity: 1; }
.smoke-news.is-out { opacity: 0; transition: opacity var(--t-slow) var(--e-in); }

.sm-line {
  position: absolute; left: var(--x); top: var(--y);
  margin: 0; max-width: min(48ch, calc(96vw - var(--x)));
  font: 500 clamp(1.05rem, 1.8vw, 1.4rem) var(--f-body); line-height: 1.3;
  text-transform: none; letter-spacing: 0; text-wrap: balance;
  color: rgb(var(--c-deep-rgb));
  padding: .9em 1.6em;
  /* дымный ореол вместо плашки */
  background: radial-gradient(closest-side, rgb(255 255 255 / .8), rgb(255 255 255 / .35) 60%, transparent);
  text-shadow: 0 0 14px rgb(255 255 255 / .95), 0 0 28px rgb(255 255 255 / .7);
  opacity: 0; transform: translate3d(0, 0, 0) scale(.55); filter: blur(22px);
  will-change: opacity, transform, filter;
  transform-origin: 50% 50%;
}
.sm-lead { font-family: var(--f-head); font-weight: 800; color: rgb(var(--c-primary-rgb)); }

.smoke-news.is-in .sm-line { animation: smokeLine 5.52s linear var(--d) both; }
@keyframes smokeLine {
  /* врыв: 0 → 7,6 % (420 мс) */
  0%    { opacity: 0; transform: translate3d(0, 0, 0) scale(.55); filter: blur(22px); animation-timing-function: cubic-bezier(.16, 1, .3, 1); }
  7.6%  { opacity: 1; transform: translate3d(0, 0, 0) scale(1); filter: blur(0); animation-timing-function: linear; }
  /* чтение: медленный наплыв на зрителя, 4 с */
  80.1% { opacity: 1; transform: translate3d(var(--drift), -6px, 0) scale(1.16); filter: blur(0); animation-timing-function: cubic-bezier(.4, 0, 1, 1); }
  /* растворение: дым поднимается и рассеивается, 1,1 с */
  100%  { opacity: 0; transform: translate3d(calc(var(--drift) * 2), -34px, 0) scale(1.32); filter: blur(18px); }
}

@media (max-width: 600px) { .sm-line { padding: .7em 1.1em; max-width: min(30ch, calc(96vw - var(--x))); } }
@media (prefers-reduced-motion: reduce) { .smoke-news { display: none !important; } }
@media print { .smoke-news { display: none !important; } }

/* ------------------------------------------------------------
   Карта маршрута (зона команды, route-map.js). Само движение самолёта
   и минибуса по пути и переезд камеры — в JS (requestAnimationFrame,
   шаг кадра ограничен, скрытая вкладка не «прыгает»); здесь — отклики
   и подпись этапа. При reduced-motion JS показывает весь маршрут сразу.
   ------------------------------------------------------------ */
.rm-land, .rm-pin, .rm-vehicle { transition: opacity var(--t-base) var(--e-std); }
.route-btn, .place-btn, .rm-leg-btn { transition: transform var(--lift-dur) var(--e-std), box-shadow var(--lift-dur) var(--e-std), border-color var(--lift-dur) var(--e-std), color var(--lift-dur) var(--e-std), background var(--t-fast) var(--e-std); }
.route-btn:hover, .place-btn:hover { transform: translateY(var(--lift)); }
.rm-caption.is-in { animation: rmCaption var(--t-slow) var(--e-out); }
@keyframes rmCaption { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
/* Часы карты маршрута: цифры меняются каждый кадр, анимировать их нельзя —
   мигание мешает читать. Двигается только полоса прогресса. */
.rm-track-fill, .rm-track-knob { transition: transform var(--t-fast) linear, left var(--t-fast) linear, border-color var(--t-fast) var(--e-std); }
/* Под пальцем полоса должна идти за курсором без задержки — иначе тянешь одно,
   а едет другое. */
.rm-track.is-scrubbing .rm-track-fill, .rm-track.is-scrubbing .rm-track-knob { transition: none; }
.rm-track.is-scrubbing .rm-track-knob { transform: scale(1.15); }
/* День без переездов: вместо движения — спокойный пульс на месте стоянки */
.rm-stay { animation: rmStay 1.8s var(--e-std) infinite; transform-box: fill-box; transform-origin: center; }
@keyframes rmStay { 0%, 100% { r: 7px; } 50% { r: 12px; } }

/* Магазины на карте маршрута: ореол точки дышит — зовёт заехать.
   При «меньше движения» стоит. */
.rm-blink { animation: rmBlink 1.8s var(--e-std) infinite; transform-box: fill-box; transform-origin: center; }
@keyframes rmBlink { 0%, 100% { opacity: .35; transform: scale(.8); } 50% { opacity: 1; transform: scale(1.25); } }
@media (prefers-reduced-motion: reduce) { .rm-blink { animation: none; opacity: .8; } }

/* Выбранный магазин: строка легенды, булавка и строка списка моргают
   четыре раза и остаются подсвеченными. При «меньше движения» — сразу. */
.rm-legend-row.is-sel { animation: rmSelBlink 1.8s var(--e-std) infinite; }
.rm-shop.is-sel .rm-halo { animation-duration: .9s; }
.shop-row.is-sel { animation: rmSelBlink 1.8s var(--e-std) infinite; }
@keyframes rmSelBlink { 0%, 100% { opacity: 1; } 50% { opacity: .4; } }
@media (prefers-reduced-motion: reduce) { .rm-legend-row.is-sel, .shop-row.is-sel { animation: none; } }

/* Боевая карта: ячейки только меняют яркость, рыбы медленно дрейфуют.
   При выключенной анимации карта статична и полностью читаема. */
.tac-cell rect { transition: fill var(--t-base) var(--e-out), stroke var(--t-base) var(--e-out); }
.tac-fish { transition: opacity var(--t-base) var(--e-std); }
.tac-fish-i { animation: tacSwim 9s var(--e-std) infinite; animation-delay: calc(var(--i) * -3s); }
@keyframes tacSwim { 0%, 100% { transform: translateX(0); opacity: .55; } 50% { transform: translateX(26px); opacity: .9; } }
.tac-svg.is-still .tac-fish-i { animation: none; opacity: .7; }

/* ------------------------------------------------------------
   Афиша турнира на странице озера (.tournament.has-poster, pages.js).
   Афиша видна всегда. Чтобы взгляд её замечал, она медленно «дышит»:
   плавный подъём на 4 px и обратно, у соседних карточек фаза сдвинута
   (--i ставит pages.js). Под курсором картинка чуть растёт. При «меньше
   движения» афиша стоит на месте. Содержимого афиша не прячет.
   ------------------------------------------------------------ */
.t-poster { animation: posterBreathe 5.2s var(--e-std) infinite; animation-delay: calc(var(--i, 0) * -1.7s); will-change: transform; }
.t-poster img { transition: transform var(--t-base) var(--e-out); }
.tournament:hover .t-poster img,
.tournament:focus-within .t-poster img { transform: scale(1.03); }
@keyframes posterBreathe { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) {
  .t-poster { animation: none; }
  .t-poster img { transition: none; }
}

/* ------------------------------------------------------------
   Карусель баннеров на странице «О чемпионате» (.nations-stage, pages.js).
   Каждые 2 с приходящий слой открывается шторкой справа налево и
   продолжает медленно наплывать (scale 1 → 1.05), уходящий гаснет.
   Под чипом страны бежит полоска отсчёта: 2 с в карусели, 10 с при
   клике (is-held), стоит под курсором. При «меньше движения» смена
   мгновенная, наплыва и полоски нет.
   ------------------------------------------------------------ */
.ns-img { transform: scale(1.05); transition: opacity var(--t-base) var(--e-in); }
.ns-img.is-on {
  animation: nsWipe var(--t-slow) var(--e-out) both, nsDrift 2.6s linear both;
}
@keyframes nsWipe { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0); } }
@keyframes nsDrift { from { transform: scale(1); } to { transform: scale(1.05); } }
.nation { transition: border-color var(--t-fast) var(--e-std), box-shadow var(--t-fast) var(--e-std), background var(--t-fast) var(--e-std), color var(--t-fast) var(--e-std); }
.nation.is-on::after { animation: chipTimer 2s linear both; }
.nation.is-on.is-held::after { animation-duration: 10s; }
.nations:hover .nation.is-on:not(.is-held)::after { animation-play-state: paused; }
@keyframes chipTimer { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
  .ns-img { transition: none; transform: none; }
  .ns-img.is-on { animation: none; }
  .nation.is-on::after { animation: none; transform: none; }
}

/* ------------------------------------------------------------
   Дуэль бумажных самураев (samurai-duel.js, ADR-26). Слой на весь экран
   поверх страницы, кликов не ловит; бойцы — кадры листов 4×4 через
   background-position, позиция — transform из скрипта. Эффекты: вспышка
   при сшибке (sdFlash), линии скорости при рывке (sdLines), шлейф
   полупрозрачных копий бойца (sdGhost). При «меньше движения» слоя нет.
   ------------------------------------------------------------ */
/* стайка форелей за курсором (trout-cursor.js): под сценой дуэли, кликов не ловит */
.tc-canvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 79; pointer-events: none; }
.sd-stage { position: fixed; inset: 0; z-index: 80; pointer-events: none; overflow: hidden; --sd: 150px; perspective: 900px; }
@media (max-width: 719px) { .sd-stage { --sd: 100px; } }
.sd-f {
  position: absolute; left: 0; top: 0; width: var(--sd); height: var(--sd);
  background-repeat: no-repeat; background-size: 400% 400%;
  will-change: transform; filter: drop-shadow(0 6px 6px rgb(var(--c-deep-rgb) / .28));
  transform-origin: 50% 100%; backface-visibility: visible;
}
.sd-red { background-image: url("../img/samurai/red.webp"); }
.sd-blue { background-image: url("../img/samurai/blue.webp"); }
.sd-red.is-walk { background-image: url("../img/samurai/red-walk.webp"); }
.sd-blue.is-walk { background-image: url("../img/samurai/blue-walk.webp"); }
.sd-z.is-walk { background-image: url("../img/samurai/zombie-walk.webp"); }
.sd-z { background-image: url("../img/samurai/zombie.webp"); width: calc(var(--sd) * 1.3); height: calc(var(--sd) * 1.3); }
/* трассер: короткая светящаяся чёрточка, летит от ствола к точке попадания */
.sd-tracer {
  width: 34px; height: 2px; transform-origin: 0 50%; border-radius: 1px;
  background: linear-gradient(90deg, transparent, #ffd36b 55%, #fff);
  box-shadow: 0 0 6px rgb(var(--c-gold-rgb) / .9);
}
/* след пули: прожжённая дырка в бумаге, гаснет сама */
.sd-hole {
  width: 11px; height: 11px; border-radius: 50%;
  background: radial-gradient(circle, #1a1410 0 32%, #4a3a2c 33% 58%, rgb(74 58 44 / 0) 62%);
  box-shadow: 0 0 0 1px rgb(255 255 255 / .35), inset 0 0 2px #000;
  animation: sdHole 8s linear forwards;
}
@keyframes sdHole { 0%, 55% { opacity: .9; } 100% { opacity: 0; } }
.sd-spark {
  width: 18px; height: 18px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0 20%, rgb(var(--c-gold-rgb) / .9) 21% 45%, rgb(var(--c-gold-rgb) / 0) 70%);
  animation: sdSpark .24s var(--e-out) forwards;
}
@keyframes sdSpark { from { transform: translate(-50%, -50%) scale(.3); opacity: 1; } to { transform: translate(-50%, -50%) scale(1.5); opacity: 0; } }
/* смерть: белая вспышка стоп-кадра, струи крови, пыль при падении,
   бумажные лепестки у последней стойки */
.sd-white { position: fixed; inset: 0; background: #fff; opacity: .85; animation: sdWhite .42s var(--e-out) forwards; transform: none !important; }
@keyframes sdWhite { to { opacity: 0; } }
.sd-streak { height: 3px; border-radius: 2px; background: linear-gradient(90deg, #b3121c, rgb(179 18 28 / 0)); animation: sdStreak .7s var(--e-out) forwards; }
@keyframes sdStreak { from { opacity: 1; scale: .2 1; } 40% { opacity: 1; scale: 1 1; } to { opacity: 0; scale: 1.15 .6; } }
.sd-dust { border-radius: 50%; background: radial-gradient(circle, rgb(120 110 95 / .55) 0 45%, rgb(120 110 95 / 0) 75%); animation: sdDust .7s var(--e-out) forwards; }
@keyframes sdDust { from { transform: translate(-50%, -50%) scale(.4); opacity: 1; } to { transform: translate(calc(-50% + var(--bx, 0px)), calc(-50% + var(--by, 0px))) scale(1.8); opacity: 0; } }
.sd-petal { width: 9px; height: 6px; border-radius: 40% 60% 50% 50%; animation: sdPetal 2.4s linear forwards; opacity: 0; }
@keyframes sdPetal { 0% { opacity: 0; transform: translate(-50%, -50%) rotate(0); } 10% { opacity: .95; } 100% { opacity: 0; transform: translate(calc(-50% + var(--sway, 0px)), calc(-50% + var(--fall, 100px))) rotate(var(--spin, 360deg)); } }
/* смерть самурая: брызги разлетаются и гаснут, пятна на экране тают,
   лужа под ним растёт и темнеет, сам он гаснет */
.sd-blood { border-radius: 50%; background: #b3121c; box-shadow: 0 0 2px #7a0a10; animation: sdBlood .7s var(--e-out) forwards; }
@keyframes sdBlood { from { transform: translate(-50%, -50%) scale(1); opacity: 1; } to { transform: translate(calc(-50% + var(--bx, 0px)), calc(-50% + var(--by, 0px) + 40px)) scale(.4); opacity: 0; } }
.sd-splat { border-radius: 50% 40% 55% 45% / 45% 55% 40% 50%; background: radial-gradient(ellipse at 40% 45%, rgb(179 18 28 / .85) 0 35%, rgb(179 18 28 / .45) 55%, rgb(179 18 28 / 0) 75%); animation: sdSplat 3.2s linear forwards; }
@keyframes sdSplat { 0% { opacity: .95; } 40% { opacity: .85; } 100% { opacity: 0; } }
.sd-pool { border-radius: 50%; background: radial-gradient(ellipse, #7a0a10 0 40%, #b3121c 41% 70%, rgb(179 18 28 / 0) 100%); animation: sdPool 7s var(--e-out) forwards; }
@keyframes sdPool { 0% { transform: translate(-50%, -50%) scale(.15); opacity: .9; } 25% { transform: translate(-50%, -50%) scale(1); opacity: .9; } 70% { opacity: .8; } 100% { transform: translate(-50%, -50%) scale(1.05); opacity: 0; } }
.sd-f.is-dying { transition: opacity .5s var(--e-in); opacity: 0; }
/* тряска слоя при попаданиях и дрожь во время очереди */
.sd-stage.is-shake { animation: sdShake .16s var(--e-std); }
.sd-stage.is-shake-hard { animation: sdShakeHard .22s var(--e-std); }
.sd-stage.is-fire { animation: sdRumble .09s linear infinite; }
@keyframes sdShake { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(3px, -2px); } 50% { transform: translate(-3px, 2px); } 75% { transform: translate(2px, 1px); } }
@keyframes sdShakeHard { 0%, 100% { transform: translate(0, 0); } 20% { transform: translate(6px, -4px); } 40% { transform: translate(-6px, 3px); } 60% { transform: translate(4px, 3px); } 80% { transform: translate(-3px, -2px); } }
@keyframes sdRumble { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(1px, -1px); } }
/* шлейф гаснет на месте: transform у копии инлайновый, анимировать его нельзя —
   иначе копия улетает к началу координат */
.sd-ghost { filter: none; opacity: .45; animation: sdGhost .36s var(--e-out) forwards; }
@keyframes sdGhost { to { opacity: 0; } }
.sd-fx { position: absolute; transform: translate(-50%, -50%); pointer-events: none; }
.sd-flash {
  width: calc(var(--sd) * .9); height: calc(var(--sd) * .9); border-radius: 50%;
  background: radial-gradient(circle, #fff 0 18%, rgb(var(--c-gold-rgb) / .95) 19% 34%, rgb(var(--c-gold-rgb) / 0) 62%);
  clip-path: polygon(50% 0, 61% 38%, 100% 38%, 68% 60%, 80% 100%, 50% 74%, 20% 100%, 32% 60%, 0 38%, 39% 38%);
  animation: sdFlash .42s var(--e-out) forwards;
}
.sd-flash.is-hit { background: radial-gradient(circle, #fff 0 16%, rgb(var(--c-red-rgb) / .95) 17% 36%, rgb(var(--c-red-rgb) / 0) 64%); }
@keyframes sdFlash { 0% { transform: translate(-50%, -50%) scale(.2) rotate(0); opacity: 1; } 60% { opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1.6) rotate(40deg); opacity: 0; } }
.sd-lines {
  width: calc(var(--sd) * 2.4); height: calc(var(--sd) * .8);
  background: repeating-linear-gradient(180deg, transparent 0 7px, rgb(var(--c-deep-rgb) / .35) 7px 9px);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 40%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 40%, transparent);
  animation: sdLines .32s var(--e-in) forwards;
}
.sd-lines.is-left { -webkit-mask-image: linear-gradient(270deg, transparent, #000 40%, transparent); mask-image: linear-gradient(270deg, transparent, #000 40%, transparent); }
@keyframes sdLines { from { opacity: .9; } to { opacity: 0; } }
/* Курсор-прицел (samurai-duel.js). Центр точки — ровно под курсором; кольцо
   медленно вращается, при зажатой кнопке сжимается и краснеет. Над
   элементами управления прицел гаснет, стрелка возвращается. */
html.sd-cursor, html.sd-cursor body { cursor: none; }
.sd-ret { position: absolute; left: 0; top: 0; width: 0; height: 0; opacity: 0; transition: opacity var(--t-fast) var(--e-std); will-change: transform; --rc: var(--c-gold); }
.sd-ret.is-on { opacity: 1; }
.sd-ret.is-ui { opacity: 0; }
.sd-ret.is-down { --rc: #ff3b30; }
.sd-ret i { position: absolute; left: 0; top: 0; display: block; }
.sd-ret-dot { width: 4px; height: 4px; margin: -2px 0 0 -2px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 1px var(--rc), 0 0 8px var(--rc); }
.sd-ret-h, .sd-ret-v { background: var(--rc); box-shadow: 0 0 4px var(--rc); }
.sd-ret-h { width: 14px; height: 1px; margin: 0 0 0 -7px; }
.sd-ret-v { width: 1px; height: 14px; margin: -7px 0 0 0; }
.sd-ret-h::before, .sd-ret-h::after { content: ""; position: absolute; top: 0; width: 10px; height: 1px; background: var(--rc); }
.sd-ret-h::before { left: -26px; } .sd-ret-h::after { right: -26px; }
.sd-ret-v::before, .sd-ret-v::after { content: ""; position: absolute; left: 0; width: 1px; height: 10px; background: var(--rc); }
.sd-ret-v::before { top: -26px; } .sd-ret-v::after { bottom: -26px; }
.sd-ret-ring { width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 50%; border: 1px dashed var(--rc); opacity: .85; animation: sdRetSpin 6s linear infinite; transition: transform var(--t-base) var(--e-out); }
.sd-ret-ring2 { width: 64px; height: 64px; margin: -32px 0 0 -32px; border-radius: 50%; border: 1px solid transparent; border-top-color: var(--rc); border-bottom-color: var(--rc); opacity: .6; animation: sdRetSpin 3.2s linear infinite reverse; transition: transform var(--t-base) var(--e-out); }
.sd-ret.is-down .sd-ret-ring { transform: scale(.6); animation-duration: 1.2s; }
.sd-ret.is-down .sd-ret-ring2 { transform: scale(.75); animation-duration: .8s; }
@keyframes sdRetSpin { to { transform: rotate(360deg); } }
.sd-ret.is-down .sd-ret-ring { animation-name: sdRetSpinTight; }
@keyframes sdRetSpinTight { from { transform: scale(.6) rotate(0); } to { transform: scale(.6) rotate(360deg); } }
.sd-ret-txt { position: absolute; left: 40px; top: 22px; display: grid; gap: 1px; font-family: var(--f-mono); font-size: 10px; line-height: 1.1; letter-spacing: .04em; color: var(--rc); text-shadow: 0 0 6px rgb(0 0 0 / .6); white-space: nowrap; }
.sd-ret-txt b { font-weight: 700; }
.sd-ret-txt small { font-size: 9px; opacity: .85; }
@media (prefers-reduced-motion: reduce) { .sd-ret-ring, .sd-ret-ring2 { animation: none; } }
/* зажатая кнопка ведёт прицел зомби — текст страницы в это время не выделяется */
html.sd-aiming, html.sd-aiming * { user-select: none !important; -webkit-user-select: none !important; }
/* Выключатель дуэли: кружок над плеером; выключено — слой спрятан */
.sd-toggle { position: fixed; right: var(--s-sm); bottom: var(--s-sm); /* над плеером поднимает samurai-duel.js по его высоте */ z-index: 91; width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--c-line); background: var(--c-surface); color: var(--c-ink-soft); font-size: 16px; line-height: 1; cursor: pointer; box-shadow: var(--shadow); transition: transform var(--t-fast) var(--e-std), color var(--t-fast) var(--e-std), border-color var(--t-fast) var(--e-std); }
.sd-toggle:hover { transform: translateY(-2px); color: var(--c-primary); border-color: var(--c-primary); }
.sd-toggle[aria-pressed="true"] { color: var(--c-red); border-color: rgb(var(--c-red-rgb) / .4); } /* включено — красный ✕; выключено (умолчание) — нейтральная ⚔ */
.sd-stage.is-off { display: none; }
@media print { .sd-toggle { display: none; } }
@media (prefers-reduced-motion: reduce) { .sd-stage { display: none !important; } }
@media print { .sd-stage { display: none !important; } }

/* ============================================================
   Плашка регламента FIPSed на «О чемпионате»: вход снизу, затем
   по плашке раз в несколько секунд пробегает золотой блик, значок
   документа слегка покачивается, стрелка «Открыть» дышит. Окно
   просмотра проявляется за --t-base. «Меньше движения» — всё стоит.
   ============================================================ */
.rules-plate { animation: rpIn var(--t-slow) var(--e-out) backwards; /* не both: держала бы transform и глушила подъём при наведении */ transition: transform var(--t-fast) var(--e-std), box-shadow var(--t-fast) var(--e-std); }
.rules-plate:hover { transform: translateY(-2px); box-shadow: 0 14px 40px rgb(var(--c-deep-rgb) / .28); }
.rules-plate::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(105deg, transparent 35%, rgb(var(--c-gold-rgb) / .35) 50%, transparent 65%);
  transform: translateX(-100%); animation: rpShine 4.5s var(--e-std) 1s infinite;
}
.rp-ico { animation: rpBob 3s var(--e-std) infinite; transform-origin: 50% 90%; }
.rp-go { animation: rpPulse 2.2s var(--e-std) infinite; }
@keyframes rpIn { from { opacity: 0; transform: translateY(16px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes rpShine { 0% { transform: translateX(-100%); } 35%, 100% { transform: translateX(100%); } }
@keyframes rpBob { 0%, 100% { transform: rotate(0); } 25% { transform: rotate(-6deg); } 75% { transform: rotate(6deg); } }
@keyframes rpPulse { 0%, 100% { box-shadow: 0 0 0 0 rgb(var(--c-gold-rgb) / .6); } 60% { box-shadow: 0 0 0 10px rgb(var(--c-gold-rgb) / 0); } }
.rules-view { opacity: 0; transition: opacity var(--t-base) var(--e-out); }
.rules-view .rv-card { transform: translateY(24px) scale(.98); transition: transform var(--t-base) var(--e-out); }
.rules-view.open { opacity: 1; }
.rules-view.open .rv-card { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .rules-plate, .rules-plate::after, .rp-ico, .rp-go { animation: none !important; }
  .rules-plate::after { display: none; }
  .rules-view, .rules-view .rv-card { transition: none; opacity: 1; transform: none; }
}

/* Инфографика этапов Trout King (v1.63.3): при раскрытии карточки (.is-on) полосы вырастают слева
   направо, точки блёсен «выскакивают» по весу, ячейки ротаций загораются по ходу матчей, блоки сетки
   идут по стрелкам; старт-блёсны и зарыбление пульсируют. Анимируются отдельные свойства scale и
   translate — transform у ромбов и стрелок остаётся своим */
.tk-lures :is(.tkv-band, .tkv-seg, .tkv-dreg, .tkv-dplay, .tkv-zmax, .tkv-zavg) { scale: 0 1; transition: scale var(--t-slow) var(--e-out); }
.tk-lures .tkv-band { transition-delay: calc(var(--i, 0) * 120ms); }
.tk-lures .tkv-seg { transition-delay: calc(150ms + var(--i, 0) * 140ms + var(--j, 0) * 70ms); }
.tk-lures .tkv-dreg { transition-delay: 100ms; }
.tk-lures .tkv-dplay { transition-delay: 350ms; transition-duration: .9s; }
.tk-lures .tkv-zmax { transition-delay: calc(300ms + var(--i, 0) * 150ms); }
.tk-lures .tkv-zavg { transition-delay: calc(450ms + var(--i, 0) * 150ms); }
.tk-lures .tkv-dot { scale: 0; transition: scale var(--t-base) cubic-bezier(.34, 1.56, .64, 1) calc(350ms + var(--i, 0) * 45ms); }
.tk-lures :is(.tkv-col li, .tkv-pchip, .tkv-wx, .tkv-fb, .tkv-sw, .tkv-chip, .tkv-stock, .tkv-bento, .tkv-sun, .tkv-arr, .tkv-how) { opacity: 0; translate: 0 6px; transition: opacity var(--t-slow) var(--e-out), translate var(--t-slow) var(--e-out); }
.tk-lures .tkv-col li { transition-delay: calc(500ms + var(--i, 0) * 120ms + var(--j, 0) * 50ms); }
.tk-lures .tkv-how { transition-delay: calc(var(--i, 0) * var(--stagger)); }
.tk-lures .tkv-fb { transition-delay: calc(200ms + var(--i, 0) * 110ms); }
.tk-lures .tkv-arr { transition-delay: calc(260ms + var(--i, 0) * 110ms); }
.tk-lures :is(.tkv-wx, .tkv-chip, .tkv-pchip) { transition-delay: calc(400ms + var(--i, 0) * 120ms); }
.tk-lures .tkv-sw { transition-delay: calc(300ms + var(--i, 0) * 120ms + var(--j, 0) * 60ms); }
.tk-lures .tkv-stock { transition-delay: calc(1s + var(--i, 0) * 200ms); }
.tk-lures :is(.tkv-bento, .tkv-sun) { transition-delay: 800ms; }
.tk-lures .tkv-tab td.is-hl { background-color: transparent; transition: background-color var(--t-slow) var(--e-out) calc(300ms + var(--d, 0) * 350ms), color var(--t-slow) var(--e-out) calc(300ms + var(--d, 0) * 350ms); color: inherit; }
.tk-lures.is-on :is(.tkv-band, .tkv-seg, .tkv-dreg, .tkv-dplay, .tkv-zmax, .tkv-zavg, .tkv-dot) { scale: 1; }
.tk-lures.is-on :is(.tkv-col li, .tkv-pchip, .tkv-wx, .tkv-fb, .tkv-sw, .tkv-chip, .tkv-stock, .tkv-bento, .tkv-sun, .tkv-arr, .tkv-how) { opacity: 1; translate: none; }
.tk-lures.is-on .tkv-tab td.is-hl { background-color: color-mix(in srgb, var(--c-primary) 14%, #fff); color: var(--c-primary); }
.tk-lures.is-on .tkv-dot.is-start::after { animation: tkvPulse 1.8s var(--e-out) 1.4s infinite; }
.tk-lures.is-on .tkv-stock { animation: tkvBlink 2.4s var(--e-std) 1.6s infinite; }
@keyframes tkvPulse { from { transform: scale(1); opacity: .8; } to { transform: scale(2.2); opacity: 0; } }
@keyframes tkvBlink { 0%, 100% { background: #fff; } 50% { background: var(--c-gold); } }
@media (prefers-reduced-motion: reduce) {
  .tk-lures :is(.tkv-band, .tkv-seg, .tkv-dreg, .tkv-dplay, .tkv-zmax, .tkv-zavg, .tkv-dot, .tkv-col li, .tkv-pchip, .tkv-wx, .tkv-fb, .tkv-sw, .tkv-chip, .tkv-stock, .tkv-bento, .tkv-sun, .tkv-arr, .tkv-how) { scale: none; translate: none; opacity: 1; transition: none; animation: none; }
  .tk-lures .tkv-tab td.is-hl { background-color: color-mix(in srgb, var(--c-primary) 14%, #fff); color: var(--c-primary); transition: none; }
  .tk-lures .tkv-dot.is-start::after { animation: none; }
}
