/* ==========================================================================
   Сетка кейсов — Figma 3:3905 / 3:3906 (320–767), 3:3632 (768–1023),
                  3:3697 (1024–1279), 3:3750 (1280–1920), 3:3815 (1920–max)
   Колонка = (ширина − 4·2 − 4·(N−1)) / N:
     425 → 206.5 × 2   576 → 282 × 2   768 → 187 × 4   1024 → 336 × 3
     1280 → 315 × 4    1920 → 315.33 × 6    4362 → 722.33 × 6
   Число колонок N живёт в --grid-columns (tokens.css), всё остальное резиновое.
   На 1024 колонок становится меньше, чем на 768 — так в макете: плитки крупнее;
   на 1280 их снова четыре, на 1920 — шесть.
   ========================================================================== */

/* Под сеткой лежит постоянный запас в самую высокую подпись кейса. Именно
   постоянный, а не выдаваемый на наведение: раскрытая подпись выводит колонку
   за нижний край сетки (высота сетки заморожена — см. ниже), и этот выход
   должен приземляться в пустоту, а не на футер.

   Раньше запас давался под активную плитку и вдобавок анимировался — из-за
   этого футер уезжал на 63–126px на каждое наведение по любой плитке, а вся
   страница 150мс пересчитывала раскладку. Величину ставит js/grid.js,
   measureShell(); от наведения она не зависит вовсе. */
.cases {
  display: flex;
  justify-content: center;
  /* поля сетки — 4 из макета, но не ближе к краю, чем позволяет вырез
     телефона, положенного набок (css/tokens.css) */
  padding-inline:
    max(var(--grid-inset), var(--safe-inline-start))
    max(var(--grid-inset), var(--safe-inline-end));
  padding-block-end: calc(var(--spacing-xl) + var(--cases-caption-space, 0px));
}

/* Высоту сетки js/grid.js замораживает на натуральной (inline height). Тогда
   выросшая колонка выходит за нижний край сетки, а документ выше не
   становится: футер стоит на месте при любом наведении, и точка финиша
   параллакса не уплывает. Без замка сетка тянулась бы за самой высокой
   колонкой — и футер ехал бы даже без запаса выше. */
.cases__grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  gap: var(--grid-gap);
  width: 100%;
  /* Предел свой, а не --container-max: поля у сетки 4, а не 24, поэтому на 1920
     она шире контента ровно на эти 40px (1912 против 1872). С 1920 предел
     снимается вовсе (--grid-max: none) — в макете 1920–max сетка резиновая,
     а замирает и центрируется только контент. */
  max-width: var(--grid-max);
}

/* Колонки выровнены по верху — разницу высот добирает параллакс */
.cases__column {
  display: flex;
  flex-direction: column;
  align-self: start;
  gap: var(--grid-gap);
  min-width: 0;
  /* Раскрытие подписи меняет раскладку внутри колонки. Containment запирает
     этот пересчёт в ней одной: соседние колонки и сетка целиком в него не
     попадают. Клиппинга здесь нет (это paint-containment), поэтому выросшая
     колонка по-прежнему видна ниже края сетки. */
  contain: layout;
}

/* --- Сдвиг колонки ------------------------------------------------------
   Основной путь — прокруточная анимация: она живёт на компоситоре и привязана
   к самому смещению прокрутки, а не к событию scroll. Это и есть лекарство от
   «потряхивания»: при загруженном main thread страница уезжает на компоситоре
   сразу, а колонки, которые двигал JS, догоняли её через кадр-другой.
   Покадрового JS в этом режиме нет вовсе — скрипт раз проставляет дельту.

   Конец диапазона — момент, когда верх футера коснулся низа экрана. Высота
   экрана берётся в dvh, а не замеряется в JS: в мобильных браузерах она
   меняется при схлопывании адресной строки, и CSS пересчитает это сам.
   max() страхует вырожденный случай, когда страница короче экрана. */
@keyframes cases-parallax {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, var(--parallax-delta, 0px), 0); }
}

@supports (animation-timeline: scroll()) and (animation-duration: auto) {
  .is-parallax-css .cases__column {
    animation-name: cases-parallax;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
    animation-range: 0px max(1px, calc(var(--parallax-anchor, 0px) - 100dvh));
  }
}

/* Запасной путь (пока без прокруточных анимаций — Firefox): сдвиг пишет JS,
   но прямо в style.transform, а не через наследуемую CSS-переменную. Правка
   переменной инвалидирует стиль всего поддерева колонки: замер на 23 плитках
   давал 0.47мс на кадр против 0.005мс у прямой записи. Слой в этом режиме
   держим явно — иначе браузер перерастрирует колонку на каждом кадре; в
   режиме прокруточной анимации слой создаёт сама анимация. */
.is-parallax-active:not(.is-parallax-css) .cases__column {
  will-change: transform;
}

/* ==========================================================================
   Активное состояние плитки — Figma 14:1765 (320–575), 14:1425 (576–767),
   14:1474 (768–1023), 14:1535 (1024–1279), 14:1596 (1280–1920), 14:1668
   (1920–max). Во всех шести кадрах оно одинаковое, отличается только шкала
   шрифта (мод переменных на 1024+), поэтому медиазапросов по ширине здесь
   нет вовсе — только по способу ввода.

   Подпись раздвигает колонку, как в макете, и делает это одинаково при мыши
   и при касании — но плавно: соседняя плитка не прыгает на 87px, а уезжает
   вниз за --duration-enter. Раскрытие идёт по grid-template-rows — это
   пересчёт раскладки, поэтому он заперт в колонке (contain: layout выше), а
   наружу не выходит вовсе: высота сетки заморожена, дельты параллакса на это
   время тоже (js/cards.js).
   ========================================================================== */

/* --- Плитка ------------------------------------------------------------- */

.case-card {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.case-card__frame {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  background-color: var(--color-bg-placeholder);
  /* aspect-ratio приходит из данных инлайном: пропорция плитки задаётся
     макетом/бэком и может отличаться от натуральной пропорции файла */
}

.case-card__media {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

/* --- Оверлей ------------------------------------------------------------
   Ссылка накрывает плитку целиком и сама же служит затемнением: клик в любое
   место плитки ведёт в кейс, а «View Case» стоит ровно по центру. Прозрачной
   ссылка остаётся кликабельной — до активации плитка ведёт себя как обычная
   ссылка.

   Везде переходы, а не keyframes: курсор уходит с плитки посреди анимации, и
   она должна развернуться с текущего кадра, а не доиграть до конца. Уход
   короче прихода — длительности и кривая в токенах. */

.case-card__overlay {
  position: absolute;
  /* -1px, а не 0: высота плитки дробная (её задаёт aspect-ratio от ширины
     колонки), и на дробной кромке композитор округляет слой затемнения к
     пиксельной сетке, а картинку под ним — нет. Разница в полпикселя видна на
     ретине светлой полоской по низу плитки. Слой с запасом накрывает эту
     кромку, а наружу не вылезает: фрейм обрезает его тем же скруглением, что
     и картинку (overflow: hidden выше). Центровка «View Case» не едет —
     запас одинаковый со всех сторон. */
  inset: -1px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-xl);
  background-color: var(--color-overlay-card);
  opacity: 0;
  transition: opacity var(--duration-leave) var(--ease-soft);
}

.case-card.is-active .case-card__overlay {
  opacity: 1;
  transition-duration: var(--duration-enter);
}

/* Линия — тенью, а не бордюром: в макете stroke идёт внутрь и высоту блока не
   меняет (17 + 4 до 1024, 20 + 4 на 1024+). Тот же приём, что в шапке. */
.case-card__action {
  padding-block-end: var(--spacing-sm);
  box-shadow: inset 0 -1px 0 var(--color-border-on-solid-img);
  color: var(--color-text-on-solid-img);
  font-size: var(--font-size-xs);
  line-height: var(--line-height-text-sm);
  letter-spacing: var(--letter-spacing-uppercase);
  text-transform: uppercase;
}

/* Иконка 20×20 в поле 8px от краёв плитки. Кнопка лежит рядом со ссылкой, а не
   внутри неё, и перекрывает её порядком в потоке. */
.case-card__share {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  display: flex;
  align-items: center;
  padding: var(--spacing-md);
  /* Иконка внутри красится currentColor. Кнопка лежит на картинке, поэтому
     чернила здесь не темы, а --color-text-on-solid-img: те же #e6e6e6 в обеих
     темах, как и подпись «View Case» рядом. */
  color: var(--color-text-on-solid-img);
  opacity: 0;
  transition: opacity var(--duration-leave) var(--ease-soft);
  /* до активации кнопки нет: клик по этому углу ведёт в кейс */
  pointer-events: none;
}

.case-card.is-active .case-card__share {
  opacity: 1;
  pointer-events: auto;
  transition-duration: var(--duration-enter);
}

/* Иконка share — из библиотеки (.icon), 20 вместо обычных 16. Цвет она берёт
   у кнопки — см. .case-card__share выше. */
.case-card__share-icon {
  --icon-size: 20px;
}

/* Содержимое оверлея приходит с 6px — движение небольшое, только чтобы
   появление не выглядело плоским. translate — отдельное свойство, а не
   transform: композитор двигает его без пересчёта раскладки. */
@media (prefers-reduced-motion: no-preference) {
  .case-card__action,
  .case-card__share-icon {
    translate: 0 6px;
    transition: translate var(--duration-leave) var(--ease-soft);
  }

  .case-card.is-active .case-card__action,
  .case-card.is-active .case-card__share-icon {
    translate: 0 0;
    transition-duration: var(--duration-enter);
  }
}

/* --- Подпись ------------------------------------------------------------
   Figma: заголовок Text xl (16/24 до 1024, 18/27 на 1024+), теги Uppercase sm
   (12/17 и 14/20) с переносом по строкам, зазор 2 по вертикали и 8 по
   горизонтали. Высота подписи в макете: 87 до 1024 (три строки тегов на
   опорной 425) и 77 на 1024+ (две строки). */

/* Схлопывание идёт по внешнему слою: строка грида от 0fr до 1fr — это
   прерываемый переход (курсор ушёл на полпути — подпись поедет обратно с
   текущего кадра), в отличие от анимации по ключевым кадрам. Поля и зазор до
   картинки живут во внутреннем слое: будь они снаружи, закрытая подпись всё
   равно занимала бы свои 16px и сдвинула бы всю сетку. */
.case-card__caption {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows var(--duration-leave) var(--ease-soft),
              opacity var(--duration-leave) var(--ease-soft);
}

.case-card.is-active .case-card__caption {
  grid-template-rows: 1fr;
  opacity: 1;
  transition-duration: var(--duration-enter);
}

.case-card__caption-clip {
  min-height: 0;
  overflow: hidden;
}

/* Поля живут здесь, а не на слое с overflow: вертикальные паддинги не
   сжимаются вместе со строкой грида, и закрытая подпись занимала бы их
   высоту. Верхнее поле 8 — это зазор картинка↔подпись из макета. */
.case-card__caption-inner {
  padding-block: var(--spacing-md) var(--spacing-sm);
  padding-inline: var(--spacing-sm);
}

.case-card__title {
  color: var(--color-text-primary);
  font-size: var(--font-size-md);
  line-height: var(--line-height-text-relaxed);
  letter-spacing: var(--letter-spacing-text);
}

.case-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-xs) var(--spacing-md);
  padding-block-start: var(--spacing-sm);
  list-style: none;
}

.case-card__tag {
  color: var(--color-text-subtle);
  font-size: var(--font-size-xs);
  line-height: var(--line-height-text-sm);
  letter-spacing: var(--letter-spacing-uppercase);
  text-transform: uppercase;
  white-space: nowrap;
}

/* Тег ведёт в каталог по своей услуге (/cases/identity/) — и в подписи на
   главной, и в постоянной подписи каталога. Услуга, которой нет в
   data/categories.js, ссылкой не становится: вести некуда, страницы под неё
   не собрано.

   Состояния наведения в макете у тега нет, и своих правил на этот счёт здесь
   больше нет тоже: тег — обычная ссылка без обводки, и по наведению он гаснет
   вместе со всеми такими же (css/base.css, «Наведение»).

   Раньше он, наоборот, подчёркивался — линия выдавалась по наведению, тенью.
   От тени пришлось отказаться не из-за неё самой: она не меняла высоту строки,
   а это здесь обязательное условие — в закрытом виде подпись меряется
   (js/grid.js, measureShell). Отказались потому, что это был единственный на
   сайте отклик, который дорисовывал элементу то, чего в покое у него нет. */

/* Тактильный отклик на касание — единственная обратная связь до того, как
   появится оверлей (правило press feedback: 0.96, не мельче). Условие —
   «не мышь», а не «(hover: none)»: так под правило попадают и гибриды вроде
   стилуса, где наведения фактически нет.

   Скобки вокруг not обязательны, и без них правило не работало нигде. В
   грамматике медиазапросов `not <условие>` — это целиком всё условие, и
   приписать к нему `and` нельзя: запрос становится невалидным, а невалидный
   запрос браузер молча считает «not all» и весь блок отбрасывает. То есть
   отклик на касание не срабатывал ни на одном устройстве, и увидеть это было
   нельзя — ошибка не в консоли, а в тишине. Проверено на Chromium и WebKit:
   `matchMedia(запрос).media` возвращал «not all», со скобками — сам запрос. */
@media (not ((hover: hover) and (pointer: fine))) and (prefers-reduced-motion: no-preference) {
  .case-card__frame {
    transition: scale var(--duration-press) var(--ease-soft);
  }

  .case-card:active .case-card__frame {
    scale: 0.96;
  }
}

/* Приход и уход остаются, но мгновенные: состояние по-прежнему читается по
   затемнению и подписи, а не по движению */
@media (prefers-reduced-motion: reduce) {
  .case-card__overlay,
  .case-card__share {
    transition-duration: 0ms;
  }

  .case-card__caption {
    transition: none;
  }
}

/* Заглушки на время загрузки первой порции */
.cases__grid[data-state="loading"] {
  min-height: 50vh;
}

.cases__error {
  padding-block: var(--spacing-4xl);
  color: var(--color-text-subtle);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-text-md);
  text-align: center;
  text-transform: uppercase;
}

/* --- Исключения из общего перехода ---------------------------------------
   Общее правило в css/base.css даёт переход всему, чему его не задали. Этим
   четырём задавать его нельзя — не «не нужно», а именно нельзя: их геометрию
   пишет JS и тут же меряет обратно, и переход подсунул бы замеру промежуточный
   кадр вместо конечного значения.

     .cases__grid — высоту сетки js/grid.js замораживает на натуральной:
       сбрасывает inline-высоту и сразу читает getBoundingClientRect(). С
       переходом на height он прочитал бы её на полпути, и замок встал бы не на
       ту высоту — колонка перестала бы выходить за нижний край, а футер поехал
       бы на каждое наведение.

     .cases__column — в запасном пути параллакса (Firefox: нет прокруточных
       анимаций) сдвиг пишется в style.transform на каждом кадре прокрутки.
       Переход превратил бы покадровый догон в отставание на 300мс — колонки
       тянулись бы за страницей. В основном пути сдвигом занимается анимация,
       а она переход и так перебивает, — но путь выбирается в рантайме, и
       правило должно держать оба.

     .case-card__caption-inner — тот самый слой, который measureShell меряет,
       чтобы выдать сетке постоянный запас под самую высокую подпись. Поля на
       нём меняются на брейкпоинтах; в переходе замер при смене ширины окна
       пришёлся бы на анимацию полей.

     .cases — запас под подпись measureShell выдаёт через --cases-caption-space,
       а он входит в нижнее поле секции. С переходом поле ехало бы к новому
       значению 300мс после каждой загрузки порции, и вместе с ним ехала бы
       высота документа: 1114 → 1121 за те же 300мс. Параллакс считает точку
       финиша от верха футера, то есть от этой самой высоты, — и переставал
       сводить низы колонок ровно в ноль (расхождение до 1.5px в замере
       tools/verify-layout.mjs). */
.cases,
.cases__grid,
.cases__column,
.case-card__caption-inner {
  transition: none;
}
