/* ────────────────────────────────────────────────────────────
   СТРАНИЦА SHORTS — вертикальная лента с доводчиком

   Подключается ТРЕТЬИМ слоем, после design-system.css и case.css.
   Оболочку — рельсу, шапку, стопку карточек, контакты — даёт case.css.
   Здесь только то, чего в кейсе нет: сама лента, кадр, глубина,
   подсказка на загрузке и отмена одноколоночной раскладки в альбоме.

   ГЛАВНОЕ ОТЛИЧИЕ ОТ КЕЙСА. Кейс — длинная страница, которую крутят.
   Shorts — экран без прокрутки, внутри которого крутится ЛЕНТА. Отсюда
   всё остальное: своя область прокрутки вместо страничной, высота, заданная
   числом, а не набранная содержимым, и раскладки, которые делятся по
   ориентации, а не по ширине.
   ──────────────────────────────────────────────────────────── */

/* Страница не прокручивается: прокручивать нечего, вся высота посчитана.
   Строка страхует от лишнего пикселя переполнения, из-за которого
   на экране появлялась бы полоса прокрутки в никуда. */
body { overflow: hidden; }


/* ── ГЕОМЕТРИЯ ЛЕНТЫ ────────────────────────────────────────
   Четыре величины, из которых выведено всё остальное. Меняются только они.

   --fill  доля высоты ленты, которую занимает квадрат в центре
   --dim   во сколько раз ужат сосед — та самая глубина из референса
   --frame сторона квадрата
   --step  расстояние между центрами соседних кадров

   ПОЧЕМУ ШАГ МЕНЬШЕ КАДРА. Если положить кадры встык, как обычные блоки
   в колонке, ужатый сосед уедет от края экрана на разницу масштабов и
   пропадёт совсем: при --dim 0.55 от него не остаётся видно ничего.
   Поэтому в потоке кадр занимает не свою сторону, а --step — меньше
   стороны, — и соседи налезают друг на друга. В центре лежит полный
   квадрат, по краям — ужатые, между ними зазор в --gap.

   Формула шага читается так: половина полного кадра плюс половина ужатого
   плюс зазор. То есть ровно расстояние, при котором активный кадр и сосед
   не пересекаются и стоят на положенном друг от друга расстоянии.

   Значения сняты с референса обратным счётом по кадрам видео: картинка
   занимает 0.80 высоты экрана, сосед ужат до 0.55, зазор почти в ноль.  */
.shorts .reel {
  /* Лента во весь экран, БЕЗ полей страницы, — и отрицательный отступ ниже
     вытаскивает её из-под них.

     Так соседи прилипают к экрану. Кадр обрезается кромкой окна, а не
     кромкой блока, отступившего от окна на --edge: полоска соседа доходит
     до самого края, и лента читается уходящей за экран. Со стандартным
     полем между полоской и краем оставалась светлая щель в десять пикселей,
     и вместо «дальше есть ещё» это выглядело как обрезанный блок в рамке.

     Правая колонка на всю высоту — единственное место на сайте, где поле
     страницы снято. Рельса слева свои --edge сохраняет, и это не сбой
     ритма, а разница ролей: рельса — блоки на фоне, лента — само полотно. */
  --track: 100dvh;
  --fill:  0.80;
  --dim:   0.55;

  /* Ширина колонки считается ФОРМУЛОЙ, а не берётся сотней процентов,
     и это не педантизм — на процентах раскладка ломается молча.

     Проценты в CSS разрешаются от разных величин в зависимости от свойства,
     в которое их подставили: в width — от ширины контейнера, в height,
     inset-block и padding-block — от его ВЫСОТЫ (padding — единственное
     исключение, он всегда считается от ширины). Токен же один и тот же,
     и подставляется он и туда, и туда. Поймано замером: коробка кадра
     вышла 352 при расчётных 444, потому что в height сотня процентов
     означала не ширину колонки, а высоту ленты, и та ещё и зависела
     от самой коробки.

     Формула из абсолютных величин разрешается одинаково везде. Слагаемые
     ровно те, из которых собран грид страницы: экран минус поля, минус
     рельса, минус зазор между колонками.

     Сам min — предохранитель, а не основной путь. В альбомной раскладке
     квадрат всегда ниже колонки: справа остаётся под три четверти ширины
     экрана при его же высоте. Упереться в ширину можно только на очень
     узком окне — тогда кадр перестаёт расти, а не вылезает за колонку. */
  --col:   calc(100vw - var(--edge) * 2 - var(--case-rail) - var(--gap));
  --frame: min(var(--col), calc(var(--track) * var(--fill)));

  /* ШАГ ЛЕНТЫ. Читается как «половина полного кадра плюс половина ужатого
     плюс зазор» — ровно расстояние, при котором активный кадр и сосед
     не пересекаются и стоят на положенном друг от друга расстоянии.

     Шаг МЕНЬШЕ стороны кадра, и это обязательное условие глубины. Положи
     кадры встык, как обычные блоки в колонке, — ужатый сосед отъедет от края
     на разницу масштабов и пропадёт совсем: при --dim 0.55 от него
     не остаётся видно ничего. Поэтому в потоке кадр занимает не свою
     сторону, а шаг, и кадры налезают друг на друга; порядок наложения
     раздаёт z-index. */
  --step:  calc(var(--frame) * (1 + var(--dim)) / 2 + var(--gap));

  /* Поле сверху и снизу: без него первый и последний кадр не смогли бы
     встать по центру — прокрутка упирается в край содержимого. Считается
     от ШАГА, потому что в потоке кадр занимает именно шаг. */
  --side:  calc((var(--track) - var(--step)) / 2);

  /* Размах подсказки. Доля от шага, а не пиксели: жест должен читаться
     одинаково на телефоне и на большом мониторе. Минус — потому что
     лента отъезжает ВВЕРХ, показывая, что под кадром есть следующий. */
  --hint:  calc(var(--step) * -0.09);

  height: var(--track);
  /* Вылезаем из полей .case ровно на их величину: сверху и снизу лента
     начинается от кромки окна. Высота при этом остаётся 100dvh, а её
     margin-box — те же 100dvh минус два поля, что и у рельсы, поэтому
     страница не становится выше экрана и прокрутка не появляется. */
  margin-block: calc(var(--edge) * -1);
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow-y: auto;
  overflow-x: hidden;
  padding-block: var(--side);

  /* position: relative нужен не для наложения, а ради АРИФМЕТИКИ скрипта.
     offsetTop кадра считается от ближайшего позиционированного предка;
     не будь его здесь, точкой отсчёта стал бы body, и складывать offsetTop
     кадра с scrollTop ленты было бы складыванием величин из разных систем
     координат. Лента позиционирована — обе величины меряются от одного
     края, и центр кадра на экране это ровно offsetTop минус scrollTop. */
  position: relative;

  /* ДОВОДЧИК. Это scroll-snap, а не скрипт: mandatory значит «остановка
     только в точке привязки». Колесо, палец и стрелки доводит браузер сам,
     с системной инерцией. Скрипт добавляет доводчик только там, где
     нативного нет, — на перетаскивании мышью. */
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;

  /* Прокрутка ленты до края не утягивает за собой страницу и не вызывает
     жест «назад». */
  overscroll-behavior-y: contain;

  /* Полосы прокрутки не нужно: у зацикленной ленты нет ни начала, ни конца,
     и полоса врала бы о положении. */
  scrollbar-width: none;

  /* Курсор сообщает, что ленту можно взять рукой. */
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
}
.shorts .reel::-webkit-scrollbar { display: none; }

/* Кольцо фокуса рисуем сами: у ленты tabindex, и с клавиатуры видно должно
   быть, что она взята. Внутрь, а не наружу, — снаружи его обрезала бы
   собственная прокрутка. */
.shorts .reel:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: calc(var(--gap) * -1);
  border-radius: var(--radius);
}

/* КАДР — это КОРОБКА В ПОТОКЕ, а не сама картинка. Её высота — шаг ленты,
   и она же точка привязки для доводчика.

   Трансформировать эту коробку нельзя ничем и никогда: область привязки
   scroll-snap считается по ТРАНСФОРМИРОВАННОЙ рамке. Сдвинь или сожми
   кадр — браузер поедет за ним, и глубина превратится в прокрутку, которую
   потом ещё и доведёт снап. Ровно на эти грабли наступала подсказка
   про свайп на витрине, там же они и описаны. Поэтому всё движение живёт
   на .frame-in внутри, а коробка стоит намертво. */
.shorts .frame {
  position: relative;
  flex: 0 0 auto;
  width: var(--frame);
  height: var(--step);
  margin: 0;
  scroll-snap-align: center;
}

/* САМ КВАДРАТ. Он БОЛЬШЕ своей коробки — на разницу между стороной и шагом,
   поровну сверху и снизу. Отсюда отрицательные отступы: элемент вылезает
   за коробку и налезает на соседние.

   Абсолютным он стоит не ради красоты: только так его размер перестаёт
   влиять на поток, и коробка остаётся ровно шагом ленты. */
.shorts .frame-in {
  position: absolute;
  inset-block: calc((var(--step) - var(--frame)) / 2);
  inset-inline: 0;
  border-radius: var(--radius);
  background: var(--tone, var(--panel));
  overflow: hidden;

  /* ГЛУБИНА и ПОДСКАЗКА едут РАЗНЫМИ СВОЙСТВАМИ, и это не мелочь.

     scale и translate — самостоятельные свойства, а не части transform.
     Будь они одним transform, анимация подсказки переписывала бы его целиком
     и на время подсказки сносила бы масштаб. Разведённые по своим свойствам,
     они складываются браузером и не знают друг о друге: скрипт крутит
     scale на прокрутке, ключевые кадры крутят translate.

     --k ставит скрипт: единица в центре, --dim на расстоянии шага.
     Единица по умолчанию — без скрипта лента остаётся рабочей,
     просто плоской. */
  scale: var(--k, 1);
}

/* ── ОПИСАНИЕ БЕЗ ПОДЛОЖКИ ──────────────────────────────────
   Панель в рельсе отменена: у .brief больше нет класса .panel, и правила
   из case.css, которые её собирали, здесь только мешают.

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

   Множитель тот же, каким case.css задаёт внутреннее поле панели, —
   не подобранное число, а то же самое. Текст стоит там же, где стоял бы
   на подложке; убрали только подложку.

   Поле боковое, вертикального нет: сверху и снизу текст держит полоса
   растворения у .cards, и второй отступ поверх неё был бы двойным.      */
.shorts .brief { padding: 0 calc(var(--gap) * 2.4); }

/* Шапка: аватар и название раздела. Две колонки, вторая забирает остаток
   рельсы — как «About me» в шапке главной.

   Правило объявлено вне медиазапросов и потому перебивает три колонки,
   которые case.css раздаёт в одну колонку под название и кнопку возврата.
   Кнопки здесь нет, третьей колонке нечего занимать. */
.shorts .idrow { grid-template-columns: var(--ava) minmax(0, 1fr); }

/* Название — просто текст рядом с аватаром, без подложки.

   Подложка здесь не нужна дважды. Во-первых, слева больше нет ни одной
   панели, кроме контактов: описание лежит прямо на фоне, и плашка под одним
   названием читалась бы единственной рамкой ни для чего. Во-вторых, подложку
   в этой шапке case.css заводил под свою нужду — в кейсе под фиксированной
   шапкой проезжает страница, и текст без фона лёг бы прямо на фотографию.
   Здесь под шапкой не проезжает ничего: лента — своя область прокрутки,
   её содержимое обрезано её же рамкой и за неё не выходит.

   Поэтому правило одно на все раскладки и снимает подложку в том числе
   в портрете. Специфичности (0,3,0) хватает, чтобы перебить `.idrow >
   .case-title` из case.css.

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

   КЕГЛЬ — --fs-title, тот же, каким набран «Projects» на витрине. Это
   подпись раздела, и на двух страницах она обязана быть одного размера.

   Потолок от аватара — своё ограничение страницы, которого на витрине нет.
   Там заголовок лежит поперёк всего экрана и в высоту упирается только
   в себя; здесь он стоит в ряду ростом с аватар, а --ava выше 1836 пикселей
   ширины упирается в свой потолок 112 и расти перестаёт, тогда как
   --fs-title продолжает. На широком мониторе строка вылезла бы из ряда.
   0.8 аватара даёт с интерлиньяжем 1.1 ровно 0.88 его высоты — заголовок
   остаётся в ряду и на 2560.                                             */
.shorts .idrow > .case-title {
  margin: 0;
  display: grid;
  place-items: center;
  min-width: 0;
  font-size: min(var(--fs-title), calc(var(--ava) * 0.8));
  background-image: none;
  border-radius: 0;
}

/* ── ОПИСАНИЕ ПО ЦЕНТРУ ─────────────────────────────────────
   В кейсе карточка прижата к верху панели, и это правильно: там текст
   длинный, он панель заполняет, а прижатый верх даёт всем карточкам
   одну линию начала.

   Здесь текста две-три строки на всю высоту рельсы. Прижатый верх
   оставил бы их висеть под аватаром, и середина рельсы пустовала бы.
   Центр ставит описание напротив кадра — а кадр тоже стоит по центру.

   safe center вторым объявлением: обычный center при переполнении
   срезает содержимое СВЕРХУ и прокрутить к началу становится нечем.
   safe в этом случае откатывается к прижатому верху. Браузеру, который
   safe не знает, достаётся первая строка — для двух-трёх строк
   разницы нет.                                                        */
.shorts .card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  justify-content: safe center;
}

/* Картинка, когда придут настоящие шоты. Раскладка от этого не меняется:
   кадр остаётся квадратом, картинка заполняет его целиком. */
.shorts .frame-in img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}


/* ── ПОДСКАЗКА ПРО ПРОКРУТКУ ────────────────────────────────
   На загрузке лента чуть отъезжает вверх и возвращается — показывает,
   что под кадром есть следующий.

   На витрине такая подсказка живёт только в компактных раскладках: там
   на десктопе весь ряд виден целиком и листать нечего. Здесь она нужна
   ВЕЗДЕ — лента вертикальная на любом экране, и на десктопе тоже не видно,
   что кадров больше одного.

   Двигается СОДЕРЖИМОЕ, а не коробка кадра, — см. комментарий выше.
   Прокрутки при этом не происходит вовсе: translate не меняет ни scrollTop,
   ни размеры содержимого. Сосед снизу при этом въезжает в кадр
   по-настоящему — его двигает та же анимация.

   ДВЕ КРИВЫЕ, а не одна, и в этом весь жест. Туда — --e-soft, плавный
   старт: как будто ленту потянули. Обратно — --e-fan: трогается бодро
   и долго доезжает. Это и читается как магнит, вернувший кадр на место.
   Одной кривой на оба конца вышло бы равномерное качание.

   Ключевые кадры несут собственный animation-timing-function — он
   действует на отрезок, который с этого кадра начинается. */
@keyframes reel-hint {
  0%   { translate: 0 0;           animation-timing-function: var(--e-soft); }
  40%  { translate: 0 var(--hint); animation-timing-function: var(--e-fan); }
  100% { translate: 0 0; }
}

/* Класс ставит скрипт — после того, как собрано кольцо и лента встала
   на середину. Привязка к загрузке документа не годится: кольцо
   перебрасывает ленту, и подсказка шла бы по едущему содержимому. */
.shorts .reel.is-lead .frame-in { animation: reel-hint 1.5s .5s; }

/* Человек тронул ленту сам — досматривать демонстрацию незачем. */
.touched .frame-in { animation: none; }

/* На перетаскивании доводчик выключен, иначе он дёргает кадр к точке
   привязки на каждом движении мыши и тащить становится нечего.
   Возвращает его скрипт — уже в точке привязки, поэтому скачка нет. */
.shorts .reel.is-free { scroll-snap-type: none; }

/* Плавность на время перетаскивания тоже лишняя: scrollTop ставится
   каждым движением руки, и сглаживать тут нечего — рука и есть анимация. */
.shorts .reel.is-dragging {
  scroll-behavior: auto;
  cursor: grabbing;
}


/* ══════════════════════════════════════════════════════════════
   АЛЬБОМНАЯ УЗКАЯ: возврат к двум колонкам

   case.css разворачивает страницу в одну колонку по условию
   «max-width: 900px ИЛИ портрет». Для кейса это верно: страница длинная,
   её крутят, и прибитые шапка с контактами всегда под рукой.

   Для Shorts на узком АЛЬБОМЕ это ломает всё. Страница здесь не крутится,
   вся высота посчитана, и прибитые полосы сверху и снизу вычитаются
   из ленты дважды. На телефоне боком (812×375) от экрана после шапки,
   контактов и панели описания остаётся 53 пикселя — кадра там нет.

   Поэтому в альбоме страница остаётся ДВУХКОЛОННОЙ на любой ширине:
   в альбоме дефицитна высота, и рельса обязана забирать ширину, а не её.
   Граница получается чисто по ориентации — ровно так, как дизайн-система
   и предписывает раскладкам делиться.

   Блок только ОТМЕНЯЕТ чужие правила и ничего своего не вводит. Селекторы
   начинаются с .shorts — этого хватает, чтобы перебить case.css
   по специфичности, и гарантирует, что кейсов правка не коснётся.
   ══════════════════════════════════════════════════════════════ */
@media (orientation: landscape) and (max-width: 900px) {
  /* Рельсе 42% — на 812 это 341 при тексте в 175 по высоте. 25% дали бы
     203 на всю панель вместе с полями: двадцать знаков в строке. */
  :root { --case-rail: 42vw; }

  .shorts.case {
    grid-template-columns: var(--case-rail) minmax(0, 1fr);
    padding: var(--edge);
  }

  /* Рельса снова блок, а не display: contents. */
  .shorts .rail {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
  }

  /* Описание возвращается: оно и есть содержимое левой колонки. */
  .shorts .brief { display: flex; }

  /* Шапка возвращается в поток. */
  .shorts .idrow {
    position: static;
    padding: 0;
  }

  /* Двухслойная непрозрачная подложка нужна была, пока под шапкой проезжала
     лента. В потоке под названием не проезжает ничего — возвращаем обычную
     панель одним цветом. background-image: none, а не background: —
     шорткод снёс бы и цвет панели тоже. */
  .shorts .idrow > .case-title { background-image: none; }

  /* Контакты возвращаются в рельсу полосой. Отменять приходится всё,
     чем их прибивали: и положение, и сдвиг, и заданные руками размеры. */
  .shorts .socials {
    position: static;
    transform: none;
    width: 100%;
    height: auto;
  }
  .shorts .socials--row { aspect-ratio: 300 / 63; }
  .shorts .socials--row a { aspect-ratio: auto; }

  /* Плашка снова меряется рельсой, а не аватаром. */
  .shorts .toast { height: calc(var(--case-rail) * 63 / 300); }

  /* Полоса растворения ужимается: она съедает высоту дважды, сверху и снизу,
     а строк тут дефицит. На 812×375 описанию остаётся 175 по высоте, и полтора
     кегля с каждой стороны забирали из них 51 — две строки из семи. При 0.9em
     мягкий край остаётся, а строки возвращаются. Тот же приём и по той же
     причине, что у горизонтального планшета в case.css. */
  .shorts .cards { --растворение: 0.9em; }
}


/* ══════════════════════════════════════════════════════════════
   ПОРТРЕТ: одна колонка

   Здесь одноколоночная раскладка case.css подходит целиком — прибитая
   шапка сверху, прибитые контакты снизу, — и отменять нужно ровно одно:
   панель описания. В кейсе она прячется, потому что текст переезжает
   к своим кадрам; здесь переезжать некуда, панель и есть описание шота.

   Порядок в потоке задан руками: в разметке панель стоит выше ленты,
   потому что она часть рельсы, а на экране должна быть под ней.
   ══════════════════════════════════════════════════════════════ */
@media (orientation: portrait) {
  .shorts.case {
    height: 100svh;
    /* Лента забирает остаток, описание — свою заданную высоту.
       minmax(0, 1fr) вместо 1fr: у доли автоминимум равен содержимому,
       и лента отказалась бы становиться ниже своих кадров. */
    grid-template-rows: minmax(0, 1fr) auto;
    gap: var(--gap);
  }

  .shorts .reel  { order: 1; }
  .shorts .brief { display: flex; order: 2; }

  /* Высота описания задана числом, а не набрана текстом, и это вынужденно:
     из неё вычитается высота ленты. Набирайся она содержимым — длинное
     описание молча съедало бы кадр. Число обязано совпасть со слагаемым
     в --track ниже; разъедутся — лента вылезет за экран.

     2.2 аватара: на 375 это 167 пикселей, около пяти строк за вычетом
     полосы растворения. Не влезло — обрежется, как и в стопке кейса,
     поэтому описание шота пишется коротким. */
  .shorts .brief { height: calc(var(--ava) * 2.2); }

  /* Высота ленты считается вычитанием: экран минус две прибитые полосы,
     минус описание, минус зазор между ним и лентой. Слагаемые ровно те же,
     из которых case.css собирает свои поля страницы, — иначе они разъедутся.

     Кадр здесь упирается в ШИРИНУ, а не в высоту: в портрете колонка
     узкая, и доля от высоты ленты почти всегда больше ширины экрана.
     min выбирает меньшее, поэтому предохранитель из альбомной раскладки
     тут становится основным путём. */
  .shorts .reel {
    --track: calc(100svh - (var(--edge) + var(--ava) + var(--gap)) * 2
                  - var(--ava) * 2.2 - var(--gap));
    /* Колонка одна, рельсы нет — из ширины экрана вычитаются только поля. */
    --col:  calc(100vw - var(--edge) * 2);
    --fill: 0.80;
    height: var(--track);
    width: 100%;

    /* Выпуск за поля страницы отменён. В альбоме лента занимает колонку
       во всю высоту окна, и прилипать соседям больше не к чему. Здесь она
       ряд между прибитой шапкой и описанием: потяни её вверх — и кадры
       поехали бы ПОД аватар, а вниз — наехали бы на описание. Кромкой,
       которая их обрезает, остаётся край самой ленты. */
    margin-block: 0;
  }
}


/* ══════════════════════════════════════════════════════════════
   ДВИЖЕНИЕ ВЫКЛЮЧЕНО

   Гаснет подсказка и плавность программной прокрутки. Глубина остаётся:
   это не самостоятельное движение, а положение кадра — она меняется
   ровно настолько, насколько человек сам прокрутил ленту.
   ══════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .shorts .reel { scroll-behavior: auto; }
  .shorts .frame-in { animation: none !important; }
}
