﻿/* 02-effect.css — сетка, появление плашек, подсветка фактов, параллакс.
 *
 * ЖЕЛЕЗНОЕ ПРАВИЛО (§6.2), нарушение которого стоит половины страницы:
 * БАЗОВОЕ СОСТОЯНИЕ = КОНЕЧНОЕ, ВИДИМОЕ. Таймлайн только «доигрывает вход».
 * Типовая ошибка «opacity: 0 в базе + таймлайн на проявление» оставляет
 * пол-страницы навсегда невидимой в Safari. Так не делаем нигде.
 *
 * Три уровня, от худшего железа к лучшему:
 *   1) статика            — всё видно, ноль анимаций;
 *   2) html.io            — IntersectionObserver, класс ставит инлайн-скрипт;
 *   3) @supports view()   — нативные scroll-driven анимации.
 */

@layer effect {
  /* Без регистрации кастомное свойство не интерполируется и переключается
   * скачком в середине. initial-value обязателен. */
  @property --tint { syntax: '<color>'; inherits: true; initial-value: #C09057; }

  /* ─── сетка ────────────────────────────────────────────────────────────
   * В сниппете она на body::before во весь экран. Так нельзя: псевдоэлемент
   * на body во время скраба перерисовывается каждый кадр вместе с видео, а
   * две linear-gradient — дорогая заливка, способная съесть замеренные
   * 40-44 кадра/с. Уводим внутрь hero и outro и запираем перерисовку. */
  .grid {
    position: absolute; inset: -10% 0 0; z-index: 0; pointer-events: none;
    contain: paint;
    --cell: clamp(48px, 5vw, 72px);
    --grid-line: color-mix(in oklab, var(--ink) 5%, transparent);
    --grid-mask: radial-gradient(128% 78% at 50% 0%, #000 0%, transparent 74%);
    background-image:
      linear-gradient(to right,  var(--grid-line) 1px, transparent 1px),
      linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
    background-size: var(--cell) var(--cell);
    -webkit-mask-image: var(--grid-mask);
            mask-image: var(--grid-mask);
  }
  /* Клетка референса: мельче, заметнее и с диагональной маской — сетка
   * растворяется в угол, а не обрывается прямой линией поперёк экрана. */
  .grid--diag {
    --cell: 45px;
    --grid-line: color-mix(in oklab, var(--ink) 13%, transparent);
    --grid-mask: linear-gradient(-20deg, transparent 42%, #000);
  }
  /* На чёрном линии рисует бумага, а не чернила: 13 % чёрного по чёрному
   * не видно вовсе — это была бы просто мёртвая заливка. */
  .grid--dark {
    --grid-line: color-mix(in oklab, #F3F4F8 11%, transparent);
  }
  /* Параллакс сетки — transform, а не background-position: композитор, а не пейнт. */
  @keyframes grid-drift {
    from { transform: translate3d(0, 7%, 0); }
    to   { transform: translate3d(0, -7%, 0); }
  }

  /* ─── появление плашки ─────────────────────────────────────────────────
   * После посадки плашка НЕ ДВИГАЕТСЯ. Параллакса на читаемом тексте нет. */
  @keyframes rise {
    from { opacity: 0; transform: translate3d(0, 14px, 0); }
    to   { opacity: 1; transform: none; }
  }
  .rise { opacity: 1; transform: none; }

  /* ─── лента фактов вбок ────────────────────────────────────────────────
   * Ход поперёк экрана. Конечный сдвиг считается от СОБСТВЕННОЙ ширины
   * списка (проценты в translate — это его ширина), поэтому правый край
   * ленты приходит ровно к правому краю экрана на любом вьюпорте и при
   * любом числе фактов. Ни одного числа подбирать не нужно. */
  @keyframes reel-x {
    to { translate: calc(100vw - 100%) 0; }
  }
  /* Колода: ровно один экран вбок, с языков на контакты. */
  @keyframes deck-x {
    to { translate: -100vw 0; }
  }
  /* Дальний факт приглушён, ближний к центру — в полную силу. Тон не
   * трогаем: цвет факта — его опознавательный знак, гасить можно только
   * силу голоса. */
  @keyframes reel-lit {
    0%        { opacity: var(--dim); }
    42%, 62%  { opacity: 1; }
    100%      { opacity: var(--dim); }
  }

  /* ─── параллакс контактов ──────────────────────────────────────────────
   * Смещение любого слоя обращается в 0 к моменту, когда центр элемента
   * доходит до 50 % высоты вьюпорта. Пока читаешь — всё стоит.
   * Движется только то, что ещё не читают (§8.4). */
  @keyframes par { from { translate: 0 calc(var(--par, .3) * 90px); } to { translate: 0 0; } }

  /* ── уровень 2: IntersectionObserver ───────────────────────────────── */
  @media (prefers-reduced-motion: no-preference) {
    html.io .rise:not(.plate) {
      opacity: 0; transform: translate3d(0, 14px, 0);
      transition: opacity .38s var(--ease-soft), transform .38s var(--ease-out-expo);
    }
    html.io .rise.is-in { opacity: 1; transform: none; }
  }

  /* ─── одна плашка в кадре ──────────────────────────────────────────────
   * Плашки соседних битов липкие, и на стыке обе оказывались на экране
   * одновременно: у пар с одной стороны (lenty -> arena, обе слева)
   * они сходились границами. Правило §8.3 говорит прямо: «ровно одна
   * плашка в кадре, никогда две одновременно».
   *
   * Поэтому прозрачностью плашек управляет движок через onProgress, по
   * положению ВНУТРИ бита: уходящая гаснет ДО того, как приходящая
   * начинает проявляться. Пересечения не существует по построению — это
   * надёжнее увеличенного зазора, который всё равно кончится на узком
   * экране или при длинном заголовке.
   *
   * Класс film-live ставит page.js, только когда движок реально поехал.
   * Без JS плашки просто видны: база остаётся конечным состоянием,
   * железное правило §6.2 не нарушено. */
  html.film-live .film__beats .plate { will-change: opacity; }

  /* ── уровень 3: нативные scroll-driven анимации ────────────────────── */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      /* :not(.plate) — плашками фильма управляет движок по кадру, а не
       * таймлайн: CSS-анимация перебивает инлайновый style и они бы
       * подрались за opacity. */
      html:not(.io) .rise:not(.plate) {
        animation: rise .38s var(--ease-out-expo) both;
        animation-timeline: view();
        animation-range: entry 8% entry 52%;
      }
      .grid {
        animation: grid-drift linear both;
        animation-timeline: view(); animation-range: cover;
      }
      /* Сетка ленты не дрейфует: сцена залипшая, дрейф под ней читался бы
       * как съезжающий фон, а не как параллакс. */
      .grid--diag { animation: none; }

      /* Раскладка ленты (пин + ход вбок) живёт в 01-page.css, в слое outro:
       * слой outro объявлен ПОСЛЕ effect, и отсюда position: sticky молча
       * проигрывал бы базовому position: relative. Здесь остаётся только то,
       * чему в базе никто не противоречит, — подсветка пунктов.
       *
       * Каждый пункт получает СВОЮ долю одного таймлайна ленты. View() на
       * самом пункте здесь бесполезен: настоящей прокрутки по X нет, список
       * едет трансформом, и для view() он никуда не двигается. */
      .reel__list li {
        --dim: .34;
        opacity: var(--dim);
        animation: reel-lit linear both;
        animation-timeline: --reel;
        /* 20 % + 80 % — та же поправка на подъём вуали, что у reel-x.
         * Без неё подсветка первого факта успевала отгореть, пока он ещё
         * под вуалью, и на свет он выезжал уже приглушённым. */
        animation-range: contain calc(20% + var(--i) * 80% / var(--count))
                         contain calc(20% + (var(--i) + 2) * 80% / var(--count));
      }
      /* .pult добавлен к тому же правилу, а не заведён отдельным: параллакс
       * на странице должен быть ОДИН механизм с одним диапазоном, иначе
       * блоки поедут в разном темпе и это прочитается как рассинхрон. */
      .outro [data-par],
      .pult [data-par] {
        animation: par linear both;
        animation-timeline: view(); animation-range: entry 0% cover 50%;
      }
      .outro__mark { --par: .28; }
      .outro__h { --par: .62; }
      /* Внутри залипшей колоды параллакса нет: view() у пришпиленной сцены
       * не двигается, и слои просто застыли бы в смещённой позиции. */
      .deck [data-par] { animation: none; translate: none; }
      /* .25 -> ход 22.5 px при запасе 26 px (см. .pult__img в 01-page.css).
       * Было .5 при запасе 59 px; с 20.08.2026 кадр вписан целиком, запас
       * ужался до 26 px, и прежний ход вылезал бы за край. Параллакс здесь
       * и был вторым планом — половина хода ничего не теряет. */
      .pult__img { --par: .25; }
    }
  }

  /* ── reduced-motion: ни одного <video>, 8 стиллов, всё стоит ─────────── */
  @media (prefers-reduced-motion: reduce) {
    .film__stage { position: relative; height: auto; margin-bottom: 0; }
    .film__stage > *, .phone { display: none; }
    .film__beats { z-index: auto; }
    .beat {
      height: auto; display: grid; gap: 20px;
      padding: 48px var(--pad); justify-content: stretch;
    }
    .beat::before {
      content: ''; display: block; aspect-ratio: 16 / 9; border-radius: 18px;
      background: var(--still) center 46% / cover no-repeat, var(--bg);
    }
    /* Кадр здесь стоит отдельной картинкой над текстом (.beat::before), а
     * плашка лежит на бумаге. Размывать под ней нечего — стекло гасим и
     * возвращаем заливке полную плотность, иначе сквозь неё просвечивала бы
     * бумага и тёмная плашка выцветала бы в серую. */
    .plate {
      position: static; translate: none; max-width: 60ch;
      --plate-a1: .97; --plate-a2: 1;
      -webkit-backdrop-filter: none; backdrop-filter: none;
    }
    .phone__ring { animation: none; }
  }

  /* ── forced-colors и печать: весь текст виден, прозрачных заголовков нет */
  @media (forced-colors: active) {
    /* background-шорткат затирает и градиенты; фильтр и тени снимаем явно —
     * в режиме высокой контрастности им нечего показывать. */
    .plate {
      background: Canvas; color: CanvasText; border: 1px solid CanvasText;
      box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none;
    }
    .plate p, .plate h2, .steps li, .steps li::before, .plate .chip span { color: CanvasText; }
    .reel__list li, .rise { opacity: 1; filter: none; }
  }
  @media print {
    .film__stage, .topbar, .grid { display: none; }
    .beat { height: auto; }
    /* Печать по умолчанию не кладёт фоны. Тёмная заливка не напечаталась бы,
     * а светлые чернила остались бы светлыми — плашка вышла бы пустым
     * прямоугольником. Поэтому на печати она снова бумажная. */
    .plate {
      position: static; translate: none; box-shadow: none;
      background: #FFF; color: #000; border: 1px solid #000;
      -webkit-backdrop-filter: none; backdrop-filter: none;
    }
    .plate h2, .plate p, .steps li, .steps li::before { color: #000; }
    .plate .chip span { background: none; box-shadow: inset 0 0 0 1px #000; color: #000; }
    .reel__list li, .rise { opacity: 1; filter: none; transform: none; }
  }
}

@layer debug {
  /* включается через ?debug=1, класс ставит page.js */
  html.debug .beat { outline: 1px dashed rgb(42 28 16 / .35); }
  html.debug .film__stage::after {
    content: attr(data-frame); position: absolute; left: 12px; top: 12px; z-index: 9;
    font: 500 12px/1 'Geist Mono', monospace; color: #FFF6E8;
    background: rgb(0 0 0 / .55); padding: 6px 8px; border-radius: 6px;
  }
}
