/* =====================================================================
   РУЧКИ. Всё, что подгоняется на глаз, лежит здесь и больше нигде.
   Правь числа тут — правило ниже их только читает.

   Рядом с каждым стоит замеренное на becaneparis значение, чтобы было
   видно, куда именно ты отходишь от эталона.
   ===================================================================== */
:root {
    /* --- поведение 1: заголовок, блок. едет снизу и проявляется ---- */
    --b1-time:  490ms;   /* эталон 490                                */
    --b1-ease:  cubic-bezier(0.83, 0, 0.17, 1);  /* эталон cubic-bezier(0.83, 0, 0.17, 1)      */
    --b1-from:  1lh;     /* откуда едет: своя строка. можно 0.5lh, 24px */

    /* трогается сразу; если нужен разнос — здесь                     */
    --b1-delay:   0ms;

    /* --- стопка листов, замер шторки ios 22 августа ----------------
       Приходящий лист не сменяет предыдущий, а встаёт поверх него, не
       доходя до верха на --stack-gap. В щели виден лист, с которого
       пришли: он остаётся жив, опускается, скругляется и притухает.

       Кривая .32 .72 0 1 съедает три четверти пути за первую четверть
       времени и дальше долго выбегает — отсюда ощущение веса.

       Усадки с боков на замере было по 13 с каждой стороны; снята:
       видно должно быть полоску листа, а не рамку вокруг него.

       Опускание нижнего листа на 14 снято 22 августа следом. НИЖНИЙ
       ЛИСТ НЕ ДВИГАЕТСЯ ВОВСЕ: он стоит там, где стоял, пока верхний
       поверх него ездит. От него остаётся только свет — затемнение. */
    --stack-dur:      500ms;   /* верхний лист: приход и падение       */
    --stack-ease:     cubic-bezier(0.32, 0.72, 0, 1);
    --stack-gap:       11px;   /* насколько верхний не доходит до верха,
                                  он же вся полоска нижнего листа. было
                                  25, пока нижний опускался на 14 и съедал
                                  полоску собой; опускание снято — щель
                                  подтянута на те же 14                 */
    --stack-dim:        0.4;   /* затемнение нижнего                    */

    /* ВОЗВРАЩЕНИЕ НИЖНЕГО ЛИСТА правится отдельно от всего остального.
       Падение верхнего идёт по числам открытия — это одно и то же
       движение в две стороны. У нижнего своё: сам он стоит неподвижно,
       и возвращается на нём только свет.                            */
    --stack-return-dur:  500ms;
    --stack-return-ease: linear;

    /* --- СМЕНА ВЕРХНЕГО ЛИСТА, подобрано на swap.html 23 августа ----
       untitled и docs меняются между собой, не разбирая стопки: index
       под ними не двигается и не гаснет заново. Уходящий падает своим
       закрытием, приходящий поднимается своим открытием, но позже.

       Своих длительности и кривой у этого хода НЕТ, и это нарочно: он
       собран из тех же двух движений стопки, что и всё остальное.
       Поменяешь --stack-dur — смена поедет за ним.

       Новых чисел ровно два, и оба подобраны глазом на стенде:

         отставание 140 — прореха, в которой виден index, выходит 646
           из 760 на экране стенда. При нуле она всё равно 365: листы
           расходятся навстречу и пересекаются посередине, спрятать
           нижний отставанием нельзя;
         ПРИХОДЯЩИЙ СВЕРХУ — он накрывает уезжающего собой и съедает
           его хвост. Обратный порядок («уходящий сверху») давал ту же
           прореху, но приходящего не было видно, пока первый не
           отъедет; выбран этот.                                     */
    --swap-lag: 140ms;

    /* Скругление углов снято 22 августа. На замере было 10 сверху у
       верхнего листа и 8 у нижнего; правил под них больше нет.      */
}

/* -----------------------------------------------------------------------
   motion.css — словарь появления. Пять поведений, три числа, один шаг.

   Снято с becaneparis: это ровно их набор, с их длительностями и их
   кривыми. У них на весь сайт больше ничего нет, и это главное, что
   стоит из них взять — не конкретную анимацию, а то, насколько мало
   их нужно.

   ПРАВИЛА, которые держат всё остальное:

   1. Кривых две, не двадцать. Всё, что приходит, идёт по quintEaseOut;
      всё, что сдвигается на свою высоту, — по quintEaseInOut. Третьей
      не заводить: если поведению нужна своя кривая, значит это не
      поведение, а исключение, и его надо назвать.

   2. Смещение задаётся долей СВОЕЙ высоты, не пикселями. Строка в 41px
      уедет на 4, абзац в 105 — на 10.5. Поэтому очередь читается одним
      жестом, а не чередой одинаковых толчков.

   3. В покое ни трансформы, ни прозрачности. fill: backwards, не both:
      до своей очереди элемент держится в начальном состоянии, после —
      возвращается к обычному стилю и следа не оставляет. Остаточный
      translateY(0) держал бы слой композитинга и мылил текст.

   4. Разнос — шаг сетки, а не задержка каждому. --i это место
      элемента в очереди, --l номер строки внутри него; задержка их
      складывает. Обе ставит motion.js, считает CSS.

   5. Очередь трогается ПОСРЕДИ перехода, а не после него. На референсе
      текст выезжает на 130 мс от старта хода страницы — движения идут
      внахлёст, и это то, что отличает жест от очереди щелчков. Смещение
      задаёт --m-lead; ждать finished было ошибкой.

   6. Без скрипта всё играет на загрузке. Файл можно снести — страница
      не заметит.
   ----------------------------------------------------------------------- */

:root {
    --m-base:   700ms;  /* приход: подъём, наплыв                   */
    --m-shift:  500ms;  /* сдвиг на свою высоту: маска строки       */
    --m-fade:   300ms;  /* проявление                               */
    --m-step:   100ms;  /* шаг сетки разноса                        */
    --m-lead:   105ms;  /* трогается посреди перехода, не после     */

    --m-out:   cubic-bezier(0.22, 1, 0.36, 1);  /* quintEaseOut     */
    --m-inout: cubic-bezier(0.83, 0, 0.17, 1);  /* quintEaseInOut   */

    --m-rise:    10%;   /* доля своей высоты для подъёма            */
    --m-swell:  0.95;   /* с какого масштаба наплывает              */
}

@keyframes m-rise   { from { transform: translateY(var(--m-rise)); opacity: 0; } }
@keyframes m-mask   { from { transform: translateY(100%);          opacity: 0; } }
@keyframes m-swell  { from { transform: scale(var(--m-swell));     opacity: 0; } }
@keyframes m-fade   { from { opacity: 0; } }
@keyframes m-appear { from { visibility: hidden; } }

/* общее для всех пяти: место в очереди и отсутствие следа после */
[data-rise],
[data-swell],
[data-fade],
[data-appear],
.m-line {
    /* два слагаемых: --i место элемента в очереди (ставит order),
       --l номер строки внутри него (ставит split). строка наследует
       --i от своего элемента, поэтому они складываются.            */
    animation-delay: calc(var(--m-lead) + var(--m-step) * (var(--i, 0) + var(--l, 0)));
    animation-fill-mode: backwards;
}

[data-rise]   { animation-name: m-rise;   animation-duration: var(--m-base);
                animation-timing-function: var(--m-out);   }
[data-swell]  { animation-name: m-swell;  animation-duration: var(--m-base);
                animation-timing-function: var(--m-out);   }
[data-fade]   { animation-name: m-fade;   animation-duration: var(--m-fade);
                animation-timing-function: linear;         }
[data-appear] { animation-name: m-appear; animation-duration: 0s;
                animation-timing-function: linear;         }

/* маска строки. строку выкраивает motion.js: CSS не отдаёт строчные
   боксы, померить их можно только в разметке. .m-clip — окно, .m-line —
   то, что в нём едет.                                               */
.m-clip { display: block; overflow: hidden; }
.m-line { display: block;
           animation-name: m-mask;
           animation-duration: var(--m-shift);
           animation-timing-function: var(--m-inout); }

/* до открытия очередь стоит. правило срабатывает только когда
   motion.js жив; без него всё играет на загрузке.                   */
.js [data-rise],
.js [data-swell],
.js [data-fade],
.js [data-appear],
.js .m-line { animation-play-state: paused; }

.js.revealed [data-rise],
.js.revealed [data-swell],
.js.revealed [data-fade],
.js.revealed [data-appear],
.js.revealed .m-line { animation-play-state: running; }

/* системная просьба убавить движение. поведения остаются на местах,
   но перестают быть движением: всё сводится к включению.            */
@media (prefers-reduced-motion: reduce) {
    [data-rise], [data-swell], [data-fade], .m-line {
        animation-name: m-appear;
        animation-duration: 0s;
    }
}

/* ------------------------------------------------- ПОВЕДЕНИЕ 1
   Заголовочное появление, снятое с becaneparis прямым чтением их DOM,
   90 отсчётов за настоящий переход.

     ход        translateY(1lh) -> 0, на СВОЮ строку
     время      490 мс
     кривая     quintInOut .83 0 .17 1, rms 0.0027
     прозрачн.  анимируется и приварена к ходу: при y 80.15 она 0.1985,
                при 52.38 — 0.4762, при 20.21 — 0.7979, в сумме всегда
                100. Одна доля хода на два свойства.
     окна нет   элемент едет целиком; маска была моей выдумкой.

   Играет ТОЛЬКО на холодной загрузке. При смене страницы блок носит
   data-keep и стоит неподвижно — это его работа как якоря перехода. */
@keyframes rise-1 { from { transform: translateY(var(--b1-from)); opacity: 0; } }

/* играет и на холодной загрузке, и на приходе листа при смене
   страницы — иначе подмене нечего ждать и она схлопывается в кадр. */
html[data-nav="cold"] [data-rise-1],
.sheet.is-in [data-rise-1] {
    animation: rise-1 var(--b1-time) var(--b1-ease) var(--b1-delay) backwards;
}

/* ПОВЕДЕНИЕ 4, плашка, снято 20 августа: лист проявляется целиком, и
   коробке внутри него нечего доигрывать. Ручки убраны 23 августа.
   Замер с референса, если понадобится вернуть: 300ms,
   cubic-bezier(0.165, 0.84, 0.44, 1).                               */

/* ПОВЕДЕНИЕ 2, барабан, снято 20 августа вслед за плашкой. Подпись над
   кнопкой больше не проворачивается на смене страницы и не выезжает на
   загрузке: лист проявляется целиком, и строке внутри него нечего
   доигрывать. Ручки --b1-* остались, ими пользуется поведение 1.    */

/* =====================================================================
   ОБОЛОЧКА. Статическое преобразование.

   Страница не налистывается. Рама стоит: ни один лист никуда не едет,
   не накрывает соседа и не раскрывается клипом. Оба листа занимают одну
   и ту же коробку, и внутри неё одна страница ПРЕВРАЩАЕТСЯ в другую.

   Из этого следуют три вещи, и они же вся конструкция:

   1. Что есть на обеих страницах — не участвует. Шапка и передний блок
      помечены data-keep: у уходящего листа они гасятся мгновенно, у
      приходящего стоят на тех же координатах. Никто их не анимирует,
      поэтому они читаются как никуда не девавшиеся. Неподвижная точка
      не изображается движением — её просто не трогают.

   2. Остальное уходит НА МЕСТЕ и приходит НА МЕСТЕ. Уходящее опускается
      в своё окно и гаснет, приходящее поднимается из своего. Никакой
      разницы в координатах: обе страницы стоят там, где стояли.

   3. Уход и приход идут внахлёст, а разнос на уходе — в обратную
      сторону. Что пришло первым, уходит последним: очередь сматывается,
      а не проигрывается заново. Это их правило (staggerDirection -1).

   Вуали здесь нет: она была нужна, чтобы накрываемая страница уходила
   в глубину. Накрывать больше нечем.
   ===================================================================== */

.sheet       { position: relative; z-index: 5; display: flex;
               flex-direction: column; min-height: 100vh; }
.sheet-inner { position: relative; display: flex; flex-direction: column;
               flex: 1; min-height: 100%; }

/* --- во время преобразования. классы вешает motion.js --------------- */

/* оба листа в одной коробке, приходящий поверх. ни transform, ни clip:
   рама неподвижна.                                                    */
.sheet.is-out,
.sheet.is-in {
    position: fixed; inset: 0;
    overflow: hidden;
}
.sheet.is-out { z-index: 10; }
.sheet.is-in  { z-index: 20; }

/* уходящий приморожен на своей прокрутке: виден тот кусок, на который
   ты смотрел, а не начало страницы.                                   */
.sheet.is-out .sheet-inner { top: calc(-1 * var(--scroll-top, 0px)); }

/* общее на обеих страницах не анимируется вовсе. у уходящего гасится
   мгновенно, чтобы не двоилось с тем же самым у приходящего; у
   приходящего стоит как стояло. «не участвует» значит и его нутро
   тоже — иначе неподвижная точка дёргается изнутри.                   */
.sheet.is-out:not(.as-under) [data-keep] { visibility: hidden; }

/* ...но НЕ на нижнем листе стопки. Он никуда не уходит и остаётся
   виден в щели: гасить на нём общее незачем — двоиться нечему, лист
   под верхним один, — а мигание шапки в полоске видно насквозь.    */

/* блок — якорь: при смене страницы он не участвует ни сам, ни его
   нутро. на холодной загрузке участвует, там якорить нечего.       */
.sheet.is-in [data-keep] [data-rise-1] { animation: none; }

.js .sheet.is-out [data-keep] .m-line,
[data-keep] .m-line,
[data-keep][data-rise], [data-keep][data-swell],
[data-keep][data-fade], [data-keep][data-appear] { animation-name: none; }

/* уход: опускается в своё окно и гаснет. кривая и длительность те же,
   что у прихода, — движение одно, направления два.                    */
@keyframes m-mask-out { to { transform: translateY(100%); opacity: 0; } }

.js .sheet.is-out .m-line {
    animation-name: m-mask-out;
    animation-duration: var(--m-shift);
    animation-timing-function: var(--m-inout);
    animation-fill-mode: forwards;
    animation-play-state: running;
    /* обратный разнос: последний уходит первым */
    animation-delay: calc(var(--m-step) * (var(--n, 1) - 1 - var(--i, 0) - var(--l, 0)));
}

@media (prefers-reduced-motion: reduce) {
    .js .sheet.is-out .m-line { animation-duration: 1ms; }
}


/* =====================================================================
   СТОПКА ЛИСТОВ — шторка ios, замер 22 августа.

   Переливание xk снято целиком: листы больше не меняются прозрачностью
   и вообще не меняются один на другой. Приходящий выезжает снизу и
   ВСТАЁТ ПОВЕРХ, а тот, с которого пришли, остаётся жить под ним —
   опущенный, скруглённый и притухший. Полоска в --stack-gap сверху и
   есть вся мысль: стопку видно, и потому у листов есть вес.

   Отсюда два следствия, и они важнее чисел:

   1. Нижний лист НЕ УДАЛЯЕТСЯ. Он приморожен на своей прокрутке и ждёт.
      Обратный ход поэтому ничего не грузит и не собирает заново — он
      просто уводит верхний вниз.

   2. Прокрутка переезжает внутрь верхнего листа: документ больше не
      прокручивается, прокручивается лист. Липкое внутри него липнет
      к его окну, а не к окну браузера, и это ровно то, что нужно.

   Пар две: index -> untitled и index -> documentation. На остальных
   путях подмена идёт без движения — сменять там нечему, старой
   анимации больше нет.
   ===================================================================== */

/* --- нижний лист ---------------------------------------------------- */

.js .sheet.as-under {
    position: fixed; inset: 0; overflow: hidden; z-index: 10;
}
/* приморожен на той прокрутке, на которой с него ушли */
.js .sheet.as-under .sheet-inner { top: calc(-1 * var(--scroll-top, 0px)); }

/* затемнение — не слой в разметке, а тень самого листа.

   z-index ОБЯЗАТЕЛЕН, и вот почему: первый экран index (.banner) стоит
   на z-index 1, чёрное поле под ним на 2, и оба выше этой тени, у
   которой z-index был auto. Тень честно лежала с прозрачностью 0.4 и
   была не видна ни секунды — её закрывала собой подложка баннера,
   растянутая на всё окно. Замер: полоска нижнего листа давала 232, ту
   же яркость, что и неприкрытый index; с этой строкой даёт 139, то
   есть ровно 0.4 чёрного поверх.

   Выше листа он не вылезет: сам лист — z-index 10, верхний — 20, а
   отсюда наружу не выйти.                                          */
.js .sheet.as-under::after {
    content: ''; position: absolute; inset: 0; z-index: 5;
    background: #000; opacity: var(--stack-dim); pointer-events: none;
}

@keyframes stack-dim { from { opacity: 0; } }

.js .sheet.as-under.is-out::after {
    animation: stack-dim var(--stack-dur) var(--stack-ease) backwards;
}
/* обратный ход: свет уходит так же, как приходил, только назад */
.js .sheet.as-under.is-back::after {
    animation: stack-dim var(--stack-return-dur) var(--stack-return-ease) reverse forwards;
}

/* --- верхний лист --------------------------------------------------- */

/* ВСТАВШИЙ ЛИСТ ЛЕЖИТ В ПОТОКЕ И ПРОКРУЧИВАЕТСЯ ДОКУМЕНТОМ, как всякая
   другая страница. Своей прокрутки у него нет и быть не должно: она
   ломает структуру — полосу браузера, клавиши, привязку липкого.
   Сверху он просто отбит на щель, и в ней виден нижний лист.        */
.js .sheet.as-sheet {
    position: relative; z-index: 20;
    margin-top: var(--stack-gap);
    min-height: calc(100vh - var(--stack-gap));
}

/* НА ХОДУ — и только на ходу — лист вынут из потока: иначе его сдвиг
   на высоту экрана растил бы документ вдвое и дёргал полосу. В конце
   хода прокрутка стоит на нуле, а там оба положения совпадают до
   пикселя, и подмена одного другим не видна.                        */
.js .sheet.as-sheet.is-in,
.js .sheet.as-sheet.is-away {
    position: fixed; inset: var(--stack-gap) 0 0 0;
    margin-top: 0; min-height: 0;
    overflow: hidden;
}
/* уходя, лист замирает на той прокрутке, на которой его оставили */
.js .sheet.as-sheet.is-away .sheet-inner { top: calc(-1 * var(--scroll-top, 0px)); }

/* ПРИХОДЯЩИЙ СВЕРХУ. Пока верхний лист один, слой не значит ничего:
   он падает над нижним, и падать ему больше не над кем. Значит это
   только на смене верхнего, когда листов на экране три — и там
   приходящий должен накрывать уезжающего, а не ждать под ним.
   Ниже нижнего листа (10) отсюда не уйти.                          */
.js .sheet.as-sheet.is-away { z-index: 19; }

/* ОТСТАВАНИЕ ПРИХОДЯЩЕГО — единственное новое число смены. Стоит
   отдельной строкой, а не в сокращении выше: там оно было бы нулём
   для налистывания со стопки, где ждать не надо.                   */
.js .sheet.as-sheet.is-in.is-late { animation-delay: var(--swap-lag); }

@keyframes stack-sheet      { from { transform: translateY(100%); } }
@keyframes stack-sheet-away { to   { transform: translateY(100%); } }

.js .sheet.as-sheet.is-in {
    animation: stack-sheet var(--stack-dur) var(--stack-ease) backwards;
}
/* падение верхнего — то же движение, что и его приход, только вниз */
.js .sheet.as-sheet.is-away {
    animation: stack-sheet-away var(--stack-dur) var(--stack-ease) forwards;
}

/* внутри листов не играет ничего: движение одно — сам лист. у листа,
   ставшего верхним, молчание держится и после хода, иначе очередь
   тронется в тот момент, когда он уже встал.                         */
.js .sheet.is-in  .m-line,
.js .sheet.is-out .m-line,
.js .sheet.as-sheet .m-line,
.js .sheet.is-in  [data-rise-1],
.js .sheet.as-sheet [data-rise-1] { animation-name: none; }


@media (prefers-reduced-motion: reduce) {
    .js .sheet.as-under.is-out::after,
    .js .sheet.as-under.is-back::after,
    .js .sheet.as-sheet.is-in,
    .js .sheet.as-sheet.is-away { animation-duration: 1ms; }
}

/* --------------------------------------- восстановление стопки после F5
   Щель должна стоять УЖЕ НА ПЕРВОЙ ОТРИСОВКЕ, иначе лист секунду живёт
   во всю высоту и прыгает вниз, когда скрипт доберётся до класса. Метка
   ставится синхронно, из головы, ещё до разбора тела — поэтому правило
   висит на корне, а не на листе.

   Нижний лист приходит готовым: ни очереди появления, ни переходов. Он
   не приезжает, он там был.                                          */
.js[data-restack] > body > .sheet {
    margin-top: var(--stack-gap);
    min-height: calc(100vh - var(--stack-gap));
}
/* Гасим ТОЛЬКО нутро листа -- очередь появления, которая иначе
   проигралась бы заново в щели. Собственные анимации листа и его
   затемнение (::after) не трогаем: на обратном ходе свет обязан
   возвращаться так же, как уходил.                                */
.js .sheet.no-anim *,
.js .sheet.no-anim *::before,
.js .sheet.no-anim *::after {
    animation: none !important;
    transition: none !important;
}
