/* ============================================================
   Пересборка: правки поверх исходного CSS Webflow.

   Подключается сборщиком последним, на всех страницах, файлом
   /assets/css/redesign.css. Исходный vova-new-site.webflow.css
   не трогается — так видно, что именно изменено, и откат
   сводится к git.

   Геометрия снята с des.obys.agency, замерено в браузере
   на ширине 1280. Шрифты, цвета и тексты свои: все значения —
   переменные из библиотеки, своих чисел нет.
   ============================================================ */


/* ============================================================
   1. Типографика
   ============================================================ */

/* --- Крупные текстовые заголовки ----------------------------

   Один класс .heading-h2 обслуживает четыре роли:
     вводные абзацы с отступом (3), вводные без отступа (2),
     CTA в общем компоненте (5 страниц), заголовки статей (6).

   Кегль поднимается с heading-h2 (37.5px) до heading-h1 (47px).
   Межстрочный и межбуквенный не трогаются.

   На ≤479 остаётся heading-h2: heading-h1 там превращается
   в 9.767vw, и абзац в двести знаков становится стеной.        */

.heading-h2 {
  font-size: var(--_typography---font-size--heading-h1);
}

@media screen and (max-width: 479px) {
  .heading-h2 {
    font-size: var(--_typography---font-size--heading-h2);
  }
}

/* .heading-h2.text-indented.spec_title не задет: у него свой
   font-size с более высокой специфичностью. */


/* --- Начертание основного текста ----------------------------

   Вес 400 вместо 500, основной чёрный вместо серого, нулевой
   трекинг вместо −0.024em.

   Кегль не менялся намеренно: у референса 14px против наших
   13.4px, разница 0.6px, а межстрочный у нас даже просторнее
   (1.35 против 1.25). Читаемость там держится на этих трёх
   вещах, а не на размере.

   Область действия выбрана по замеру. Класс .text-muted стоит
   в 295 местах, но роли у него разные:

     div.text-muted   — 242 шт, медиана 10 знаков. Подписи
                        и метаданные: «3D, SPLINE, FIGMA»,
                        «Co-Founder». Остаются серыми, иначе
                        схлопнется иерархия.
     p.text-muted     — 11 шт, медиана 93 знака. Это описания.
                        Их и меняем.

   Плюс абзацы внутри rich-text — тексты статей и кейсов.       */

p.text-muted,
.news-rich-text p,
.work-rich-text p,
.type-rich-text p,
.about-body p {
  font-weight: 400;
  letter-spacing: normal;
  color: var(--colors--matt-black);
}

/* Крупные заголовки под правило не попадают: они тоже <p>,
   но с классом .heading-h2, и вес 500 у них остаётся. */


/* ============================================================
   2. Блок About на главной

   Референс: ярлык 14px inline-block шириной 311px в начале
   первой строки крупного текста; вторичный блок в правой
   половине двумя колонками; между ними 50 + 40 = 90px.

   Родная 12-колоночная сетка дала координаты референса
   без подгонки: колонки 7–9 и 10–12.
   ============================================================ */

/* Ярлык лежит в отступе первой строки. Отступ сверху ставит его
   на ту же долю высоты строки, что и в референсе: центр на 26%. */
.about-label {
  grid-area: 1 / 1 / 2 / 4;
  padding-top: var(--_spacing---spacing-5);
  z-index: 1;
}

/* Референс: 53px / lh 53 / ls −1.59px.
   Ближайшее в библиотеке: heading-h1 47px, межстрочный heading
   1.1em, межбуквенный heading-h2 −0.035em (−1.645px при 47px). */
.about-statement {
  grid-area: 1 / 1 / 2 / 13;
  margin-bottom: 0;
  font-size: var(--_typography---font-size--heading-h1);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  /* Отступ первой строки освобождает место под ярлык — тот же
     приём и то же значение, что у исходного .text-indented. */
  text-indent: calc(25% - .25rem);
  color: var(--colors--matt-black);
}

.about-body,
.about-aside {
  padding-top: var(--_spacing---spacing-40);
  margin-top: var(--_spacing---spacing-50);
}

/* Референс: колонка x=650 w=295, текст 245 из-за отступа справа.
   Здесь колонки 7–9 = 301px, отступ 50px -> текст 251px. */
.about-body {
  grid-area: 2 / 7 / 3 / 10;
  padding-right: var(--_spacing---spacing-50);
}

.about-body p {
  margin-bottom: var(--_spacing---spacing-20);
}

/* У первого абзаца колонки в референсе есть отступ первой строки. */
.about-lead {
  text-indent: var(--_spacing---spacing-40);
}

/* Тёмная плашка, отступ сверху 30 — значения из шкалы точно.

   ⚠️ Поля 15/25, а не 5/10 из референса. Решение заказчика от 09.09.2026:
   кнопка на сайте одна, и размер у неё обязан быть один. Поля 15/25
   держал контактный блок в подвале — там это главное действие страницы
   и палец должен попадать, 48px в высоту. Кнопка «Learn more about
   myself» на главной оставалась мелкой, 5/10, и рядом с подвалом
   читалась как другой, второсортный элемент.

   Поэтому размер переехал в само правило, а из подвала снят: два места,
   отвечающие за один размер, расходятся на первой же правке. Возврат
   мелкой кнопки — вернуть сюда 5/10 и добавить в `.contact-actions
   .about-btn` строку `padding: 15px 25px` обратно. */
.about-btn {
  margin-top: var(--_spacing---spacing-30);
  padding: var(--_spacing---spacing-15) var(--_spacing---spacing-25);
  display: inline-flex;
  align-items: center;
  position: relative;
  overflow: hidden;
  border-radius: var(--sizing--border-radius);
  background-color: var(--colors--matt-black);
  color: var(--colors--white);
  font-size: var(--_typography---font-size--body);
  letter-spacing: var(--_typography---letter-spacing--body);
  text-decoration: none;
}

/* Заливка выезжает так же, как подчёркивание в ссылках сайта:
   500ms, outExpo — то же время и та же кривая, что в IX2. */
.about-btn:before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--colors--granite-gray);
  transform: translate3d(-101%, 0, 0);
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.about-btn:hover:before { transform: translate3d(0, 0, 0); }
.about-btn span { position: relative; }

/* Список справа: заголовок с волосяной линией, строки с линией. */
.about-aside {
  grid-area: 2 / 10 / 3 / 13;
}

/* Подписи над списком больше нет (решение заказчика от 13.09.2026),
   и её нижняя линия переехала на верх первой строки: список
   по-прежнему закрыт линией с обеих сторон. */
.about-aside > .about-row:first-child {
  border-top: 1px solid var(--colors--matt-black);
}

/* Строка списка — родная разметка ссылки сайта
   (.link + .underline-container + .underline) с data-w-id,
   поэтому подчёркивание выезжает штатной интеракцией.
   Здесь только разворот в строку и волосяная линия. */
.about-row.link {
  flex-flow: row;
  justify-content: space-between;
  align-items: baseline;
  width: 100%;
  max-width: 100%;
  height: auto;
  padding-top: var(--_spacing---spacing-5);
  padding-bottom: var(--_spacing---spacing-5);
  border-bottom: 1px solid var(--colors--matt-black);
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
  text-decoration: none;
}

.about-row-meta { color: var(--colors--granite-gray); }

@media screen and (max-width: 991px) {
  /* Крупный кегль остаётся: heading-h1 в библиотеке не меняется
     вплоть до 479. Ширина строки ограничена теми же 405px, что
     стоят у существующего .about-text-container. */
  .about-body {
    grid-area: 2 / 1 / 3 / 7;
    max-width: 405px;
    padding-right: var(--_spacing---spacing-40);
  }

  .about-aside { grid-area: 2 / 7 / 3 / 13; }
}

@media screen and (max-width: 479px) {
  /* Как в исходном блоке: на мобильном отступ снимается,
     ярлык и текст расходятся по своим строкам. */
  .about-label {
    grid-area: 1 / 1 / 2 / 7;
    padding-top: 0;
  }

  .about-statement {
    grid-area: 2 / 1 / 3 / 7;
    font-size: var(--_typography---font-size--heading-h2);
  }

  .about-body {
    grid-area: 3 / 1 / 4 / 7;
    max-width: none;
    padding-right: 0;
  }

  .about-aside {
    grid-area: 4 / 1 / 5 / 7;
    margin-top: var(--_spacing---spacing-45);
  }

  .about-lead { text-indent: 0; }
}


/* ============================================================
   3. Playground — обратная палитра

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

   Класс вешает сборщик, только на /playground.
   ============================================================ */

body.theme-invert {
  /* Фон — чистый чёрный, не matt-black: мокапы на странице тёмные,
     и любой оттенок серого рядом с ними читается как грязь.
     Текст — чистый белый. Два серых меняются местами.           */
  --colors--white: #000;
  --colors--matt-black: #fff;
  --colors--granite-gray: #bababa;
  --colors--gray: #666;

  /* У cloud-white пары в палитре нет: это «почти белый» для
     разделителей. Зеркальный шаг от чёрного дал бы #0f0f0f —
     на экране такая линия пропадает совсем, поэтому взят
     заметный минимум.                                          */
  --colors--cloud-white: #242424;

  /* Фон body в CSS Webflow зашит как #fff мимо переменной. */
  background-color: var(--colors--white);
}

/* Ниже — то, что тоже зашито мимо переменных и потому
   не переворачивается само. Найдено просмотром страницы,
   а не наугад: остальные жёсткие цвета на ней не встречаются. */

/* 29 подписей к работам. */
body.theme-invert .heading-h5 {
  color: var(--colors--matt-black);
}

/* Меню и его ссылки: фон берёт переменную и становится светлым,
   а цвет текста зашит белым — без правки белое по белому. */
body.theme-invert .menu-container,
body.theme-invert .text-color---white {
  color: var(--colors--white);
}

/* ============================================================
   4. Resume

   Раскладка взята со страницы Changelog из экспорта: волосяная
   линия на всю ширину, под ней название секции слева и содержимое
   справа, по шесть колонок на каждое.

   Отличие от оригинала: левая колонка липкая. В Changelog подпись
   статичная и при длинном содержимом уезжает вверх; здесь она
   держится, пока листается её раздел, — тогда это действительно
   навигация, а не просто заголовок.
   ============================================================ */

.resume-grid {
  row-gap: var(--_spacing---spacing-60);
}

.resume-rule {
  grid-column: span 12;
  margin-bottom: var(--_spacing---spacing-30);
}

.resume-label {
  grid-column: span 4;
}

.resume-label > div {
  position: sticky;
  /* Шапка сайта сама липкая, поэтому подпись держится под ней. */
  top: var(--_spacing---spacing-90);
}

.resume-body {
  grid-column: span 8;
}

/* 52ch единицы дают около 65 знаков в строке этим шрифтом:
   ch считается по ширине нуля, а он у Bdogrotesk уже среднего знака. */
.resume-body > p {
  margin-bottom: var(--_spacing---spacing-20);
  max-width: 52ch;
}

.resume-item + .resume-item {
  margin-top: var(--_spacing---spacing-45);
}

.resume-item > .heading-h5,
.resume-entry > .heading-h5 {
  margin-bottom: var(--_spacing---spacing-10);
}

.resume-item > p,
.resume-entry > p {
  max-width: 52ch;
}

.resume-meta {
  margin-bottom: var(--_spacing---spacing-5);
}

.resume-list {
  margin: var(--_spacing---spacing-10) 0 0;
  padding: 0;
  list-style: none;
  max-width: 52ch;
}

/* Пункт помечен волосяной линией слева, а не маркером: тот же
   язык линий, что и у разделителей секций. */
.resume-list li {
  padding-left: var(--_spacing---spacing-15);
  border-left: 1px solid var(--colors--cloud-white);
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
}

.resume-list li + li {
  margin-top: var(--_spacing---spacing-12);
}

/* Обложка проекта справа от текста.

   Текст в резюме упирается в 52ch и дальше не растёт, а колонка
   содержимого — восемь из двенадцати: справа остаётся пустая треть
   экрана. Туда и встаёт обложка кейса, кликабельная, тем же кадром,
   что стоит в списке работ. Второго места, где лежит обложка, не
   заводим: адрес подставляет сборщик из coverFor(), источник тот же,
   что у панелей на /projects.

   Колонка текста остаётся 52ch, обложка забирает остаток. minmax(0,…)
   обязателен обеим: без него картинка в grid не даёт колонке сжаться
   и раскладка вылезает за контейнер. */
.resume-item--cover {
  display: grid;
  grid-template-columns: minmax(0, 52ch) minmax(0, 1fr);
  gap: var(--_spacing---spacing-40);
  align-items: start;
}

.resume-cover {
  display: block;
  /* ⚠️ `clip`, а не `hidden` — причина та же, что у `.case-next-cover`:
     внутри карточка с движением при прокрутке. */
  overflow: clip;
  border-radius: var(--sizing--border-radius);
  /* Верх обложки — на строке названия, а не меты: мета набрана мелким
     и служит подписью, равнять по ней картинку не на чем. */
  margin-top: var(--_spacing---spacing-20);
  /* Половина колонки. Во всю ширину обложка весит больше текста рядом
     и читается как главное на странице, хотя это подпись к пункту.
     Уменьшена рамка ссылки, а не картинка внутри неё, — кликабельная
     площадь совпадает с тем, что видно. */
  width: 50%;
  justify-self: start;
}

/* Наезд и движение при прокрутке держит коробка `.card-media`
   внутри ссылки — общим правилом на все карточки сайта, см. раздел
   «карточки» в конце файла. Здесь остаётся только размер самой ссылки,
   и уважение к `prefers-reduced-motion` там же, одно на все карточки. */

/* Строка о праве на работу в конце Profile. Плотнее и темнее
   остального абзаца: рекрутер снимает этим возражение, а не читает
   очередной факт. 600 — DemiBold, он в системе уже подключён. */
.resume-strong {
  margin-top: var(--_spacing---spacing-15);
  max-width: 52ch;
  color: var(--colors--matt-black);
  font-weight: 600;
  letter-spacing: normal;
}

/* Название кейса как строка: имя слева, стрелка «открыть» прижата
   к правому краю текстовой колонки. Стрелка — тот же знак ↗, что
   в контактных строках и подвале, поэтому её размер задаётся в em
   и сам совпадает с высотой заголовка.

   Подчёркивание переехало с самой ссылки на имя: на flex-строке
   линия тянулась бы через всю пустоту до стрелки. */
.resume-case-link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--_spacing---spacing-15);
  max-width: 52ch;
  color: inherit;
  text-decoration: none;
}

.resume-case-name {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: var(--colors--gray);
  transition: text-decoration-color 0.2s ease;
}

.resume-case-link:hover .resume-case-name {
  text-decoration-color: var(--colors--matt-black);
}

.resume-case-go {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1em;
  height: 1em;
  line-height: 1;
  transition: transform 0.2s ease;
}

.resume-case-link:hover .resume-case-go { transform: scale(1.18); }

@media (prefers-reduced-motion: reduce) {
  .resume-case-go { transition: none; }
  .resume-case-link:hover .resume-case-go { transform: none; }
}

/* Оговорка про NDA — самым мелким кеглем, как в CV. */
.resume-note {
  margin-top: var(--_spacing---spacing-30);
  font-size: var(--_typography---font-size--small);
  color: var(--colors--granite-gray);
  font-weight: 400;
  letter-spacing: normal;
  max-width: 52ch;
}

@media screen and (max-width: 991px) {
  .resume-label { grid-column: span 12; }
  .resume-body { grid-column: span 12; }
  .resume-label > div {
    position: static;
    margin-bottom: var(--_spacing---spacing-20);
  }
}

@media screen and (max-width: 767px) {
  .resume-item--cover { display: block; }
  .resume-cover { display: none; }
}

@media screen and (max-width: 479px) {
  .resume-rule { grid-column: span 6; }
  .resume-label { grid-column: span 6; }
  .resume-body { grid-column: span 6; }
}

/* ============================================================
   5. Проекты — панели, наезжающие друг на друга

   Референс: панель с непрозрачным фоном и волосяной линией
   сверху; шапка — сетка 12 колонок, мета 1–3, название 4–9,
   инфа 10–12, всё по нижнему краю; медиа под шапкой.

   Наезд у референса делает GSAP. Здесь — голый CSS: sticky
   плюс непрозрачный фон. Следующая панель наезжает на
   предыдущую. Ни библиотеки, ни запроса наружу.

   Чего не хватает против референса: уходящая панель не
   уменьшается и не притухает — это требует скролл-скрипта,
   его место на этапе перехода на GSAP.
   ============================================================ */

/* Панели стоят внутри .container, поэтому разворачиваем их
   во всю ширину теми же боковыми отступами, что у контейнера. */
.works-stack {
  margin-left: calc(-1 * var(--sizing--padding-global));
  margin-right: calc(-1 * var(--sizing--padding-global));
}

.works-panel-wrap {
  position: sticky;
  top: 0;
}

.works-panel {
  display: flex;
  flex-direction: column;
  height: 100vh;
  padding-top: var(--_spacing---spacing-40);
  padding-bottom: var(--_spacing---spacing-10);
  border-top: 1px solid var(--colors--matt-black);
  background-color: var(--colors--white);
  color: var(--colors--matt-black);
  text-decoration: none;
}

.works-row { align-items: end; }

.works-meta {
  grid-area: 1 / 1 / 2 / 4;
  font-weight: 400;
  letter-spacing: normal;
}

.works-title {
  grid-area: 1 / 4 / 2 / 10;
  margin-bottom: 0;
  font-size: var(--_typography---font-size--heading-h1);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
}

/* Референс: 26px. Токен heading-h3 — ближайший, 28px. */
.works-side {
  grid-area: 1 / 10 / 2 / 13;
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
  font-weight: 400;
}

.works-media {
  flex: 1;
  min-height: 0;
  margin-top: var(--_spacing---spacing-40);
  overflow: hidden;
}

.works-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.works-panel:hover .works-media img { transform: scale(1.03); }

@media screen and (max-width: 991px) {
  /* Три слота в ряд на планшете не живут: название остаётся
     на своей строке, мета и инфа уходят под него. */
  .works-title { grid-area: 1 / 1 / 2 / 13; }

  .works-meta {
    grid-area: 2 / 1 / 3 / 7;
    margin-top: var(--_spacing---spacing-20);
  }

  .works-side {
    grid-area: 2 / 7 / 3 / 13;
    margin-top: var(--_spacing---spacing-20);
    text-align: right;
  }
}

@media screen and (max-width: 479px) {
  .works-title { font-size: var(--_typography---font-size--heading-h2); }
  .works-meta { grid-area: 2 / 1 / 3 / 7; }

  .works-side {
    grid-area: 3 / 1 / 4 / 7;
    margin-top: var(--_spacing---spacing-10);
    text-align: left;
  }
}


/* ============================================================
   6. Подвал

   Раскладка с того же референса, des.obys.agency. Замерено
   в браузере на ширине 1280: слева колонка 295px с подписью
   «Social Media:» и мелкими строками по 32px, справа колонка
   925px с навигацией кеглем 160px, строки по 153px с волосяной
   линией. Внизу строка 14px в три слота.

   Родная 12-колоночная сетка сайта повторяет эти координаты
   без подгонки: 3 колонки = 300px, 9 колонок = 930px.

   Кегль 160px в библиотеке отсутствует — крупнейший токен
   display-2 равен 80px. Берём его удвоенным: значение остаётся
   производным от токена и едет вместе с ним по брейкпоинтам.
   ============================================================ */

.footer-social { grid-area: 1 / 1 / 2 / 4; }
.footer-menu   { grid-area: 1 / 4 / 2 / 13; }

/* Подпись над колонкой: волосяная линия под ней открывает список. */
.footer-col-title {
  margin-bottom: 0;
  padding-bottom: var(--_spacing---spacing-10);
  border-bottom: 1px solid var(--colors--matt-black);
  color: var(--colors--matt-black);
  font-size: var(--_typography---font-size--body);
  font-weight: 400;
  letter-spacing: normal;
}

/* Мелкая строка слева — та же разметка и те же правила, что
   у строк блока About, поэтому и правило одно на двоих. */
.footer-row.link,
.about-row.link {
  flex-flow: row;
  justify-content: space-between;
  align-items: baseline;
  width: 100%;
  max-width: 100%;
  height: auto;
  padding-top: var(--_spacing---spacing-5);
  padding-bottom: var(--_spacing---spacing-5);
  border-bottom: 1px solid var(--colors--matt-black);
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
  text-decoration: none;
}

.footer-row-meta { color: var(--colors--granite-gray); }

/* --- навигация крупным кеглем ------------------------------- */

.footer-nav {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Строка целиком: волосяная линия снизу, заливка выезжает
   при наведении. Отрицательный отступ слева уводит заливку
   за край колонки, а текст остаётся на месте — иначе он
   прилипал бы к границе плашки. */
.footer-nav-item {
  position: relative;
  overflow: hidden;
  margin-left: calc(-1 * var(--_spacing---spacing-15));
  padding-left: var(--_spacing---spacing-15);
  border-bottom: 1px solid var(--colors--matt-black);
}

/* Та же кривая и то же время, что у подчёркивания ссылок
   в IX2 и у кнопки в блоке About: 500ms, outExpo. */
.footer-nav-item:before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--colors--matt-black);
  transform: translate3d(-101%, 0, 0);
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.footer-nav-item:hover:before { transform: translate3d(0, 0, 0); }

.footer-nav-link {
  position: relative;
  display: inline-block;
  padding-top: var(--_spacing---spacing-5);
  padding-bottom: var(--_spacing---spacing-5);
  color: var(--colors--matt-black);
  /* Верхняя граница — удвоенный токен, 160px. Вторая граница
     держит самое длинное слово внутри колонки: «Playground»
     набирается в 5.21 кегля, колонка — 0.73 ширины окна,
     отсюда потолок 0.73 / 5.21 = 14vw, взято 13.5vw с запасом.
     Без неё на 992–1279 слово вылезало за край колонки. */
  font-size: min(calc(var(--_typography---font-size--display-2) * 2), 13.5vw);
  line-height: 1;
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  text-decoration: none;
  transition: color 500ms cubic-bezier(.19, 1, .22, 1);
}

.footer-nav-item:hover .footer-nav-link { color: var(--colors--white); }

/* Счётчик кейсов и статей — верхним индексом, кеглем строки. */
.footer-nav-count {
  font-size: var(--_typography---font-size--small);
  letter-spacing: normal;
  vertical-align: super;
}

/* --- нижняя строка ------------------------------------------ */

.footer-bottom {
  padding-top: var(--_spacing---spacing-60);
}

.footer-bottom-left  { grid-area: 1 / 1 / 2 / 5; }
.footer-bottom-mid   { grid-area: 1 / 5 / 2 / 9; }
.footer-bottom-right { grid-area: 1 / 9 / 2 / 13; text-align: right; }

/* --- адаптив ------------------------------------------------ */

@media screen and (max-width: 991px) {
  /* Две колонки рядом не живут: под навигацию нужна вся ширина,
     мелкие ссылки уходят под неё. */
  .footer-menu   { grid-area: 1 / 1 / 2 / 13; }
  .footer-social {
    grid-area: 2 / 1 / 3 / 13;
    margin-top: var(--_spacing---spacing-40);
  }
}

@media screen and (max-width: 479px) {
  /* На телефоне display-2 равен 12.791vw, удвоение дало бы 25vw —
     «Playground» не помещается в строку. Остаётся одинарный. */
  .footer-nav-link { font-size: var(--_typography---font-size--display-2); }

  .footer-bottom-left  { grid-area: 1 / 1 / 2 / 13; }
  .footer-bottom-mid   { grid-area: 2 / 1 / 3 / 13; }
  .footer-bottom-right { grid-area: 3 / 1 / 4 / 13; text-align: left; }
}

/* ============================================================
   7. Страница кейса: Market Risk Platform

   Собрана по спецификации v3: скан-слой (hero, факты, конфликт,
   два правила, индекс), затем глубина — шесть решений.

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

   Приёмы подачи сняты с des.obys.agency/typography-principles:
   нумерованный индекс с ярлыком №, крупные цифры с подписью
   в скобках, бейджи-пилюли, точки вместо маркеров списка.
   ============================================================ */

/* --- общая раскладка секций --------------------------------- */

/* Слева ярлык или номер, справа содержимое: колонки 1–3 и 4–10.
   Правый край на 10-й, а не на 13-й — строка не должна быть
   длиннее тех же ~65 знаков, что и везде на сайте. */
.case-label {
  grid-area: 1 / 1 / 2 / 4;
  margin-bottom: 0;
}

.case-body { grid-area: 1 / 4 / 2 / 11; }

/* Подглава: не решение и не правило, но по весу равна правилу.
   Кегль и цвет те же, что у «Rule 1», — иначе раздел читается как
   подпись к картинке и теряется между решениями.

   Переносы разрешены, в отличие от `.case-rule-num`: там строка всегда
   короткая — «Rule 1», «Rule 2», — а название подглавы длиннее колонки
   и без переноса вылезло бы за её край. */
.case-label--chapter {
  font-size: min(calc(var(--_typography---font-size--display-2) * 0.6), 6.6vw);
  line-height: 1;
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  /* Цвет тот же, что у номера правила и номера решения: серый.
     Подглава — это указатель на полях, а не заголовок в потоке. */
  color: var(--colors--gray);
}

/* Номер решения: место и цвет здесь, кегль — в .case-bignum. */
.case-num {
  grid-area: 1 / 1 / 2 / 4;
  margin-bottom: 0;
  color: var(--colors--gray);
}

/* Ярлык правила — тем же серым, что и номера решений: правила важны
   не меньше, а подписью на полях они терялись.
   Кегль — 30% от номеров решений. В размер цифры слово «Rule»
   выглядело тяжелее, чем читалось, и не влезало в трёхколоночный
   ярлык целиком: «Rule» и цифра расходились на две строки, а ярлык
   из двух слов читается как одна метка.
   Уменьшен только множитель от токена. Потолок по ширине окна
   оставлен прежним намеренно: он вступает в силу примерно ниже 730px,
   и уменьшенный вдвое дал бы на телефоне 12px — мельче основного
   текста, что читается как сбой, а не как решение. На десктопе
   потолок не участвует, там ровно половина прежнего кегля.
   `nowrap` — гарантия одной строки, а не надежда на неё: при таком
   кегле «Rule 1» занимает около трети своей колонки на любой ширине. */
/* Два класса в селекторе намеренно: .case-bignum объявлен ниже
   по файлу и с одним классом перебивал бы этот кегль. */
.case-num.case-rule-num {
  font-size: min(calc(var(--_typography---font-size--display-2) * 0.6), 6.6vw);
  line-height: 1;
  white-space: nowrap;
}

/* --- шапка страницы ----------------------------------------- */

.case-eyebrow {
  margin-bottom: 0;
  font-size: var(--_typography---font-size--body);
  letter-spacing: normal;
}

.case-h1 {
  max-width: 16em;
  margin-bottom: 0;
}

.case-standfirst {
  grid-area: 1 / 4 / 2 / 11;
  margin-bottom: 0;
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
  font-weight: 400;
  color: var(--colors--matt-black);
}

/* --- параллакс в шапке -------------------------------------- */

/* Кадр стоит в окне с фиксированными пропорциями, а сам едет
   медленнее страницы. Сделано прокруткой-таймлайном в CSS:
   ни строчки JS, и это не мешает будущему переезду на GSAP.
   Где браузер таймлайн не поддерживает — кадр просто стоит
   на месте, вёрстка не ломается. */
.case-parallax {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.case-parallax-inner {
  position: absolute;
  top: -8%;
  left: 0;
  right: 0;
  height: 116%;
}

/* Ловушка: overflow:hidden делает окно самостоятельным контейнером
   прокрутки, и безымянный view() цепляется к нему, а он никуда
   не прокручивается — анимация молча остаётся мёртвой. Поэтому
   таймлайн объявляется на самом окне и берётся по имени. */
@supports (animation-timeline: view()) {
  .case-parallax { view-timeline-name: --case-parallax; }

  .case-parallax-inner {
    animation: case-parallax linear both;
    animation-timeline: --case-parallax;
    animation-range: entry 0% exit 100%;
  }

  @keyframes case-parallax {
    from { transform: translate3d(0, -7%, 0); }
    to   { transform: translate3d(0, 7%, 0); }
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-parallax-inner { animation: none; }
}

.ph--fill { height: 100%; }

/* Воздух между решениями — вдвое против шкалы.

   Разделитель у каждого решения свой: `spacing-140` первым ребёнком
   внутри `.case-decision`. На 140px решения читались слитно, одно
   наезжало на другое, страница выходила душной.

   Значение берётся удвоением токена, а не числом: так оно едет вместе
   со шкалой по брейкпоинтам — 280 на десктопе, 210 на ≤479, где сам
   токен ужимается до 105. Тот же приём, что у кегля 160px в подвале. */
.case-decision > .spacing-140:first-child,
.case-chapter-gap {
  height: calc(2 * var(--_spacing---spacing-140));
}

/* Обложка соседнего кейса в блоке перехода в конце страницы.

   Ширина — от `.case-body`, то есть ровно от блока с надписью
   и подчёркиванием, а не от страницы: это отдельная ссылка,
   стоящая над строкой перехода.

   Наезд при наведении тот же, что у панелей в списке работ:
   500ms по outExpo, те же значения, что в интеракциях сайта. */
.case-next-cover {
  display: block;
  width: 100%;
  margin-top: var(--_spacing---spacing-20);
  /* ⚠️ `clip`, а не `hidden`: внутри лежит карточка с движением при
     прокрутке, а `hidden` сделал бы эту ссылку контейнером прокрутки
     и обездвижил бы её. Ловушка разобрана в разделе «карточки». */
  overflow: clip;
  border-radius: var(--sizing--border-radius);
  text-decoration: none;
}

/* Кадр внутри лежит в коробке `.card-media` — она держит и наезд,
   и движение при прокрутке, общим правилом на все карточки сайта
   (раздел «карточки» ниже). Своих правил на картинку здесь больше
   нет: пока они были, наезд у обложки жил отдельно от наезда
   у карточек, и любая правка расходилась на две. */

/* --- полоса фактов ------------------------------------------ */

.case-facts { border-top: 1px solid var(--colors--matt-black); }

.case-fact {
  display: grid;
  grid-template-columns: 3fr 9fr;
  column-gap: var(--_spacing---spacing-15);
  padding-top: var(--_spacing---spacing-10);
  padding-bottom: var(--_spacing---spacing-10);
  border-bottom: 1px solid var(--colors--matt-black);
  font-size: var(--_typography---font-size--body);
}

.case-fact > div:last-child { max-width: 52ch; }

/* --- крупные цифры ------------------------------------------ */

/* Приём референса: мелкая подпись в скобках, под ней цифра.
   Все цифры здесь — про контекст работы: сколько групп
   пользователей, ролей, оснований, решений. Результата в цифрах
   у кейса нет, и появиться ему тут неоткуда. */
.case-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  column-gap: var(--_spacing---spacing-15);
  row-gap: var(--_spacing---spacing-40);
}

/* Подпись и цифра выровнены по центру своей ячейки: цифры разной
   ширины, и по левому краю ряд выглядел бы рваным. */
.case-stat { text-align: center; }

/* --- полоса цифр внутри главы -------------------------------

   Полоса в шапке кейса — четыре числа во всю ширину, и `repeat(4, 1fr)`
   там честен: ячеек ровно столько, сколько колонок.

   Под главой в потоке всё иначе. Чисел бывает два или три, а сетка
   по-прежнему делит ширину на четыре, и числа садятся в левую половину
   страницы — не по сетке, а «куда-то влево», и ни к чему на странице
   не привязаны.

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

   ⚠️ `flex`, а не `grid`: при трёх числах вроде «10+ 10 20+» суммарная
   ширина подходит к краю колонки вплотную, и на узком окне ряд обязан
   перенестись, а не вылезти за страницу. Перенос даёт `flex-wrap`,
   сетка из `max-content` дала бы горизонтальную прокрутку. */
.case-stats--column {
  grid-area: 1 / 4 / 2 / 13;
  display: flex;
  flex-wrap: wrap;
  gap: var(--_spacing---spacing-40) var(--_spacing---spacing-60);
}

.case-stats--column .case-stat { text-align: left; }

/* Кегль меньше, чем у полосы во всю ширину, и это следствие места,
   а не вкуса. Полоса живёт в девяти колонках из двенадцати, а не
   в двенадцати: четыре числа вида «24 240 35+ 100+» в кегле 160
   просят около 1150px при доступных 1050 и переносятся строкой
   3 + 1 — ряд, который читается как сбой вёрстки.

   Множитель 1.3 против 2 у `.case-bignum` даёт 104px на десктопе:
   те же четыре числа укладываются в 810px и остаются крупными.
   Потолок по ширине окна пересчитан в той же пропорции. */
.case-stats--column .case-stat-value {
  font-size: min(calc(var(--_typography---font-size--display-2) * 1.3), 8.8vw);
}

.case-stat-label {
  margin-bottom: var(--_spacing---spacing-5);
  font-size: var(--_typography---font-size--body);
  letter-spacing: normal;
}

.case-stat-value {
  margin-bottom: 0;
  color: var(--colors--matt-black);
  /* Гарантия одной строки, а не надежда на неё — то же требование,
     что у номера правила. Диапазон `5–10` в кегле 160px просит около
     390px, а колонка на 1440 даёт 341: браузер находит перенос
     по короткому тире, и полоса цифр становится вдвое выше соседней
     такой же. Ряд крупных чисел, где одно из них в две строки, читается
     как сбой вёрстки, а не как число.

     Переполнение безопасно: числа выровнены по центру своей ячейки,
     а соседние короче колонки на добрую сотню пикселей, так что
     выступающие края уходят в пустоту, а не в чужой текст. */
  white-space: nowrap;
}

/* Один кегль на все крупные цифры страницы — тот же, что у ссылок
   в подвале: удвоенный display-2 с потолком по ширине окна.
   Держать их в одном размере важнее, чем подгонять каждую: номер
   решения и цифра статистики читаются как один язык. */
.case-bignum {
  font-size: min(calc(var(--_typography---font-size--display-2) * 2), 13.5vw);
  line-height: .9;
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  font-variant-numeric: tabular-nums;
}

/* --- конфликт ----------------------------------------------- */

.case-conflict {
  max-width: 20em;
  margin: 0 auto;
  font-size: var(--_typography---font-size--heading-h1);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  color: var(--colors--matt-black);
  text-align: center;
}

/* --- правила и мост к индексу ------------------------------- */

.case-rule-title { max-width: 14em; }

/* Строка-мост между правилами и индексом: связывает два принципа
   с их применениями. Кегль крупнее текста, но мельче заголовка —
   читается как реплика, а не как секция. */
.case-bridge {
  margin-bottom: 0;
  max-width: 34ch;
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
  font-weight: 400;
  color: var(--colors--matt-black);
}

/* --- индекс решений ----------------------------------------- */

.case-index {
  display: grid;
  grid-template-columns: 3fr 9fr;
  column-gap: var(--_spacing---spacing-15);
}

.case-index-mark {
  grid-column: 1;
  margin-bottom: 0;
  font-size: var(--_typography---font-size--body);
  letter-spacing: normal;
}

.case-index-title {
  grid-column: 2;
  margin: 0;
  font-size: var(--_typography---font-size--heading-h1);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  font-weight: 400;
  color: var(--colors--matt-black);
}

.case-index-count { color: var(--colors--gray); }

.case-index-list {
  grid-column: 1 / -1;
  margin: var(--_spacing---spacing-40) 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--colors--matt-black);
}

/* Строка индекса — тот же язык, что у навигации в подвале:
   волосяная линия и заливка, выезжающая слева при наведении. */
.case-index-row {
  position: relative;
  display: grid;
  grid-template-columns: 3fr 7fr 1fr 1fr;
  align-items: baseline;
  column-gap: var(--_spacing---spacing-15);
  padding: var(--_spacing---spacing-15) 0;
  border-bottom: 1px solid var(--colors--matt-black);
  color: var(--colors--matt-black);
  text-decoration: none;
  overflow: hidden;
}

/* ⚠️ Подложка светлая, и это не недосмотр контраста. Проверено
   решением заказчика 09.09.2026: чёрная заливка с белым текстом была
   заведена и в тот же вечер снята — строка оглавления не должна
   перебивать собой то, к чему ведёт. Реакция здесь подсказка, а не
   событие. Возвращать чёрное нельзя. */
.case-index-row:before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--colors--cloud-white);
  transform: translate3d(-101%, 0, 0);
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.case-index-row:hover:before { transform: translate3d(0, 0, 0); }

.case-index-num,
.case-index-text,
.case-index-note,
.case-index-arrow { position: relative; }

.case-index-num {
  padding-left: var(--_spacing---spacing-15);
  color: var(--colors--granite-gray);
  font-variant-numeric: tabular-nums;
}

.case-index-text {
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
}

/* Пометка «пришло от пользователей» — половина работы пришла
   из реального спроса, половина из анализа, и читатель обязан
   уметь отличить одно от другого. */
.case-index-note {
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
  letter-spacing: normal;
  text-align: right;
}

.case-index-arrow {
  padding-right: var(--_spacing---spacing-15);
  text-align: right;
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.case-index-row:hover .case-index-arrow { transform: translate3d(6px, 0, 0); }

/* Прыжок по якорю не должен упирать заголовок решения в край окна. */
.case-decision { scroll-margin-top: var(--_spacing---spacing-60); }

/* --- свёрнутый блок с контекстом ---------------------------- */

.case-details { border-top: 1px solid var(--colors--matt-black); }

.case-details-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--_spacing---spacing-15) 0;
  border-bottom: 1px solid var(--colors--matt-black);
  cursor: pointer;
  list-style: none;
}

.case-details-head::-webkit-details-marker { display: none; }

.case-details-title {
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
  color: var(--colors--matt-black);
}

/* Подпись под заголовком свёрнутого блока. Видна в свёрнутом
   состоянии и именно там нужна: она говорит, что внутри и сколько
   это читать, — то есть отвечает на вопрос «разворачивать ли». */
.case-details-sub {
  display: block;
  margin-top: var(--_spacing---spacing-5);
  font-size: var(--_typography---font-size--small);
  line-height: 1.5;
  letter-spacing: normal;
}

/* Знак раскрытия рисуется линиями: плюс превращается в минус
   поворотом одной палочки. */
.case-details-sign {
  position: relative;
  width: 14px;
  height: 14px;
  flex: none;
}

.case-details-sign:before,
.case-details-sign:after {
  content: "";
  position: absolute;
  background-color: var(--colors--matt-black);
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.case-details-sign:before { inset: 6px 0; height: 1px; }
.case-details-sign:after { inset: 0 6px; width: 1px; }

.case-details[open] .case-details-sign:after { transform: scaleY(0); }

.case-details-body { padding-bottom: var(--_spacing---spacing-40); }

/* Раскрытие анимировано, а не мгновенно: видно, что откуда выехало
   и куда сложилось обратно. Родной <details> так не умеет, поэтому
   высота едет через ::details-content — блок, которым браузер
   оборачивает содержимое.
   Время и кривая те же, что у остальной анимации сайта: 500ms
   и outExpo из IX2. Где браузер этого не умеет, блок открывается
   мгновенно — работоспособность от анимации не зависит. */
@supports (interpolate-size: allow-keywords) {
  .case-details { interpolate-size: allow-keywords; }

  .case-details::details-content {
    height: 0;
    overflow: hidden;
    opacity: 0;
    transition:
      height 500ms cubic-bezier(.19, 1, .22, 1),
      opacity 400ms cubic-bezier(.19, 1, .22, 1),
      content-visibility 500ms allow-discrete;
  }

  .case-details[open]::details-content {
    height: auto;
    opacity: 1;
  }

  /* Содержимое всплывает следом за высотой, а не появляется разом:
     сначала едет коробка, потом проявляются блоки. Тот же приём,
     что у появления блоков на главной при первой загрузке. */
  .case-details-body > * {
    opacity: 0;
    transform: translate3d(0, var(--_spacing---spacing-20), 0);
    transition:
      opacity 500ms cubic-bezier(.19, 1, .22, 1),
      transform 500ms cubic-bezier(.19, 1, .22, 1);
  }

  .case-details[open] .case-details-body > * {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }

  /* Лесенка: блоки проявляются друг за другом, и по ней читается
     порядок — сверху вниз при открытии, снизу вверх при закрытии. */
  .case-details[open] .case-details-body > *:nth-child(2) { transition-delay: 80ms; }
  .case-details[open] .case-details-body > *:nth-child(3) { transition-delay: 160ms; }
  .case-details[open] .case-details-body > *:nth-child(4) { transition-delay: 240ms; }
  .case-details[open] .case-details-body > *:nth-child(5) { transition-delay: 320ms; }
  .case-details[open] .case-details-body > *:nth-child(6) { transition-delay: 400ms; }
}

@media (prefers-reduced-motion: reduce) {
  .case-details::details-content,
  .case-details-body > * { transition: none; }
}

/* --- списки ------------------------------------------------- */

.case-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--colors--matt-black);
}

.case-list li {
  padding-top: var(--_spacing---spacing-10);
  padding-bottom: var(--_spacing---spacing-10);
  border-bottom: 1px solid var(--colors--matt-black);
  max-width: 52ch;
  color: var(--colors--matt-black);
}

/* Точка вместо маркера — приём референса. */
.case-list--dotted li {
  position: relative;
  padding-left: var(--_spacing---spacing-20);
}

.case-list--dotted li:before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(var(--_spacing---spacing-10) + .55em);
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background-color: var(--colors--matt-black);
}

/* --- решения: заголовок, Rejected, trade-off ---------------- */

.case-decision-title {
  margin-top: 0;
  margin-bottom: var(--_spacing---spacing-20);
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
  font-weight: 400;
  color: var(--colors--matt-black);
}

/* Ярлык-пилюля. Референс: 14px, padding 3/5, радиус 8, рамка
   в цвет текста. Радиус — единственное значение мимо токена:
   у сайта --sizing--border-radius равен нулю, а без скругления
   пилюля перестаёт читаться как ярлык. */
.case-tag {
  display: inline-block;
  margin-bottom: var(--_spacing---spacing-10);
  padding: 3px var(--_spacing---spacing-5);
  border: 1px solid currentColor;
  border-radius: 8px;
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
  letter-spacing: normal;
  text-transform: uppercase;
}

/* --- Цвет метки по её роли --------------------------------------

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

   ⚠️ Цвет здесь избыточен, а не обязателен: в каждой метке роль написана
   словом. Это то же требование, что кейс предъявляет к своему гриду
   в решении 07, — информация не должна зависеть от цвета одного.
   Нарушать его на собственной странице нельзя.

   Значения выбраны под контраст на белом: 5.9, 6.6 и 6.3 к одному,
   то есть с запасом к порогу 4.5 для обычного текста. В библиотеке сайта
   таких цветов нет — она вся нейтральная, поэтому переменные заводятся
   здесь и только для меток. */
:root {
  --case-tag--rejected: #b3261e;
  --case-tag--chosen: #166534;
  --case-tag--requested: #1d4ed8;
  --case-tag--tradeoff: #92400e;
}

.case-rejected > .case-tag,
.case-prior > .case-tag:not(.case-tag--neutral),
.case-versus-side--rejected > .case-tag {
  color: var(--case-tag--rejected);
}

.case-tag--chosen { color: var(--case-tag--chosen); }
.case-tradeoff > .case-tag { color: var(--case-tag--tradeoff); }

/* Связь с правилом — тоже зелёным: это принятое решение, а не
   отвергнутое. Красного на странице и без того много: блоков
   альтернатив семнадцать, а связок с правилами двенадцать. */
.case-rule-link > .case-tag { color: var(--case-tag--chosen); }

/* Боковая линия блока идёт в цвет его метки: связь метки с абзацем
   держится не только соседством. */
.case-rejected { border-left-color: var(--case-tag--rejected); }
.case-tradeoff { border-left-color: var(--case-tag--tradeoff); }
.case-rule-link { border-left-color: var(--case-tag--chosen); }
.case-prior:has(> .case-tag:not(.case-tag--neutral)) {
  border-left-color: var(--case-tag--rejected);
}

/* Метка на заголовке решения: пришло от пользователей или
   из собственного анализа. Стоит рядом с заголовком, а не
   под ним — иначе теряется при беглом чтении. */
.case-tag--requested {
  margin-left: var(--_spacing---spacing-10);
  margin-bottom: 0;
  vertical-align: middle;
  color: var(--case-tag--requested);
}

/* Блоки-сноски на полях: от чего отказались, чем заплатили,
   как решение связано с правилом. Боковая линия отделяет их
   от основного текста, не превращая в ещё один абзац. */
.case-rejected,
.case-tradeoff,
.case-rule-link {
  margin-top: var(--_spacing---spacing-30);
  padding-left: var(--_spacing---spacing-20);
  border-left: 1px solid var(--colors--matt-black);
  max-width: 52ch;
}

/* Текст, идущий ПОСЛЕ бокового блока, получает тот же отступ, что
   боковой блок получает сверху. Иначе абзац прилипает к его нижней
   кромке: у блока ровный низ и нулевая нижняя маржа у последнего
   абзаца, и разделителя не остаётся вовсе.

   Заметно это стало не сразу: у `.case-modes` низ рваный — три колонки
   разной высоты, — и воздух там появляется сам собой. У бокового блока
   низ ровный, и та же нулевая маржа читается как склейка.

   Значение то же, что и сверху: боковой блок живёт в своём конверте
   в 30px с обеих сторон, а не приклеен к тому, что под ним. */
.case-rejected + .work-rich-text,
.case-tradeoff + .work-rich-text,
.case-rule-link + .work-rich-text,
.case-prior + .work-rich-text {
  margin-top: var(--_spacing---spacing-30);
}

.case-rejected p:last-child,
.case-tradeoff p:last-child,
.case-rule-link p:last-child {
  margin-bottom: 0;
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
}

/* --- три цветовых режима ------------------------------------ */

/* Режимы стоят в ряд, а не списком: их и выбирают сравнением.
   Волосяная линия сверху отделяет таблицу от текста решения. */
.case-modes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--_spacing---spacing-15);
  row-gap: var(--_spacing---spacing-20);
  margin-top: var(--_spacing---spacing-30);
  padding-top: var(--_spacing---spacing-15);
  border-top: 1px solid var(--colors--matt-black);
}

/* Четыре карточки вместо трёх: формы цветовой слепоты, под которыми
   проверялись режимы. Карточки короче, чем у режимов, поэтому на планшете
   они складываются в две колонки, а не в одну — четыре коротких абзаца
   столбиком занимают пол-экрана ни за чем.

   ⚠️ Селектор двойной, `.case-modes.case-modes--four`, а не одиночный.
   Ниже по файлу есть правило `≤991 { .case-modes: 1fr }` для блока
   режимов, и при равной специфичности побеждало бы оно — просто потому
   что стоит позже. Двойной класс снимает зависимость от порядка строк. */
.case-modes--four { grid-template-columns: repeat(4, 1fr); }

@media screen and (max-width: 991px) {
  .case-modes.case-modes--four { grid-template-columns: repeat(2, 1fr); }
}

@media screen and (max-width: 479px) {
  .case-modes.case-modes--four { grid-template-columns: 1fr; }
}

.case-mode p {
  margin-bottom: var(--_spacing---spacing-5);
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
}

.case-mode p:last-child { margin-bottom: 0; }

.case-mode-name { font-size: var(--_typography---font-size--body); }

/* Пара цветов подписана словами, а не образцами: образцы пришлось бы
   выдумывать точными значениями, а их на странице быть не должно. */
.case-mode-pair { font-size: var(--_typography---font-size--small); }


/* --- визуалы, пары и сравнение ------------------------------ */

.case-figure { margin: 0; }

/* ⚠️ Настоящее медиа кейса не кадрируется коробкой.

   `.media-block` в шаблоне — это `object-fit: cover` плюс `height: 100%`:
   всё, что не совпало с пропорцией коробки, срезается. А коробка едет
   с заглушки классом `aspect-ratio-*`, который шаблон переопределяет
   по брейкпоинтам: 1.74 → 1.45 на ≤991 → 1.22 на ≤479. Для декоративной
   картинки это и задумано, для снятого интерфейса — нет: у кадра 2582×1440
   на планшете уходила пятая часть ширины, у портретного скриншота
   938×1088 в коробке 1.74 — половина высоты.

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

   Обложка (`ph--fill` внутри `.case-figure--full`) под правило не
   попадает: там медиа намеренно заполняет параллакс-блок. */
.case-figure > .media-block { height: auto; }

/* Визуал от чужого плеера. Коробка нужна по той же причине, что и медиа,
   только жёстче: у iframe своего размера нет вовсе, без пропорции он
   схлопывается в строку. Пропорцию пишет сборщик из content/embeds.json. */
.case-embed {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: var(--sizing--border-radius);
  background: var(--_colors---matt-black);
}

.case-embed > iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Портрет шириной колонки не растягивается: мокап телефона во всю
   ширину даёт полтора экрана высоты и ломает ритм страницы. Ему
   задаётся своя ширина, высоту считает пропорция, место в колонке —
   по центру.

   ⚠️ Ширина именно определённая, а не `auto`: картинки грузятся лениво,
   а у незагруженной `width: auto` даёт ноль. Коробка схлопывалась бы
   до нуля и распрямлялась в момент загрузки — прыжок раскладки ровно
   там, где блок появляется при прокрутке. */
.case-figure > .media-block--portrait {
  display: block;
  width: min(100%, 560px);
  margin-left: auto;
  margin-right: auto;
}

/* --- ПРОБА, 08.09.2026: увеличение кадра под курсором -----------

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

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

   ⚠️ Коробка не имеет ни своих полей, ни своей пропорции, ни своего
   фона. Она обжимает картинку по факту, и это условие правильной
   работы: скрипт считает курсор в процентах от коробки, а картинка
   отсчитывает точку роста от собственного бокса. Разойдутся — кадр
   поедет не туда, куда ведут мышкой.

   ⚠️ Кадром считается и картинка, и луп: на MRP они стоят вперемешку
   в одном рассказе, и правила у них одни. Селекторы поэтому парные —
   `img` и `video`; пропорцию и заливку коробки держит общий
   `.case-zoom > .media-block`, класс есть и у тех, и у других.

   ⚠️ Всё увеличение — внутри `@media (hover: hover) and (pointer: fine)`.
   На тач-экране `:hover` залипает после касания: кадр остался бы
   увеличенным навсегда, а сдвинуть его нечем — курсора нет. */
.case-zoom {
  display: block;
  overflow: hidden;
}

/* Высоту картинка считает по своей пропорции — то же, что делало
   правило для прямого ребёнка фигуры, пока коробки между ними
   не было.

   ⚠️ `display: block` обязателен. Картинка в шаблоне — `inline-block`,
   а такая сидит на базовой линии строки, и под ней остаётся 4–5px
   пустоты. Коробка кроится по картинке, и эти пиксели разошлись бы
   между её низом и низом кадра — а расхождение коробки с картинкой
   уводит точку роста, см. шапку раздела. */
.case-zoom > .media-block {
  display: block;
  height: auto;
}

/* Внутри параллакс-окна наоборот: подвижный слой выше окна на 16%,
   и кадр обязан занять его целиком — иначе поедет пустота. */
.case-parallax-inner > .case-zoom,
.case-parallax-inner > .case-zoom > .media-block { height: 100%; }

/* Портрет: предельная ширина и центрирование перешли с картинки
   на коробку, причина — в `zoomFigures`. Значение то же, что
   у `.case-figure > .media-block--portrait`, и меняться они обязаны
   вместе. */
.case-figure > .case-zoom--portrait {
  width: min(100%, 560px);
  margin-left: auto;
  margin-right: auto;
}

@media (hover: hover) and (pointer: fine) {
  /* Коробка — система координат для подсказки. Кадр внутри
     не позиционируется, поэтому на раскладку это не влияет. */
  .case-zoom { position: relative; cursor: zoom-in; }
  .case-zoom.is-zoomed { cursor: zoom-out; }

  /* 600ms — столько, чтобы движение читалось как приближение,
     а не как подмена кадра. Кривая симметричная, `ease-in-out`:
     заказчик просил именно её, и для парного движения «вырос —
     вернулся» она правильнее односторонней. */
  .case-zoom > img,
  .case-zoom > video {
    transition: transform 600ms cubic-bezier(.42, 0, .58, 1);
  }

  /* Состояние ставит скрипт по щелчку. Наведения здесь намеренно нет:
     курсор проходит по кадру на пути к тексту, и кадр дёргался бы сам
     по себе. Щелчок — это намерение. */
  .case-zoom.is-zoomed > img,
  .case-zoom.is-zoomed > video { transform: scale(2); }

  /* Точку роста двигает скрипт, и делает это без перехода — намеренно.
     Переход на `transform-origin` превратил бы панораму в догоняющую:
     курсор уже там, кадр ещё едет. Под курсором должно оставаться
     то, что под ним и было. */

  /* --- подсказка -------------------------------------------

     Пилюля в углу кадра. До щелчка говорит, что кадр кликабельный;
     после — что можно вести мышкой и чем закрыть. Одно место, одна
     форма, меняется только содержимое: две подсказки в разных углах
     читались бы как два разных элемента интерфейса.

     ⚠️ `pointer-events: none` обязателен: пилюля лежит поверх кадра,
     и без этого щелчок по ней не доходил бы до коробки — то есть
     ровно в том углу, куда человек смотрит, кадр бы не закрывался.

     Форма и кегль — от `.case-tag`: это тот же жанр, метка на кадре,
     и заводить ей второй вид незачем. Отличие одно — заливка: метка
     стоит на белом поле страницы, а пилюля на самом снимке, и без
     подложки она читалась бы через раз. */
  .case-zoom-hint {
    position: absolute;
    right: var(--_spacing---spacing-15);
    bottom: var(--_spacing---spacing-15);
    display: grid;
    pointer-events: none;
    opacity: 0;
    transition: opacity 300ms cubic-bezier(.42, 0, .58, 1);
  }

  .case-zoom:hover .case-zoom-hint,
  .case-zoom.is-zoomed .case-zoom-hint { opacity: 1; }

  /* Обе подсказки лежат в одной ячейке грида друг на друге: пилюля
     не меняет размер при переключении и не дёргает угол кадра. */
  .case-zoom-hint-state {
    grid-area: 1 / 1;
    display: inline-flex;
    align-items: center;
    gap: var(--_spacing---spacing-5);
    padding: 3px var(--_spacing---spacing-10);
    border-radius: 8px;
    background: var(--colors--matt-black);
    color: var(--colors--cloud-white);
    font-size: var(--_typography---font-size--small);
    line-height: 1.4;
    letter-spacing: normal;
    white-space: nowrap;
    transition: opacity 200ms linear;
  }

  .case-zoom-hint-state svg {
    width: 1em;
    height: 1em;
    flex: none;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .case-zoom-hint-state--move { opacity: 0; }
  .case-zoom.is-zoomed .case-zoom-hint-state--in { opacity: 0; }
  .case-zoom.is-zoomed .case-zoom-hint-state--move { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .case-zoom > img,
  .case-zoom > video { transition: none; }
}

/* Первый кадр страницы идёт во всю ширину, без полей по бокам:
   нужен эффект без границ, кадр упирается в края окна.
   Снимаются ровно поля контейнера, а не 100vw: при вертикальной
   полосе прокрутки 100vw шире страницы и даёт горизонтальный скролл. */
.case-figure--full {
  margin-left: calc(-1 * var(--sizing--padding-global));
  margin-right: calc(-1 * var(--sizing--padding-global));
}

/* Подпись под ним поля возвращает себе: у самого края её не прочитать. */
.case-figure--full .case-caption {
  padding-left: var(--sizing--padding-global);
  padding-right: var(--sizing--padding-global);
}

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

   ⚠️ Правило написано через родителя, а не через исключения.
   Одиночный кадр лежит прямо в потоке — в `.container` или внутри
   решения; кадры в ряду лежат в своей сетке, `.case-pair`
   или `.case-small-grid`. Перечислять сетки в исключениях нельзя:
   первая же версия так и делала, забыла про `.case-small-grid`,
   и три кадра в ряду наехали друг на друга по 15px. Появится новая
   сетка — она не попадёт под правило сама собой.

   Снимаются ровно поля контейнера, а не 100vw: при вертикальной полосе
   прокрутки 100vw шире страницы и даёт горизонтальный скролл. */
.container > .case-figure,
.case-decision > .case-figure {
  margin-left: calc(-1 * var(--sizing--padding-global));
  margin-right: calc(-1 * var(--sizing--padding-global));
}

/* Подпись поля возвращает себе: у самого края её не прочитать. */
.container > .case-figure > .case-caption,
.case-decision > .case-figure > .case-caption {
  padding-left: var(--sizing--padding-global);
  padding-right: var(--sizing--padding-global);
}

/* Два визуала в одну строку: связанные кадры читаются вместе,
   а не по очереди через полтора экрана прокрутки. */
.case-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--_spacing---spacing-15);
  row-gap: var(--_spacing---spacing-40);
  align-items: start;
}

/* Сравнение принятого с отвергнутым. Отвергнутая сторона
   приглушена: разница должна читаться до того, как прочитан
   ярлык. При наведении возвращается в полную силу. */
.case-versus {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--_spacing---spacing-15);
  row-gap: var(--_spacing---spacing-40);
  align-items: start;
}

.case-versus-side--rejected {
  opacity: .5;
  transition: opacity 500ms cubic-bezier(.19, 1, .22, 1);
}

.case-versus-side--rejected:hover { opacity: 1; }

/* --- подписи ------------------------------------------------ */

/* Подпись у всех визуалов одна и та же: один класс, один кегль,
   по центру под кадром. Разнобой в подписях читается как
   недоделка, поэтому второго уровня здесь нет.
   Одиннадцать из них — рельс из спецификации: прочитанные
   подряд, они складываются в связный рассказ. */
.case-caption {
  display: block;
  margin: var(--_spacing---spacing-15) auto 0;
  max-width: 52ch;
  color: var(--colors--matt-black);
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
  text-align: center;
}

.case-caption strong {
  color: var(--colors--matt-black);
  font-weight: 400;
}

/* Подпись под сравнением относится к обоим кадрам сразу,
   поэтому стоит под всей парой. */
.case-caption--versus { margin-top: var(--_spacing---spacing-30); }

.case-small-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--_spacing---spacing-15);
  row-gap: var(--_spacing---spacing-40);
}

/* --- заглушки под несобранные визуалы ----------------------- */

/* Временные. Пропорции настоящие — по ним видно масштаб и место
   в раскладке. Внутри три части подряд: имя файла с форматом,
   что показываем, как снимать. Когда визуал готов, блок .ph
   заменяется на <img> или <video> с тем же классом пропорций. */
.ph {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  width: 100%;
  padding: var(--_spacing---spacing-20);
  background-color: var(--colors--cloud-white);
  border: 1px solid var(--colors--gray);
  color: var(--colors--granite-gray);
  overflow: hidden;
}

/* Схемы и видео отличаются штриховой рамкой: по странице сразу
   видно, где что ещё предстоит снять. */
.ph--diagram, .ph--video { border-style: dashed; }

/* Заглушка на одну строку: не картинка, а недостающая ссылка. Ей не
   нужны пропорции кадра и иконка — только имя того, чего не хватает. */
.ph--line {
  padding-top: var(--_spacing---spacing-10);
  padding-bottom: var(--_spacing---spacing-10);
  border-style: dashed;
}

.ph-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 48px;
  height: 48px;
  color: var(--colors--gray);
}

.ph-icon svg { width: 100%; height: 100%; stroke-width: 1.2; }

.ph-meta {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--_spacing---spacing-10);
  max-width: 60ch;
  font-size: var(--_typography---font-size--small);
  line-height: var(--_typography---font-height--body-height);
}

/* Имя файла и формат — первое, что должно быть видно: по нему
   визуал ищут и им же его называют при сдаче. */
.ph-name {
  color: var(--colors--matt-black);
  font-size: var(--_typography---font-size--body);
}

.ph-spec { color: var(--colors--gray); }

/* Строка контакта — та же разметка и те же правила, что у строк
   подвала и блока About. */
.case-row.link {
  flex-flow: row;
  justify-content: space-between;
  align-items: baseline;
  width: 100%;
  max-width: 52ch;
  height: auto;
  padding-top: var(--_spacing---spacing-5);
  padding-bottom: var(--_spacing---spacing-5);
  border-bottom: 1px solid var(--colors--matt-black);
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
  text-decoration: none;
}

.case-row-meta { color: var(--colors--granite-gray); }

/* --- адаптив ------------------------------------------------ */

@media screen and (max-width: 991px) {
  /* Ярлык и номер уходят над содержимым: две колонки на планшете
     оставляют тексту слишком узкую меру. */
  .case-label, .case-num { grid-area: 1 / 1 / 2 / 13; }

  .case-body {
    grid-area: 2 / 1 / 3 / 13;
    margin-top: var(--_spacing---spacing-15);
  }

  .case-standfirst { grid-area: 1 / 1 / 2 / 13; }
  .case-conflict { max-width: none; }
  .case-small-grid { grid-template-columns: 1fr 1fr; }
  .case-stats { grid-template-columns: 1fr 1fr; }
  .case-modes { grid-template-columns: 1fr; }

  .case-index { grid-template-columns: 1fr; }
  .case-index-mark { display: none; }
  .case-index-title { grid-column: 1; }
  .case-index-row { grid-template-columns: auto 1fr auto; }
  .case-index-note { display: none; }
}

@media screen and (max-width: 479px) {
  .case-fact {
    grid-template-columns: 1fr;
    row-gap: var(--_spacing---spacing-5);
  }

  .case-conflict { font-size: var(--_typography---font-size--heading-h2); }
  .case-caption { font-size: var(--_typography---font-size--body); }
  .case-small-grid { grid-template-columns: 1fr; }
  .ph { padding: var(--_spacing---spacing-15); }

  /* Пары и сравнение разъезжаются в столбик: два плотных кадра
     рядом на телефоне нечитаемы. */
  .case-pair, .case-versus { grid-template-columns: 1fr; }
}

/* Ссылки внутри резюме: ведут на кейсы, которые уже есть в портфолио.
   Подчёркивание тонкой линией на отступе — тот же язык линий, что
   у разделителей секций и маркеров списка. Цвет не меняем: в документе
   синяя ссылка выглядит чужеродно, а рекрутер и так наводит курсор. */
.resume-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: var(--colors--gray);
  transition: text-decoration-color 0.2s ease;
}

.resume-link:hover {
  text-decoration-color: var(--colors--matt-black);
}


/* --- 8. появление блоков при прокрутке ---------------------------------- */

/* Параметры сняты с интеракций главной, чтобы страницы кейсов
   ощущались тем же сайтом: сдвиг 4vw снизу за 2000ms по outQuart,
   прозрачность за 1000ms. Разные длительности намеренно — блок
   успевает проявиться раньше, чем доедет, и движение читается
   как торможение, а не как выцветание.

   Класс `reveal` вешает сборщик, класс `js-reveal` — короткий скрипт
   в <head>: оба на месте до первой отрисовки, поэтому блоки не успевают
   моргнуть видимыми. Наблюдатель в content/reveal.js добавляет
   `is-revealed`. Ни одного из классов нет — страница видна целиком.

   ⚠️ Переход объявлен на конечном состоянии, а не на начальном.
   Иначе включение скрытого состояния само по себе становится
   анимацией: блок на глазах уезжает вниз и гаснет, чтобы потом
   вернуться. Браузер берёт переход из того состояния, в которое
   элемент переходит, — поэтому он живёт здесь. */

.js-reveal .reveal {
  opacity: 0;
  transform: translate3d(0, 4vw, 0);
}

.js-reveal .reveal.is-revealed {
  opacity: 1;
  transform: none;
  transition:
    opacity 1000ms ease-in-out,
    transform 2000ms cubic-bezier(.25, 1, .5, 1);
}

@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal { opacity: 1; transform: none; }
  .js-reveal .reveal.is-revealed { transition: none; }
}

/* --- появление из-под кромки и белое полотно -------------------
   Оба приёма сняты с шапки главной, из того же списка действий,
   что и `.reveal` выше (`a-28` в analysis/ix2-report.md):

     .load-1…4          translateY 100% -> 0, 2000ms,
                        cubic-bezier(.25, 1, .5, 1), лесенка по 30ms
     .hero-media-overlay  белое полотно уезжает вверх, те же 2000ms
                          и та же кривая, задержка 120ms
     .main              прозрачность 0 -> 1 за 1000ms

   Заказчик назвал это «уплывающей белой линией и fade in»: кромка
   маски и есть та линия — текст выходит из-под неё, а с кадра
   белое полотно съезжает вверх целиком.

   ⚠️ Интеракциями Webflow это не повторить: они находят цели
   по `data-w-id`, которых у собранной нами разметки нет. Поэтому
   приём переписан свойствами, а числа взяты из их же payload —
   чтобы страница ощущалась той же, а не похожей.

   Оба класса самостоятельны: `.reveal` рядом с ними не нужен,
   и вешать его нельзя — сдвиг на 4vw поверх маски даёт двойное
   движение. Наблюдатель в content/reveal.js берёт все три. */

/* Маска. Снаружи — окно с обрезкой, внутри — единственный ребёнок,
   который и едет. Своей высоты у окна нет: её задаёт содержимое. */
/* Блочность объявлена явно: окном бывает и `span` — например,
   две строки заголовка на About, — а у строчного элемента
   `overflow` не работает вовсе. */
.reveal-mask {
  display: block;
  overflow: hidden;
}

/* Подвижный слой, который ставит `curtain` внутрь готового элемента.
   Блочность объявляется здесь, а не на `.reveal-mask > *`: под окном
   бывает и флекс-строка (заголовок группы со счётчиком), и ей `block`
   всё бы сломал. */
.mask-inner { display: block; }

.js-reveal .reveal-mask > * { transform: translate3d(0, 100%, 0); }

.js-reveal .reveal-mask.is-revealed > * {
  transform: none;
  transition: transform 2000ms cubic-bezier(.25, 1, .5, 1);
}

/* Белое полотно. Лежит поверх кадра и уезжает вверх, открывая его.
   `::after` вместо элемента: полотно ничего не значит для читателя
   и не должно попадать ни в разметку, ни в озвучку. */
/* Полотно обязано быть обрезано своим кадром: без обрезки оно,
   уезжая вверх, показалось бы белой полосой поверх того, что стоит
   выше. `clip` вместо `hidden` по той же причине, что у `.card-media`:
   `hidden` делает элемент контейнером прокрутки и ломает
   прокрутку-таймлайн внутри него. */
.reveal-wipe {
  position: relative;
  overflow: clip;
}

.reveal-wipe:after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background-color: var(--colors--white);
  transform: translate3d(0, -100%, 0);
  pointer-events: none;
}

.js-reveal .reveal-wipe:after { transform: none; }

.js-reveal .reveal-wipe.is-revealed:after {
  transform: translate3d(0, -100%, 0);
  transition: transform 2000ms cubic-bezier(.25, 1, .5, 1);
}

/* Кадр под полотном проявляется вместе с ним — тем же fade, что
   у `.main` на главной. Полотно белое, фон страницы белый: без
   этого кадр появлялся бы резкой кромкой, а не выходил из света. */
.js-reveal .reveal-wipe { opacity: 0; }

.js-reveal .reveal-wipe.is-revealed {
  opacity: 1;
  transition: opacity 1000ms ease-in-out;
}

/* Системная настройка «меньше движения»: всё уже на местах, полотно
   убрано, переходов нет. До этих правил дело обычно не доходит —
   скрипт в <head> при такой настройке не ставит `js-reveal` вовсе, —
   но состояние обязано быть определено и без него. */
@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal-mask > * { transform: none; }
  .js-reveal .reveal-wipe { opacity: 1; }
  .js-reveal .reveal-wipe:after { transform: translate3d(0, -100%, 0); }
  .js-reveal .reveal-mask.is-revealed > *,
  .js-reveal .reveal-wipe.is-revealed,
  .js-reveal .reveal-wipe.is-revealed:after { transition: none; }
}


/* --- 9. кейс личного продукта: жалоба → ответ --------------------------- */

/* Банковские кейсы держатся на двух правилах, из которых выводятся
   решения. У личного продукта каркас другой: девять раз повторяется
   пара «жалоба → ответ». Форма повторения и есть ритм страницы,
   поэтому она описана здесь как один приём, а не набором частных
   правил под каждую секцию. */

/* --- чипы владения ------------------------------------------ */

/* Строка меток у каждой секции: подсвечено то, что понадобилось
   именно здесь. К концу страницы читатель видит, что подсвечивалось
   всё и всегда один человек. Это визуальный ответ на «он правда вёл
   это сам» — без единой фразы об этом в тексте. */
.case-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--_spacing---spacing-5);
  margin: 0 0 var(--_spacing---spacing-15);
  padding: 0;
  list-style: none;
}

.case-chip {
  padding: 0.2em 0.7em;
  border: 1px solid var(--colors--gray);
  border-radius: 2em;
  color: var(--colors--gray);
  font-size: var(--_typography---font-size--small);
  line-height: 1.6;
  letter-spacing: normal;
  white-space: nowrap;
}

/* Подсветка — цветом и весом рамки, не фоном: плашки в ряд по восемь
   превращают строку в разноцветную гирлянду и перетягивают внимание
   с текста, который под ней. */
.case-chip--on {
  border-color: var(--colors--matt-black);
  color: var(--colors--matt-black);
}

/* --- блок жалобы -------------------------------------------- */

/* Открывает каждую секцию и должен читаться как чужая реплика,
   а не как наш текст. Отсюда крупный кегль на самой жалобе,
   отбивка линией слева и приглушённое объяснение под ней. */
.case-complaint {
  margin: 0 0 var(--_spacing---spacing-30);
  padding-left: var(--_spacing---spacing-15);
  border-left: 1px solid var(--colors--matt-black);
}

.case-complaint-said {
  margin-bottom: var(--_spacing---spacing-10);
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
}

.case-complaint-why {
  max-width: 52ch;
  margin-bottom: 0;
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--body);
}

/* --- «the version before this one» -------------------------- */

/* Место блока `Considered instead` из банковских кейсов. Разница
   не косметическая: в коммерческом проекте выбираешь между
   вариантами на бумаге, в личном — сначала делаешь неправильно,
   потом переделываешь. Вторая формулировка честнее и звучит
   как личный проект.
   Оформление намеренно то же, что у соседних кейсов: это одна
   и та же роль в чтении, менять её вид значило бы сказать, что
   роль другая. */
.case-prior {
  margin-top: var(--_spacing---spacing-20);
  padding-top: var(--_spacing---spacing-10);
  border-top: 1px solid var(--colors--cloud-white);
  max-width: 52ch;
}

.case-prior p:last-child { margin-bottom: 0; }

/* --- кадр прошлой версии продукта ---------------------------

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

   ⚠️ Кадр НЕ приглушается, в отличие от отвергнутой стороны
   в `case-versus`. Там сравниваются два варианта одного решения,
   и разница должна читаться раньше ярлыка. Здесь кадр — улика,
   и разглядывать его нужно в полную силу; роль несёт ярлык,
   набранный словами.

   Половина ширины — не вкус, а ограничение исходника: слайды
   прошлой версии существуют только в 1400px, и во всю ширину
   страницы мелкий текст на них мылит. Половину даёт `case-pair`
   с единственным ребёнком — своей раскладки здесь не заводим. */
.case-figure--prior > .case-tag {
  color: var(--case-tag--rejected);
}

/* Подпись у такого кадра прижата влево, а не по центру: кадр стоит
   в левой половине, и подпись по центру своей колонки повисала бы
   в стороне от ярлыка над ним. */
.case-figure--prior > .case-caption {
  margin-left: 0;
  text-align: left;
}

/* --- приёмы спецификации v2 кейса Quify ----------------------

   Спецификация вводит шесть типографических приёмов и запрещает
   заводить седьмой. Здесь живут четыре из них, которых в системе
   ещё не было; полоса цифр и инсет `What it cost` уже есть выше.

   ⚠️ Чипы обратных ссылок набраны моноширинным. В библиотеке сайта
   моноширинной гарнитуры нет вовсе, поэтому здесь системный стек:
   заводить ради двенадцати чипов седьмой шрифтовой файл — плата,
   несопоставимая с задачей. */
:root {
  --case-accent: var(--case-tag--requested);
  --case-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Заголовок движения. Линейка сверху — единственный разделитель
   внутри слоя решений: секции внутри движения идут подряд без
   отбивок, и именно это даёт ощущение, что их вдвое меньше. */
.case-movement {
  border-top: 1px solid var(--colors--matt-black);
  padding-top: var(--_spacing---spacing-20);
}

.case-movement-label {
  margin-bottom: var(--_spacing---spacing-5);
  font-size: var(--_typography---font-size--small);
  letter-spacing: .08em;
  text-transform: uppercase;
}

.case-movement-title {
  margin: 0;
  font-size: var(--_typography---font-size--heading-h2);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  font-weight: 400;
  color: var(--colors--matt-black);
}

/* --- пять находок ------------------------------------------

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

   Номер вынесен на поля в те же колонки 1–3, где у секций стоит их
   номер: два разных блока с крупной цифрой слева обязаны совпадать
   по вертикали, иначе страница выглядит собранной из двух вёрсток. */
.case-findings {
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-finding {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--_spacing---spacing-15);
  padding-top: var(--_spacing---spacing-30);
  padding-bottom: var(--_spacing---spacing-30);
  border-top: 1px solid var(--colors--gray);
  scroll-margin-top: var(--_spacing---spacing-60);
}

.case-finding:last-child { border-bottom: 1px solid var(--colors--gray); }

/* --- находка как ссылка -------------------------------------

   Ведёт себя ровно как строка индекса у банковских кейсов: заливка
   выезжает слева при наведении, стрелка сдвигается вправо, щелчок
   уносит к началу секции. Разнобой в поведении двух списков, стоящих
   на одном месте страницы, читался бы как недоделка.

   ⚠️ Ссылкой сделана ведущая строка, а растянута она псевдоэлементом
   на всю находку. Обернуть строку целиком в `<a>` нельзя: внутри
   уже лежат чипы на подсекции, а ссылка внутри ссылки — невалидная
   разметка, и браузеры разбирают её каждый по-своему. При таком
   решении у ссылки есть и доступное имя — текст находки, — которого
   пустая растянутая ссылка не имеет. */
.case-finding { position: relative; overflow: hidden; }

.case-finding:before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--colors--cloud-white);
  transform: translate3d(-101%, 0, 0);
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.case-finding:hover:before { transform: translate3d(0, 0, 0); }

/* Содержимое поверх заливки. Чипы отдельной ступенью выше растянутой
   ссылки — иначе щелчок по чипу доставался бы ей. */
.case-finding-num,
.case-finding-body,
.case-finding-arrow { position: relative; }

.case-finding .case-chiplinks { position: relative; z-index: 2; }

.case-finding-link {
  color: inherit;
  text-decoration: none;
}

.case-finding-link:after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.case-finding-arrow {
  grid-column: 12 / 13;
  align-self: start;
  text-align: right;
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.case-finding:hover .case-finding-arrow { transform: translate3d(6px, 0, 0); }

/* Кегль ×3 от основного и приглушённый цвет — как просит
   спецификация. Табличные цифры: пять номеров стоят столбиком,
   и разная ширина знаков сдвигала бы их друг относительно друга. */
.case-finding-num {
  grid-column: 1 / 4;
  margin: 0;
  font-size: calc(var(--_typography---font-size--body) * 3);
  line-height: .9;
  color: var(--colors--gray);
  font-variant-numeric: tabular-nums;
}

.case-finding-body { grid-column: 4 / 12; }

/* Отбивка по краям — как у строки индекса: без неё текст
   прилипает к кромке выехавшей заливки. */
.case-finding-num { padding-left: var(--_spacing---spacing-15); }
.case-finding-arrow { padding-right: var(--_spacing---spacing-15); }

/* Ведущая строка и чипы в одной строке: чипы прижаты вправо,
   и на узком экране строка распадается — чипы уходят под тело. */
.case-finding-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--_spacing---spacing-10);
  margin-bottom: var(--_spacing---spacing-10);
}

.case-finding-lead {
  margin: 0;
  font-size: var(--_typography---font-size--heading-h5);
  line-height: var(--_typography---font-height--heading-height);
  font-weight: 600;
  color: var(--colors--matt-black);
}

.case-finding-body > p:last-child { margin-bottom: 0; max-width: 52ch; }

/* --- чипы обратных ссылок -----------------------------------

   Ведут в обе стороны: из находки в секцию, которая её закрывает,
   и из секции обратно в находку. Пара ссылок, а не одна, —
   потому что читают страницу и сверху вниз, и с середины. */
.case-chiplinks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--_spacing---spacing-10);
  margin: 0 0 var(--_spacing---spacing-20);
  padding: 0;
  list-style: none;
}

.case-chiplink {
  font-family: var(--case-mono);
  font-size: 12px;
  line-height: 1.6;
  letter-spacing: normal;
  color: var(--case-accent);
  text-decoration: none;
}

.case-chiplink:hover { text-decoration: underline; }

/* --- разрядка капителью -------------------------------------

   Третий уровень выделения. Подчёркивание на эту роль не годится:
   оно зарезервировано за ссылками, и текст с подчёркнутым словом
   посреди абзаца читается как неработающая ссылка. Разрядка даёт
   ту же паузу в чтении и ни на что не претендует. */
.case-term {
  font-size: .92em;
  letter-spacing: .08em;
  font-weight: inherit;
}

/* --- блок про имя продукта ----------------------------------

   Мелкий кегль, волосяная линейка сверху, ширина в одну колонку:
   это сноска, а не заявление. Сказанная один раз, она закрывает
   все кадры, где видно прежнее имя; повторённая под каждым —
   читалась бы как оправдание. */
.case-namenote {
  border-top: 1px solid var(--colors--gray);
  padding-top: var(--_spacing---spacing-10);
  max-width: 52ch;
  font-size: var(--_typography---font-size--small);
  line-height: 1.6;
}

.case-namenote p { margin: 0; }

/* --- живые ссылки под лидом ---------------------------------

   Продукт вышел — это самый сильный сигнал на странице после самих
   решений, и он обязан стоять выше первого кадра. Не кнопки: кнопка
   в этом месте читается как реклама, а строка — как факт. */
.case-live {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--_spacing---spacing-10);
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-live li + li:before {
  content: "·";
  margin-right: var(--_spacing---spacing-10);
  color: var(--colors--gray);
}

/* --- живые ссылки крупным кеглем -----------------------------

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

   Серый здесь не приглушение, а ранг. У главы он означает «указатель
   на полях», у этой строки — «факт о продукте, а не призыв нажать».
   Кнопкой её делать нельзя: кнопка в этом месте читается как реклама.

   ⚠️ Серым красится всё разом — текст, стрелка и линия подчёркивания.
   Чёрная линия под серым текстом читалась бы как чужая, приехавшая
   из другого блока. */
.case-live--big .case-row {
  font-size: calc(var(--_typography---font-size--body) * 2);
  line-height: 1.35;
  font-weight: 500;
  color: var(--colors--gray);
}

.case-live--big .case-row-meta { color: var(--colors--gray); }

.case-live--big .underline { border-top-color: var(--colors--gray); }

/* Реакция на наведение.

   ⚠️ Своей у этих ссылок не было вовсе, и это не недосмотр вёрстки:
   родное подчёркивание сайта выезжает интеракцией Webflow, а та ищет
   цель по `data-w-id`. У ссылок, написанных нами, такого атрибута нет
   и взяться ему неоткуда — значит, реакцию надо описывать стилем.

   Серый уходит в чёрный: тот же переход, что у остального текста
   страницы при наведении на строку. Линия и стрелка идут вместе
   с текстом — линия, оставшаяся серой под почерневшим текстом,
   читалась бы как незакрашенная. */
.case-live--big .case-row,
.case-live--big .case-row-meta,
.case-live--big .underline {
  transition: color 300ms cubic-bezier(.19, 1, .22, 1),
              border-top-color 300ms cubic-bezier(.19, 1, .22, 1);
}

.case-live--big .case-row:hover,
.case-live--big .case-row:hover .case-row-meta {
  color: var(--colors--matt-black);
}

.case-live--big .case-row:hover .underline {
  border-top-color: var(--colors--matt-black);
}

/* На узком экране ссылки встают столбиком: две строки в такой кегль
   в одну линию не помещаются, а разделительная точка между ними
   повисает в воздухе. */
@media screen and (max-width: 767px) {
  .case-live--big { display: block; }
  .case-live--big li + li { margin-top: var(--_spacing---spacing-15); }
  .case-live--big li + li:before { content: none; }
}

/* --- предел ширины кадра ------------------------------------

   Слайды существуют только в ширине 1400px. Растянутый на полторы
   тысячи пикселей кадр не становится подробнее — он становится
   мягче, и мелкий текст внутри мылит. 960 — та ширина, на которой
   кадр ещё уменьшается, а не увеличивается. */
.case-figure--narrow > img {
  max-width: 960px;
  margin-left: auto;
  margin-right: auto;
}

/* --- воздух на странице кейса --------------------------------

   Множителя здесь больше нет. Отбивки были удвоены решением заказчика
   09.09.2026, между секциями учетверены, потом сведены к двум, а тем же
   вечером убраны совсем: страница должна дышать так же, как соседние
   кейсы, а не по-своему.

   Правило теперь звучит короче некуда: воздух общий, из системы.
   Разойтись он может только вместе со всем сайтом.

   ⚠️ Класс `case-page` на странице остался — за него цепляется ширина
   кадров ниже. Пустым он не стоит. */

/* --- кадр во всю ширину строки -------------------------------

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

   ⚠️ Это отменяет предел 960px из спецификации, и цена названа
   в шапке `content/quify.html`: слайды существуют только в ширине
   1400px, и во всю ширину окна мелкий текст на них мылит. */
.case-page .case-figure--full > img,
.case-page .case-figure--full > video { max-width: none; }

/* --- кадр по левому краю колонки текста ----------------------

   Внутри свёрнутого блока кадры ширину не меняют, но встают на ту же
   вертикаль, что и текст главы: колонка четвёртая, край в край
   с заголовком и абзацами. Подпись там же прижата влево — по центру
   она повисала бы в стороне от кадра. */
.case-figure--incol {
  grid-area: 1 / 4 / 2 / 13;
}

.case-figure--incol > img {
  max-width: 960px;
  margin-left: 0;
  margin-right: auto;
}

.case-figure--incol > .case-caption {
  margin-left: 0;
  text-align: left;
}

/* Пара и тройная сетка внутри свёрнутого блока встают в ту же колонку,
   что и одиночный кадр: левый край у всех кадров блока один. */
.case-pair--incol,
.case-small-grid--incol { grid-area: 1 / 4 / 2 / 13; }

.case-pair--incol > .case-figure > .case-caption,
.case-small-grid--incol > .case-figure > .case-caption {
  margin-left: 0;
  text-align: left;
}

/* --- шапка большого свёрнутого блока -------------------------

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

   Строка под ним говорит, что внутри, крупно: свёрнутый блок без
   такой строки читатель пропускает, потому что не знает, чего
   лишается. Третья строка — прямое приглашение: аффорданс плюсом
   работает не для всех. */
.case-details--chapter > .case-details-head .case-details-title {
  font-size: min(calc(var(--_typography---font-size--display-2) * 0.6), 6.6vw);
  line-height: 1;
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  color: var(--colors--matt-black);
}

.case-details--chapter > .case-details-head .case-details-sub {
  margin-top: var(--_spacing---spacing-10);
  font-size: var(--_typography---font-size--heading-h4);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: normal;
  color: var(--colors--matt-black);
}

.case-details--chapter > .case-details-head .case-details-open {
  display: block;
  margin-top: var(--_spacing---spacing-10);
  font-family: var(--case-mono);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--case-accent);
}

/* Знак раскрытия у большого блока крупнее: рядом с заголовком
   в сорок восемь пунктов четырнадцать пикселей теряются. */
.case-details--chapter > .case-details-head .case-details-sign {
  width: 24px;
  height: 24px;
}

.case-details--chapter > .case-details-head .case-details-sign:before { inset: 11px 0; }
.case-details--chapter > .case-details-head .case-details-sign:after { inset: 0 11px; }

/* --- карта жалоб -------------------------------------------- */

/* Стоит на месте индекса решений, но левая колонка — то, что сказал
   пользователь, правая — что стоит на этом месте. Правая половина
   поэтому не приглушена, как примечание в индексе банковского кейса,
   а равна левой по весу: это ответ, а не сноска. */
.case-map .case-map-said { font-style: italic; }

.case-map .case-map-built {
  color: var(--colors--matt-black);
  text-align: right;
}

/* Пост-мортем в карте — не десятая жалоба, поэтому номера у него нет,
   а строка отбита сверху. */
.case-index-row--extra { color: var(--colors--granite-gray); }

@media screen and (max-width: 767px) {
  /* Ответ уходит под жалобу: в две колонки на телефоне не помещается
     ни то, ни другое. */
  .case-map .case-map-built { text-align: left; }
}


/* --- 10. серийные номера заглушек --------------------------------------- */

/* Крупный номер в углу каждой незаснятой заглушки. Он нужен не странице,
   а работе: визуалы снимаются пачками, и по кадру должно быть видно,
   какой он по счёту, без сверки длинных имён файлов.

   Синий выбран намеренно чужим для палитры: в ней синего нет вовсе,
   и номер обязан читаться как строительные леса, а не как элемент
   вёрстки. Вместе с заглушкой он исчезнет — отдельной уборки
   не потребуется. */
.ph-num {
  position: absolute;
  top: var(--_spacing---spacing-10);
  left: var(--_spacing---spacing-15);
  z-index: 1;
  color: #0047ff;
  font-size: var(--_typography---font-size--heading-h2);
  line-height: 1;
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  font-variant-numeric: tabular-nums;
}

/* У однострочной заглушки высоты под крупный номер нет: он встаёт
   в строку перед именем файла. */
.ph--line .ph-num {
  position: static;
  margin-right: var(--_spacing---spacing-10);
  font-size: var(--_typography---font-size--body);
}

@media screen and (max-width: 767px) {
  .ph-num { font-size: var(--_typography---font-size--heading-h4); }
}

/* --- блок метода: «How this was researched» ------------------
   Раскладку целиком держит общая сетка страницы: ярлык — `.case-label
   .case-label--chapter` в столбцах 1–4, всё остальное — в `.case-body`,
   столбцы 4–11. Полоса цифр — существующий `.case-stats`. Своего здесь
   ровно то, чего в системе нет: нумерованные фазы и таблица гейтов.

   Ни карточек, ни плашек, ни заливок, ни второго начертания. Акцентный
   цвет один — красный у `Considered instead`, тот же, что в решениях. */

/* Лид на ступень крупнее основного текста: первые три предложения
   рубленые, они и задают ритм блока. */
.case-method-lead {
  font-size: var(--_typography---font-size--heading-h5, 1.25rem);
  line-height: 1.45;
}

/* Фазы. Номер отдельной колонкой, дальше обычный `work-rich-text`,
   чтобы абзацы внутри фазы жили по тем же правилам, что и везде. */
.case-method-phases {
  margin: 0;
  padding: 0;
  list-style: none;
}

.case-method-phase {
  display: grid;
  grid-template-columns: 88px 1fr;
  column-gap: var(--_spacing---spacing-20);
  padding-top: var(--_spacing---spacing-30);
  border-top: 1px solid color-mix(in srgb, var(--colors--matt-black) 20%, transparent);
}

.case-method-phase + .case-method-phase { margin-top: var(--_spacing---spacing-30); }

/* Номер фазы приглушён: он маркирует порядок, но спорить с номерами
   решений и правил ему нельзя — те на странице главные. Кегль от того
   же токена, что у ярлыка правила, только вдвое мельче. */
.case-method-num {
  margin-bottom: 0;
  font-size: min(calc(var(--_typography---font-size--display-2) * 0.3), 3.3vw);
  line-height: 1;
  color: var(--colors--gray);
  font-variant-numeric: tabular-nums;
}

.case-method-phase-title {
  margin: 0 0 var(--_spacing---spacing-15);
  font-size: inherit;
  font-weight: 600;
}

/* Три гейта. Сетка, а не схема со стрелками: стрелки просят картинку,
   а картинки в блоке нет и не должно быть. Нижняя линия обязательна —
   без неё таблица выглядит оборванной. */
.case-method-gates {
  margin: var(--_spacing---spacing-30) 0 var(--_spacing---spacing-30);
  padding: 0;
  list-style: none;
  border-bottom: 1px solid color-mix(in srgb, var(--colors--matt-black) 20%, transparent);
}

.case-method-gates > li {
  display: grid;
  grid-template-columns: 56px 1.4fr 1.6fr auto;
  column-gap: var(--_spacing---spacing-20);
  align-items: baseline;
  padding: var(--_spacing---spacing-20) 0;
  border-top: 1px solid color-mix(in srgb, var(--colors--matt-black) 20%, transparent);
}

.case-method-gate-num {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  color: var(--colors--gray);
}

/* Курсив ровно один на весь блок — у вопроса. Он и отличает вопрос
   от держателя, без всякой линии между ними. */
.case-method-gate-question { font-style: italic; }

.case-method-gate-artefact {
  justify-self: end;
  text-align: right;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--colors--gray);
}

/* Правило возврата. Воздух сверху и снизу и ни одной линии: пустота
   выделяет сильнее, чем рамка. */
.case-method-return {
  margin: var(--_spacing---spacing-40) 0;
  font-size: var(--_typography---font-size--heading-h5, 1.25rem);
  line-height: 1.45;
}

.case-method-close { margin-top: var(--_spacing---spacing-30); }

@media screen and (max-width: 767px) {
  .case-method-phase { grid-template-columns: 1fr; row-gap: var(--_spacing---spacing-15); }

  /* Строка гейта складывается: номер и артефакт в одну линию сверху,
     держатель и вопрос под ними. Волосяные линии сохраняются — они
     и держат таблицу таблицей. */
  .case-method-gates > li {
    grid-template-columns: auto 1fr;
    row-gap: var(--_spacing---spacing-15);
  }
  .case-method-gate-num { grid-column: 1; grid-row: 1; }
  .case-method-gate-artefact { grid-column: 2; grid-row: 1; }
  .case-method-gate-holder { grid-column: 1 / -1; grid-row: 2; }
  .case-method-gate-question { grid-column: 1 / -1; grid-row: 3; }
}

/* --- кнопка «наверх» на страницах кейсов ---------------------
   Появляется только при прокрутке вверх и только ниже первого
   решения; при прокрутке вниз уходит вниз и гаснет. Всё состояние
   держит класс `is-shown`, который ставит content/scroll-top.js.

   Скрытая кнопка не просто прозрачна, но и не ловит нажатия:
   невидимый круг 100×100 в углу экрана перехватывал бы клики
   по содержимому под ним. */
.to-top {
  position: fixed;
  right: 5%;
  bottom: 5%;
  z-index: 60;

  width: 100px;
  height: 100px;
  display: grid;
  place-items: center;

  padding: 0;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--colors--matt-black) 25%, transparent);
  background-color: var(--colors--white);
  color: var(--colors--matt-black);
  cursor: pointer;

  opacity: 0;
  transform: translateY(24px);
  pointer-events: none;

  /* Длительности и кривая — те же, что у появления блоков при
     прокрутке, только вчетверо быстрее: блок появляется один раз
     и его разглядывают, кнопка отвечает на жест и обязана поспевать
     за рукой. */
  transition:
    opacity 260ms ease-in-out,
    transform 420ms cubic-bezier(.25, 1, .5, 1),
    border-color 200ms ease-in-out,
    background-color 200ms ease-in-out;
}

.to-top.is-shown {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.to-top > svg {
  width: 32px;
  height: 32px;
  display: block;
}

.to-top:hover {
  border-color: var(--colors--matt-black);
}

.to-top:focus-visible {
  outline: 2px solid var(--colors--matt-black);
  outline-offset: 3px;
}

/* На узких экранах 100px — это четверть ширины окна, и круг начинает
   спорить с содержимым. Уменьшается только он, поведение то же. */
@media screen and (max-width: 767px) {
  .to-top { width: 68px; height: 68px; }
  .to-top > svg { width: 24px; height: 24px; }
}

/* Движение выключено системной настройкой — кнопка не едет, а просто
   проявляется. Смысл поведения от этого не теряется. */
@media (prefers-reduced-motion: reduce) {
  .to-top { transform: none; transition: opacity 120ms linear; }
  .to-top.is-shown { transform: none; }
}

/* --- стрелки у раскрытой ленты кадров ------------------------

   Лента слайдов — самый длинный кусок страницы: пятнадцать тысяч
   пикселей у SUMU. Раскрыв её, читатель оказывается внутри свитка,
   из которого некуда деться, кроме как крутить колесо в ту или
   другую сторону. Две стрелки дают выход в оба конца: вверх —
   к началу страницы, вниз — к блоку рекомендаций сразу за лентой.

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

   Размер вдвое меньше кнопки «наверх» — 50px против 100 — и рамки
   нет: две кнопки одна над другой рядом с плотными кадрами не должны
   спорить с содержимым. Тень вместо рамки: на белых слайдах белый
   круг без неё исчезает совсем.

   Кнопка «наверх» и эти стрелки на одной странице не встречаются:
   `scroll-top.js` подключён только к собственным кейсам, лента —
   только у кейсов из коллекции. */
.deck-nav {
  position: fixed;
  right: 5%;
  bottom: 5%;
  z-index: 60;

  display: grid;
  row-gap: 15px;

  opacity: 0;
  transform: translateY(24px);
  pointer-events: none;

  transition:
    opacity 260ms ease-in-out,
    transform 420ms cubic-bezier(.25, 1, .5, 1);
}

.deck-nav.is-shown {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.deck-nav-btn {
  width: 50px;
  height: 50px;
  display: grid;
  place-items: center;

  padding: 0;
  border: 0;
  border-radius: 50%;
  background-color: var(--colors--white);
  color: var(--colors--matt-black);
  cursor: pointer;

  box-shadow: 0 2px 10px color-mix(in srgb, var(--colors--matt-black) 12%, transparent);
  transition: box-shadow 200ms ease-in-out;
}

.deck-nav-btn > svg {
  width: 16px;
  height: 16px;
  display: block;
}

.deck-nav-btn:hover {
  box-shadow: 0 4px 16px color-mix(in srgb, var(--colors--matt-black) 22%, transparent);
}

.deck-nav-btn:focus-visible {
  outline: 2px solid var(--colors--matt-black);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .deck-nav { transform: none; transition: opacity 120ms linear; }
  .deck-nav.is-shown { transform: none; }
}

/* --- полоса кадров во всю ширину внизу кейса из коллекции ------------------

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

   Полей снимать не приходится: секция лежит вне `.container`, а `.section`
   в исходной теме их не имеет вовсе — ширина уже полная. Поэтому здесь
   нет ни отрицательных полей, ни 100vw: последний при вертикальной полосе
   прокрутки шире страницы и даёт горизонтальный скролл.

   ⚠️ `display: block` у самих картинок обязателен. Инлайновая картинка
   сидит на базовой линии текста, и под каждой остаётся 4–5px пустоты —
   ровно те зазоры, которых здесь быть не должно. */
.case-strip-inner {
  display: block;
  font-size: 0;
  line-height: 0;
}

.case-strip-inner img {
  display: block;
  width: 100%;
  height: auto;
  margin: 0;
}

/* --- ПРОБА, 08.09.2026: лента, отбитая от краёв ------------------

   Второй вид ленты: кадры не край в край, а с полями, на своём фоне.
   Заказчик просил посмотреть на PURLY.

   Фон замерен по самим кадрам, а не подобран на глаз: угол пяти
   слайдов — ровно #000000. Поэтому здесь литерал, а не токен:
   в палитре сайта чистого чёрного нет, ближайший `matt-black`
   темнее не бывает, но и не равен нулю — на стыке поля и кадра
   разница была бы видна полосой.

   ⚠️ Поля в `vw`, а не в `--sizing--padding-global`. У ленты нет
   контейнера и равняться ей не на что, кроме окна: токен отступа
   держит текстовые блоки на сетке страницы, а лента живёт вне её.
   `vw` считает вместе с полосой прокрутки, поэтому поле выходит
   чуть шире объявленного — но никогда уже, и горизонтальной
   прокрутки от него не будет.

   ⚠️ Два слайда из сорока девяти выпускают цветной блок в край
   кадра — 06 и 10. На чёрном поле он обрывается видимой границей.
   Это свойство исходников, а не вёрстки. */
.case-strip--inset {
  padding-left: 10vw;
  padding-right: 10vw;
  background-color: #000;
}

/* --- кейс из коллекции: шапка, парные ряды, рекомендации -------------------

   ⚠️ Классы здесь не могут называться `case-details`: это имя уже занято
   свёрнутым блоком с контекстом, и у того стоит `border-top`. Полоса
   колонок унаследовала его и рисовала линию прямо под заголовком кейса.
   Отсюда `case-meta`.

   Колонки шапки: одна строка, четыре ячейки по два столбца из двенадцати
   и описание справа на четыре. В четвёртой ячейке две пары «подпись —
   значение», роль и команда: пятая колонка в ряд не влезает, а вторую
   строку заводить ради неё незачем. */
.case-meta {
  align-items: start;
}

.case-meta-cell {
  grid-column: span 2;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Вторая пара внутри ячейки отбивается воздухом, а не линией: линий
   в этой полосе нет вовсе, и заводить их ради одной ячейки нельзя. */
.case-meta-cell > .case-meta-pair + .case-meta-pair {
  margin-top: var(--_spacing---spacing-20);
}

.case-meta-summary {
  grid-column: 9 / 13;
}

/* Ряд «кадр и текст». Две равные половины: кадр с одной стороны, глава
   с другой. Высоты сходятся сами — колонки грида по умолчанию тянутся
   на всю строку, поэтому кадр и текст всегда кончаются на одной линии. */
.case-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--_spacing---spacing-15);
}

/* Коробка кадра. Высоту резервирует распорка, а не `aspect-ratio` на
   самой коробке: коробка с заданной пропорцией до высоты строки не
   растягивается — в ряду, где текст оказывался длиннее, кадр отставал
   от него на два десятка пикселей и край строки выходил рваным.
   Распорка задаёт только минимум, растянуться коробке ничто не мешает.

   116% — та же пропорция 1:1.16, что у кадров этой страницы
   в исходной теме.

   Сам кадр внутри едет параллаксом: коробка несёт ещё и `case-parallax`,
   а внутри неё лежит `case-parallax-inner`. Тот абсолютный и высотой
   116% от коробки, поэтому дотягивается до любой её высоты сам. */
.case-split-media {
  min-width: 0;
}

.case-split-media::before {
  content: "";
  display: block;
  padding-top: 116%;
}

/* Текст по центру своей половины: когда глава короче кадра, прижатый
   к верху столбец выглядит оборванным, а не воздушным.

   Поля — вчетверо против шага сетки, по 60px со всех сторон. Глава
   в половину экрана без них упирается в кадр и в край страницы. */
.case-split-text {
  min-width: 0;
  padding: var(--_spacing---spacing-60);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Перевёрнутый ряд: текст слева, кадр справа. Меняется порядок вывода,
   а не разметка — в потоке кадр остаётся первым, и когда строка
   распадается в столбик, он везде оказывается над своим текстом. */
.case-split--flip .case-split-media { order: 2; }

/* --- ряд карточек в блоке рекомендаций -----------------------

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

   Ширину задаёт не этот класс, а `.case-body`, в котором ряд лежит:
   столбцы 4–11, ровно те же, что у обложки соседнего кейса в блоке
   перехода ниже. Две концовки страницы совпадают по ширине не потому,
   что её подобрали, а потому что это один и тот же столбец.

   Промежуток — шаг сетки, тот же, что между половинами пары
   «кадр и текст». */
.more-work-row {
  display: grid;
  /* ⚠️ `none` обязателен. Родной `.works-collection-list` объявляет
     `grid-template-columns: 1fr 1fr 1fr` — сетку под три карточки,
     которую выводила коллекция. Явные колонки старше `grid-auto-*`,
     поэтому две карточки вставали в первые две из трёх и занимали
     две трети ряда: 231px вместо 354 при ширине ряда 723. */
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  column-gap: var(--_spacing---spacing-15);
}

/* ⚠️ Карточка обязана занять свою ячейку целиком.

   Ссылка внутри — родной `a.w-inline-block`, то есть инлайн-блок:
   его ширина считается по содержимому. Содержимое — картинка
   с `width: 100%`, процент от ширины, которой ещё нет. Круг
   замыкается собственным размером файла, а у ленивой картинки его
   до загрузки не существует: коробка схлопывалась до 192px вместо
   354, и карточка EVVA стояла в ряду заметно ниже соседней.

   Атрибуты `width`/`height` этого не лечат — их перебивает `width:
   100%` из `.media-block`. Лечит ссылка, ставшая блоком: тогда
   ширину задаёт колонка сетки, а не картинка.

   Область — только ряд рекомендаций: `w-inline-block` рассыпан
   по всему шаблону Webflow, и трогать его целиком нельзя. */
.more-work-row .w-inline-block {
  display: block;
  width: 100%;
}

/* Телефон: ряд распадается в столбик. Две карточки рядом на 375px —
   это две картинки по 170px с подписью в две строки под каждой. */
@media screen and (max-width: 479px) {
  .more-work-row {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    row-gap: var(--_spacing---spacing-30);
  }
}

@media screen and (max-width: 991px) {
  /* Планшет: полей в 60px половина экрана уже не держит. */
  .case-split-text { padding: var(--_spacing---spacing-30); }
}

@media screen and (max-width: 767px) {
  /* Узкий экран: половины не остаётся, ряд распадается в столбик. */
  .case-split {
    grid-template-columns: 1fr;
    row-gap: var(--_spacing---spacing-30);
  }

  .case-split-text {
    display: block;
    padding: var(--_spacing---spacing-30) 0;
  }

  .case-split--flip .case-split-media { order: 0; }

  /* Колонки шапки: на узком экране сетка в 12 столбцов не работает. */
  .case-meta-cell,
  .case-meta-summary {
    grid-column: 1 / -1;
  }
}

/* --- лента кадров под ключом ------------------------------------------------

   Полоса слайдов лежит внутри свёрнутого блока — того же `<details>`,
   что и «How it was made» у Quify. Наверху страницы уже стоят разбор
   и пара мокапов; всё остальное это приложение к ним, и открывает его
   тот, кому нужно.

   Внутри блока лента остаётся во всю ширину окна: поля контейнера
   снимаются отрицательными отступами ровно на свою величину. Не 100vw —
   при вертикальной полосе прокрутки тот шире страницы и даёт
   горизонтальный скролл. */
.case-deck .case-details-body > .case-strip-inner {
  margin-left: calc(-1 * var(--sizing--padding-global));
  margin-right: calc(-1 * var(--sizing--padding-global));
}

/* Скрепка перед подписью: знак вложения. Садится на строку по базовой
   линии текста, а не по центру строки, — иначе висит выше букв. */
.case-clip {
  width: 1em;
  height: 1em;
  vertical-align: -.12em;
  margin-right: .45em;
  flex: none;
}


/* ============================================================
   9. Страница About

   Вектор — DOCS/about-page-spec-v1.md. Из спеки взяты два
   ограничения, из которых выведено остальное:

     минимум текста — на странице до 420 слов копирайта;
     максимум разнообразия — ни один блок не повторяет приём
     соседнего. Семь блоков, семь приёмов: типографика → видео →
     схема → таблица → медиа-сетка → лента → навигация.

   Основной механизм разнообразия здесь не декор, а ширина меры:
   полноширинные элементы чередуются с колонкой до 70 знаков.
   Поэтому почти всё, что ниже, — про max-width, а не про рамки.

   Всё заскоплено классом `.about-page`: остальной сайт не задет.
   ============================================================ */

.about-page {
  /* Акцент. На сайте он один и уже используется — синий из меток
     кейсов. Работает ровно в двух местах: подсветка строки таблицы
     на наведении и подчёркивание ссылок на выходе. Больше нигде. */
  --about-accent: var(--case-tag--requested, #1d4ed8);

  /* Мера текстовой колонки: до 70 знаков в строке. Держим в одном
     месте — от неё зависят подводка, абзацы принципов, строка под
     таблицей и абзац под лентой, то есть всё, что читается подряд.

     ⚠️ Значение не 70ch, а 57ch, и это не описка. Единица `ch` —
     ширина нуля, а в Bdogrotesk он заметно шире средней буквы:
     при 68ch в строку реально влезало 83 знака. 57ch замерено —
     даёт 69–70. Меняя гарнитуру, пересчитать замером, а не на глаз. */
  --about-measure: 57ch;

  /* Волосяная линия. Своя, а не `divider-20`: у того цвет
     cloud-white, почти невидимый на белом рядом с таблицей. */
  --about-hairline: 1px solid var(--colors--cloud-white);
}

/* --- 01. Hero ----------------------------------------------

   Изображения в герое нет намеренно. Заголовок работает как
   графический элемент, поэтому трекинг плотнее общего и высота
   строки ниже. Перенос между предложениями жёсткий, <br/>
   в разметке: по случайному переносу вторая фраза уезжает
   куда попало и связка рвётся. */

.about-h1 {
  /* Меры хватает на самую длинную из двух фраз. Уже — и жёсткий
     перенос перестаёт работать: строка ломается ещё раз, сама,
     и связка «не про раскладку — про происхождение» рвётся. */
  max-width: 46ch;
  line-height: 1.05;
  letter-spacing: -.03em;
}

.about-lede {
  grid-column: 1 / 9;
  max-width: var(--about-measure);
  font-size: var(--_typography---font-size--heading-h5);
  line-height: 1.4;
}

/* Полоса фактов — не капс и не мелкий кегль. Это body-строка,
   отбитая линейкой сверху: набор капсом сделал бы из неё лейбл,
   а это содержание. */
.about-rule { border-top: var(--about-hairline); }

/* Полоса фактов — единственный текст на странице шире меры,
   и намеренно: это одна строка через всю ширину, а не абзац.
   Ограничить её мерой значило бы сломать ровно тот приём,
   ради которого она стоит под линейкой. */
.about-facts {
  max-width: none;
  color: var(--colors--granite-gray);
}

/* Одна строка над блоком. Общая для 02, 05 и 06 — три разных
   блока, один способ представиться. */
.about-kicker {
  max-width: var(--about-measure);
  color: var(--colors--granite-gray);
}

/* --- 02. Видео-визитка -------------------------------------

   Полная ширина, без рамки и без тени: рамка вокруг говорящей
   головы читается как встроенный чужой плеер.

   Контролы полные и обязательны — это не анимация интерфейса,
   а человек, которого хотят перемотать и услышать. Старт
   автоматический, звук выключен; включает его зритель. */

.about-video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background-color: var(--colors--cloud-white);
  object-fit: cover;
}

/* Подпись под медиа. Единственный мелкий кегль на странице,
   и он же несёт рельс подписей: восемь строк, прочитанные
   подряд, должны складываться в связный рассказ. */
.about-caption {
  max-width: 60ch;
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
}

/* Заглушка транскрипта: строка, а не кадр. */
.about-ph-line { max-width: var(--about-measure); }

/* --- 2б. Дизайн-системы -------------------------------------

   Раскладка снята с блока «(About Filmmakers)» на des.obys.agency —
   образец прислан заказчиком. Замерено там на ширине 1440:

     лид во всю ширину, ярлык в скобках инлайном в начале строки;
     ниже четыре равные колонки по 335px с зазором 20px —
     два кадра с подписями в скобках, затем текст в две колонки.

   На нашу сетку это ложится без подгонки: три столбца из двенадцати
   дают 341px против 335px в образце, и зазор берётся тот же, что
   у всей сетки. Поэтому своих ширин здесь нет — только grid-column.

   Смысл блока — назвать вслух то, что заказчик просил подсветить:
   дизайн-система удешевляет и ускоряет разработку. Отсюда и порядок
   чтения: сначала утверждение крупно, потом два артефакта, потом
   пять коротких абзацев мелко. */

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

   Кегль всё же не H1, а H2: H1 на странице занят героем, и второе
   такое же утверждение спорило бы с ним за первенство. */
.about-ds-lead {
  max-width: none;
  font-size: var(--_typography---font-size--heading-h2);
  line-height: 1.15;
  letter-spacing: var(--_typography---letter-spacing--heading-h2);
  color: var(--colors--matt-black);
}

/* Два слова под подчёркиванием — деньги и разработка. Это и есть
   мысль блока: система удешевляет и ускоряет разработку, и многие
   не знают, что это вообще про деньги.

   Подчёркивание — `text-decoration`, а не `border-bottom`: слово
   на переносе строки должно подчёркиваться на обеих строках, и
   рамка снизу так не умеет. */
.about-ds-mark {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
}

/* Значок сразу за словом: доллар у денег, скобки кода у разработки.
   Вынесен из подчёркнутого span, иначе линия шла бы и под ним.

   Единственное место на странице, кроме подсветки строки таблицы,
   где работает акцентный цвет. Кегль мелкий и подъём вверх — это
   пометка на слове, а не часть фразы. */
.about-ds-sym {
  color: var(--about-accent);
  font-size: .38em;
  line-height: 1;
  letter-spacing: 0;
  vertical-align: super;
  white-space: nowrap;
}

/* Текст и кадры стоят в центральной колонке сетки, столбцы 4–10 —
   тех же, что тело решения на страницах кейсов. Левый край текста
   здесь и левый край текста в блоке принципов оказываются на одной
   линии не потому, что их подогнали, а потому что это один столбец.

   Ряды заданы явно: у `grid-global` межстрочный зазор нулевой,
   и автоматическое размещение развело бы блоки без отбивки. */

.about-ds-figures {
  grid-area: 2 / 4 / 3 / 11;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  column-gap: var(--_spacing---spacing-15);
  justify-items: start;
  margin-top: var(--_spacing---spacing-50);
}

/* Текст в две колонки — именно колонками, а не двумя блоками:
   абзацы разной длины сами разложатся поровну, и при правке текста
   ничего не надо перекладывать руками. */
.about-ds-text {
  grid-area: 1 / 4 / 2 / 11;
  columns: 2;
  column-gap: var(--_spacing---spacing-15);
}

.about-ds-text p {
  margin: 0 0 var(--_spacing---spacing-20);
  /* Абзац не должен разрываться между колонками посередине. */
  break-inside: avoid;
}

.about-ds-text p:last-child { margin-bottom: 0; }

/* Первый абзац с втяжкой — как в образце. Втяжка отмечает начало
   текста там, где заголовка над ним нет. */
.about-ds-first { text-indent: var(--_spacing---spacing-40); }

.about-ds-figure { margin: 0; width: 100%; }

/* Кадр вертикальный, 5:6 — как в образце. Горизонтальный скриншот
   в такой рамке пришлось бы резать по бокам, поэтому пропорция
   названа и в спецификации самой заглушки. */
.about-ds-frame { aspect-ratio: 5 / 6; }

.about-ds-frame img,
.about-ds-frame video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Решение заказчика от 13.09.2026: оба кадра стоят от верхнего
     левого угла. Если файл не совпадает с рамкой по пропорции,
     режется правый или нижний край — левый и верхний никогда:
     там подписи строк и колонок, по ним кадр и читается. */
  object-position: left top;
}

/* Подписи в скобках, как в образце. Скобки набраны текстом,
   а не сделаны псевдоэлементом: иначе они не попадут в поиск
   по странице и в чтение голосом. */
.about-ds-caption {
  display: block;
  margin-top: var(--_spacing---spacing-12);
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
}

/* --- 3. Три принципа ---------------------------------------

   Своей раскладки здесь нет и не должно быть. Пункт собран
   штатным `case-block` со страниц кейсов: номер `case-num
   case-bignum` в столбцах 1–3, содержимое в `case-body`,
   столбцы 4–10. Кегль номера, его цвет и место приезжают
   оттуда же, что и у решений на MRP, — это один и тот же код,
   а не подобранные значения. Ниже — только отбивка между
   пунктами и сам рисунок. */

.about-principle + .about-principle { margin-top: 0; }

/* Рисунок — не иконка. Иконка украшает, схема показывает механику.
   Один язык линий на все три: 1px, без заливок, без теней,
   currentColor, поэтому они переживут инверсию палитры без правок.

   Маленький: схема из двух кадров во всю ширину колонки читается
   как иллюстрация к разделу, а нужна пометка на полях. */
.about-diagram {
  max-width: 30rem;
  margin: var(--_spacing---spacing-30) 0 0;
}

.about-diagram svg {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--_spacing---spacing-12);
  color: var(--colors--matt-black);
}

/* --- 04. Метод ---------------------------------------------

   Единственная таблица на странице. Hairline между строками,
   без внешней рамки и без зебры: рамка и зебра нужны, когда
   строк много, а их пять. */

/* Заголовок блока метода стоит на месте ярлыка секции, но ярлыком
   не становится: `case-label` даёт ему только столбцы 1–3, а кегль
   и цвет остаются заголовочными. Ярлыки на сайте серые и мелкие —
   здесь же утверждение, ради которого блок и существует. */
.about-method-title {
  max-width: none;
  color: var(--colors--matt-black);
}

.about-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

/* Заголовки столбцов — body-размер, не капс: капс сделал бы
   из них лейблы, а это первая строка данных. */
.about-table th,
.about-table td {
  padding: var(--_spacing---spacing-15) var(--_spacing---spacing-15)
           var(--_spacing---spacing-15) 0;
  border-bottom: var(--about-hairline);
  font-size: var(--_typography---font-size--body);
  font-weight: 400;
  line-height: var(--_typography---font-height--body-height);
  vertical-align: top;
}

.about-table thead th {
  color: var(--colors--granite-gray);
  border-bottom-color: var(--colors--matt-black);
}

.about-table tbody th { width: 16%; }
.about-table tbody td { width: 34%; }

/* Второй столбец — то, чем работа не является. Приглушён,
   чтобы глаз шёл в третий. */
.about-table tbody td:first-of-type { color: var(--colors--granite-gray); }

/* Единственное место на странице, где акцент работает
   интерактивно. Подсветка идёт по всей строке: сравнивают
   строку, а не ячейку. */
.about-table tbody tr {
  transition: color 200ms ease-in-out, background-color 200ms ease-in-out;
}

.about-table tbody tr:hover { color: var(--about-accent); }
.about-table tbody tr:hover td:first-of-type { color: var(--about-accent); }

/* Первая ячейка шапки пустая по смыслу, но пустой <th> ломает
   чтение таблицы голосом. Поэтому подпись есть и она скрыта. */
.about-th-blank {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.about-undertable {
  max-width: var(--about-measure);
  color: var(--colors--granite-gray);
}

/* Блок «Примеры работы» снят решением заказчика 09.09.2026 —
   вместе с ним ушёл и его стиль. Кадры дизайн-системы живут
   в блоке 2б, у них своя раскладка. */

/* --- Лента лет ----------------------------------------------

   Общий компонент двух страниц: «Путь» на About и полоса проектов
   в шапке резюме. Поэтому имена нейтральные, без `about-`.

   Горизонтальная ось, узлы стоят по годам, а не через равные
   промежутки: позиция каждого приезжает из разметки переменной
   --tl-pos. Неравномерность честнее и читается как данные,
   а не как декоративный ряд.

   Два параметра задаются на самой ленте:
     --tl-steps   сколько ступеней в лесенке (3 на About, 4 в резюме);
     --tl-step    высота одной ступени.
   Ширина узла — --tl-w. Больше ничего настраивать не нужно. */

.timeline {
  --tl-step: 95px;
  --tl-w: 26%;
  position: relative;
  display: block;
  height: 340px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Сама линия. Прочерк — это масштаб по X от левого края,
   поэтому анимируется transform, а не width: ширина дёргает
   раскладку на каждом кадре. */
.timeline::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1px;
  background-color: var(--colors--matt-black);
  transform: scaleX(1);
  transform-origin: left center;
}

/* Кучные годы — 2020, 2021, 2022 — рядом в полную ширину
   не помещаются, а выравнивать их поровну нельзя: пропадёт
   то единственное, ради чего лента и рисуется. Поэтому узлы
   идут лесенкой: позиция по оси честная, опускается только текст.

   Сдвиг узла влево пропорционален его позиции. В нуле сдвига нет,
   в конце оси узел уходит влево на всю свою ширину, посередине —
   на половину. Это снимает единственную настоящую проблему такой
   ленты: узел у правого края больше не вылезает за контейнер,
   и не нужно ловить его отдельным правилом. Точка при этом
   остаётся ровно на своём годе — она смещена внутрь узла
   на ту же долю его ширины. */
.timeline-node {
  position: absolute;
  top: 0;
  left: var(--tl-pos);
  display: flex;
  flex-direction: column;
  gap: var(--_spacing---spacing-5);
  width: var(--tl-w);
  padding-top: calc(var(--_spacing---spacing-20) + var(--tl-drop, 0px));
  transform: translateX(calc(-1 * var(--tl-pos)));
}

/* Лесенка. Ступеней три по умолчанию; на ленте с --tl-steps: 4
   работает четвёртое правило, и первые три перекрываются им же
   по порядку следования. */
.timeline-node:nth-child(3n + 2) { --tl-drop: var(--tl-step); }
.timeline-node:nth-child(3n)     { --tl-drop: calc(var(--tl-step) * 2); }

.timeline--steps-4 .timeline-node:nth-child(4n + 1) { --tl-drop: 0px; }
.timeline--steps-4 .timeline-node:nth-child(4n + 2) { --tl-drop: var(--tl-step); }
.timeline--steps-4 .timeline-node:nth-child(4n + 3) { --tl-drop: calc(var(--tl-step) * 2); }
.timeline--steps-4 .timeline-node:nth-child(4n)     { --tl-drop: calc(var(--tl-step) * 3); }

/* Точка на оси. Стоит внутри узла на той же доле его ширины,
   на которую узел сдвинут влево, — то есть ровно на своём годе. */
.timeline-node::before {
  content: "";
  position: absolute;
  top: -3px;
  left: var(--tl-pos);
  margin-left: -3px;
  width: 7px;
  height: 7px;
  background-color: var(--colors--matt-black);
  border-radius: 50%;
  z-index: 1;
}

/* Поводок от оси к тексту. Без него опущенный узел висит
   в воздухе, и к какому году он относится — вопрос. */
.timeline-node::after {
  content: "";
  position: absolute;
  top: 4px;
  left: var(--tl-pos);
  width: 1px;
  height: calc(var(--_spacing---spacing-20) + var(--tl-drop, 0px) - 8px);
  background-color: var(--colors--cloud-white);
}

/* Узлы правой половины прижимают текст к своей точке справа:
   иначе год стоит у левого края узла, а точка — у правого,
   и поводок тянется через весь бокс мимо текста. */
.timeline-node--right {
  align-items: flex-end;
  text-align: right;
}

.timeline-year {
  font-size: var(--_typography---font-size--heading-h4);
  line-height: 1;
  letter-spacing: -.02em;
}

.timeline-title { max-width: 24ch; }

/* Вторая строка узла приглушена и проявляется на наведении:
   при беглом взгляде читаются годы и места, при внимательном —
   чем каждое было. */
.timeline-note {
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
  opacity: .55;
  transition: opacity 200ms ease-in-out;
}

.timeline-node:hover .timeline-note { opacity: 1; }

/* Единственное неспровоцированное движение на обеих страницах.
   Скрытое состояние живёт только под классом `js-about`, который
   ставит скрипт в <head> и только когда движение разрешено
   системной настройкой. Без скрипта и при prefers-reduced-motion
   лента просто стоит нарисованной. */
.js-timeline .timeline:not(.is-drawn)::before {
  transform: scaleX(0);
}

.js-timeline .timeline::before {
  transition: transform 600ms cubic-bezier(.25, 1, .5, 1);
}

/* ⚠️ Скрытое состояние узла — только прозрачность. Сдвиг снизу
   сюда добавлять нельзя: у узла уже есть свой transform, которым
   он смещается влево по оси, и второй перебил бы первый —
   лента разъехалась бы на время анимации. */
.js-timeline .timeline:not(.is-drawn) .timeline-node { opacity: 0; }

.js-timeline .timeline-node { transition: opacity 400ms ease-in-out; }

/* Узлы проявляются по очереди, уже после того как линия дошла
   до правого края. Лесенка на 90ms — та же, что у reveal.js
   на остальных страницах, чтобы страницы ощущались одним сайтом. */
.timeline .timeline-node:nth-child(1) { transition-delay: 600ms; }
.timeline .timeline-node:nth-child(2) { transition-delay: 690ms; }
.timeline .timeline-node:nth-child(3) { transition-delay: 780ms; }
.timeline .timeline-node:nth-child(4) { transition-delay: 870ms; }
.timeline .timeline-node:nth-child(5) { transition-delay: 960ms; }
.timeline .timeline-node:nth-child(6) { transition-delay: 1050ms; }
.timeline .timeline-node:nth-child(7) { transition-delay: 1140ms; }
.timeline .timeline-node:nth-child(8) { transition-delay: 1230ms; }

/* Полоса проектов в шапке резюме: восемь лет вместо пяти,
   поэтому лесенка в четыре ступени и узел уже. Высота считается
   от ступеней: три интервала по 100px плюс сам текст. */
.timeline--resume {
  --tl-step: 100px;
  --tl-w: 22%;
  height: 450px;
}

/* --- 7. Куда идти дальше -----------------------------------

   Две крупные ссылки заменены блоком рекомендаций — тем же, что
   закрывает страницы кейсов. Его стиль живёт в разделе про кейсы
   и здесь не дублируется: `.more-work-row` одна на весь сайт,
   и второй её копии взяться неоткуда.

   Своего здесь только отбивка перед строкой контакта. */

/* --- адаптив ------------------------------------------------ */

@media screen and (max-width: 991px) {
  .about-lede { grid-column: 1 / 13; }

  /* Принципы и блок метода: колонки схлопывает сам `case-block`
     правилом `.case-label, .case-num { grid-area: 1 / 1 / 2 / 13 }`
     в его же адаптиве. Своего здесь ничего не нужно.

     Рисунку снимаем потолок: в одну колонку 30rem от неё уже
     не половина, а почти вся ширина. */
  .about-diagram { max-width: 26rem; }

  /* Дизайн-системы: центральной колонки на планшете нет — её схлопывает
     общий адаптив сетки, — поэтому текст и кадры занимают всю ширину.
     Кадры остаются по два в ряд: на 804px это по 395px, читаемо. */
  .about-ds-text { grid-area: 1 / 1 / 2 / 13; }
  .about-ds-figures { grid-area: 2 / 1 / 3 / 13; }

  /* Лента остаётся горизонтальной до 767px, но узел здесь вдвое уже,
     и название переносится в три строки вместо двух. Ступень в 95px
     на такой высоте перестаёт разводить соседей: замер показал
     наложение 2020 на 2021 в десять пикселей. Ступень и высота
     ленты растут вместе с высотой узла — по одной строке на ленту,
     потому что высота ступени вынесена в переменную. */
  .timeline { --tl-step: 125px; height: 430px; }

  /* Полоса резюме: узел шире, чем на десктопе, а не уже. Четыре
     ступени разводят соседей на половину ленты, и место есть —
     при 22% от 804px название уезжало в четыре строки. */
  .timeline--resume { --tl-step: 130px; --tl-w: 30%; height: 560px; }
}

@media screen and (max-width: 767px) {
  /* Лента становится вертикальной. Движение сохраняется, но
     прочерк идёт сверху вниз, а не слева направо. */
  .timeline {
    height: auto;
    padding-left: var(--_spacing---spacing-30);
  }

  .timeline::before {
    width: 1px;
    height: 100%;
    transform: scaleY(1);
    transform-origin: center top;
  }

  .js-timeline .timeline:not(.is-drawn)::before { transform: scaleY(0); }

  /* Узлы встают в поток: ни абсолютной позиции, ни сдвига по оси,
     ни лесенки — иначе они лягут друг на друга. */
  .timeline-node {
    position: static;
    width: 100%;
    padding-top: 0;
    padding-bottom: var(--_spacing---spacing-30);
    transform: none;
    --tl-drop: 0px;
  }

  .timeline-node::before {
    left: calc(var(--_spacing---spacing-30) * -1 - 3px);
    top: 6px;
    margin-left: 0;
  }

  .timeline-node::after { display: none; }
  .timeline-node:last-child { padding-bottom: 0; }
  .timeline-node--right { align-items: flex-start; text-align: left; }

  /* Полоса в шапке резюме на телефоне снимается целиком — решение
     заказчика 09.09.2026. Восемь узлов в столбик это лишний экран
     до первого абзаца, а те же годы стоят ниже в самом резюме. */
  .timeline--resume { display: none; }

  /* Дизайн-системы: на телефоне всё в один столбец. Две текстовые
     колонки по 170px — это шесть-семь слов в строке, а два кадра
     в ряд — по 170px на кадр, где не разобрать ни состояния,
     ни отступы на спеке. */
  .about-ds-text { columns: 1; }
  .about-ds-first { text-indent: 0; }
  .about-ds-figures {
    grid-template-columns: 1fr;
    row-gap: var(--_spacing---spacing-30);
  }

  .about-ds-lead { font-size: var(--_typography---font-size--heading-h3); }

  /* Вторая строка узла на тачскрине не раскрывается наведением,
     поэтому стоит открытой сразу. */
  .timeline-note { opacity: 1; }

  /* Таблица метода разворачивается в две группы с подзаголовками,
     а не уезжает в горизонтальный скролл: скролл внутри таблицы
     на телефоне не находят. */
  .about-table,
  .about-table tbody,
  .about-table tr,
  .about-table th,
  .about-table td { display: block; width: auto; }

  .about-table thead { display: none; }

  .about-table tbody tr {
    padding: var(--_spacing---spacing-20) 0;
    border-bottom: var(--about-hairline);
  }

  .about-table tbody th {
    width: auto;
    padding: 0 0 var(--_spacing---spacing-10);
    border-bottom: 0;
  }

  .about-table tbody td {
    width: auto;
    padding: 0 0 var(--_spacing---spacing-5);
    border-bottom: 0;
  }

  /* Подзаголовок группы приезжает из CSS, а не из разметки:
     в таблице он уже есть — это заголовок столбца. */
  .about-table tbody td::before {
    display: block;
    color: var(--colors--granite-gray);
    font-size: var(--_typography---font-size--small);
  }

  .about-table tbody td:first-of-type::before { content: "Consumer product"; }
  .about-table tbody td:last-of-type::before { content: "Internal platform"; }
}


/* ============================================================
   10. Контактный блок в подвале

   Вектор — DOCS/contact-page-simple.md, решение о переезде —
   заказчик, 09.09.2026.

   Блок стоял отдельной секцией на четырёх страницах кейсов,
   а страница `/contact` показывала то же самое третий раз.
   Теперь он один, он часть подвала и стоит на каждой странице,
   где подвал есть. Страница снята.

   Раскладка — `case-block` со страниц кейсов: ярлык слева,
   содержимое в колонках 4–10. Своей не заводим: расстояние
   до подвала и ширина колонки заказчику подходили как есть,
   а вторая раскладка для того же смысла — это два сайта.

   Почта и LinkedIn набраны одним кеглем. Ровно на этой
   одинаковости блок и держится: любая разница между ними
   читается как подсказка «сюда лучше», а это два равных
   способа связи.

   Разметка — `contactBlock()` в tools/lib/redesign.mjs.
   ============================================================ */

.footer-contact {
  /* Мера текстовой колонки — та же, что на About: 57ch даёт
     69–70 знаков в строке. Значение замерено, не выведено;
     подробности в разделе 9.

     ⚠️ Объявлять её надо здесь, на самом блоке. Пока блок был
     страницей, переменная жила на `.contact-page`; после переезда
     этого класса на странице не стало, `var()` перестала
     разрешаться, и строка под каналами молча растянулась
     на всю колонку — 816px вместо 500. */
  --contact-measure: 57ch;
}

/* Замечание заказчика от 13.09.2026: линии под почтой и LinkedIn
   были толще системных и обрывались на десятой колонке. Толщина
   стала 1px — как у всех линий сайта, — а колонка содержимого
   идёт до правого борта контейнера, туда же, где кончаются линии
   подвала ниже (1425px на 1440, замерено).

   ⚠️ Расширена колонка, а не линия. Отрицательное поле под линией
   пришлось бы пересчитывать на каждом брейкпоинте; колонка же
   на планшете и так до 13, а на резюме `5 / 13` — правило ниже
   с ними не спорит. Мера строки под каналами своя, `57ch`,
   и от ширины колонки не зависит. */
.footer-contact .case-body { grid-column-end: 13; }

/* --- Два канала ---------------------------------------------

   Адрес и слово LinkedIn — кегль заголовка страницы.

   ⚠️ `min()` здесь не перестраховка, а обязательное условие.
   Токен heading-h1 на мобильном переключается с пикселей на
   9.767vw, и двадцать знаков `bronov1995@gmail.com` в такой
   кегль не влезают ни на одном телефоне: адрес переносится
   посреди слова или уезжает за край. 8vw — верхняя граница,
   при которой самая длинная из двух строк помещается на 320px:
   замерено, у адреса 11.19em на двадцать знаков.

   На десктопе `min()` возвращает сам токен: 8vw при ширине
   1440 — это 115px, то есть заведомо больше 47px. Медиазапроса
   не нужно, и порог не разъедется с токеном. */

/* Решение заказчика от 10.09.2026: линия под адресом стоит всегда,
   а не выезжает при наведении, и идёт до самого правого края блока —
   как у всех строк сайта.

   ⚠️ Отсюда `block` вместо `inline-block`. Пока элемент был строчным,
   линия кончалась там же, где текст, и у почты с LinkedIn выходили
   линии разной длины — при том что оба это одна и та же строка списка.

   Наведение осталось за собой: раз линия теперь постоянная,
   отзывается текст — тем же серым, что у ссылок в меню глав. */
.contact-address {
  display: block;
  max-width: 100%;
  padding-bottom: var(--_spacing---spacing-10);
  font-size: min(var(--_typography---font-size--heading-h1), 8vw);
  line-height: 1.05;
  letter-spacing: -.03em;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--colors--matt-black);
  transition: color 200ms ease-in-out;
}

.contact-address:hover { color: var(--colors--granite-gray); }

/* Подпись под словом LinkedIn. Мелкая и серая — это не второй
   способ связи, а уточнение, какой именно профиль. */
.contact-handle {
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
}

/* --- Кнопки -------------------------------------------------

   Класс `.about-btn` из раздела 2 — единственная кнопка на сайте,
   и здесь она же. Второй, «контактной», не заводим: она бы
   отличалась от общей ровно на те мелочи, которые потом
   расходятся.

   Отступ сверху снят: расстояние держат отбивки в разметке,
   как везде.

   ⚠️ Поля здесь больше не задаются. Раньше 48px в высоту были только
   у этих кнопок, и от них же кнопка на главной отличалась вдвое.
   С 09.09.2026 размер стоит в самом `.about-btn` (раздел 2) и одинаков
   везде. */

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--_spacing---spacing-10);
}

.contact-actions .about-btn {
  margin-top: 0;
}

/* Кнопка-элемент, а не ссылка: у `<button>` своя рамка, свой
   курсор и своё выравнивание текста, и всё это надо снять руками.

   ⚠️ Селектор привязан к блоку, а не к странице. Пока блок был
   страницей, здесь стоял `.contact-page button.about-btn`, и после
   переезда в подвал правило перестало находить кнопку вовсе. */
.footer-contact button.about-btn {
  border: 0;
  font-family: var(--_typography---font-family);
  font-size: var(--_typography---font-size--body);
  font-weight: 500;
  line-height: var(--_typography---font-height--body-height);
  cursor: pointer;
  appearance: none;
}

/* Надпись меняется на `Copied` и обратно. Ширину держим по самой
   длинной из двух: без этого кнопка на две секунды сжимается
   и уводит за собой соседнюю. */
.contact-copy span {
  display: inline-block;
  min-width: 8.5em;
  text-align: center;
}

/* --- Строка под каналами ------------------------------------

   Мелко и спокойно: ни рамки, ни заливки, ни линии сверху.
   Линия сделала бы из этого абзаца третий канал. */

.contact-note {
  max-width: var(--contact-measure);
  color: var(--colors--granite-gray);
}

.contact-note p + p { margin-top: var(--_spacing---spacing-20); }

/* Начертание читаемого текста — то же, что у абзацев на остальных
   собранных страницах: вес 400 и нулевой трекинг (раздел 1). Под
   общее правило эти абзацы не попадают: условием там стоит класс
   `.text-muted`, а он на сайте работает и как метка, и как описание.
   Цвет остаётся серым — это единственный текст блока, который
   не является ни ссылкой, ни ярлыком. */
.footer-contact .contact-note p {
  font-weight: 400;
  letter-spacing: normal;
}

/* --- адаптив ------------------------------------------------ */

@media screen and (max-width: 479px) {
  /* Кнопки в столбик: две рядом на 320px дают по 140px каждая,
     и надпись `Copy address` переносится на две строки. */
  .contact-actions { flex-direction: column; align-items: flex-start; }
  .contact-actions .about-btn { width: 100%; justify-content: center; }
  .contact-copy span { min-width: 0; }
}

/* ============================================================
   КАРТОЧКИ: движение при прокрутке и наезд при наведении

   Правило заказчика от 09.09.2026: у карточки на сайте всегда
   два движения. Едет кадр внутри коробки, пока страница
   прокручивается, и наезжает под курсором, когда на него навели.

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

   Разметку ставит `cardMedia` в tools/lib/redesign.mjs: коробка
   с пропорцией и внутри неё подвижный слой.

   ⚠️ Классы свои, а не `.case-parallax`. Разметка та же, но по
   `.case-parallax-inner` ищет свои кадры увеличение под курсором
   на страницах кейсов, и общий класс молча раздал бы карточкам
   внизу страницы ещё и его. Правило движения при этом одно
   на оба — оно и объявлено сразу для двух классов.

   ⚠️ Цена движения — кадрирование. Внутренний слой выше коробки
   на 16% и ездит в её пределах, поэтому у каждой карточки
   постоянно срезано около 8% сверху и снизу. На обложках это
   поля, терять там нечего.
   ============================================================ */

/* ⚠️ `clip`, а не `hidden`, и это не вкусовщина. `overflow: hidden`
   делает элемент контейнером прокрутки, а прокрутка-таймлайн ищет
   ближайший такой контейнер и меряет положение кадра относительно
   него. Контейнер при этом никуда не прокручивается — движение молча
   замирает на одном значении. Ровно это и случилось на первой сборке:
   таймлайн стоял на 54% и не двигался. `clip` режет так же, но
   контейнером прокрутки не становится. */
.card-media {
  position: relative;
  width: 100%;
  overflow: clip;
  border-radius: var(--sizing--border-radius);
}


/* Ход кадра внутри окна задаётся одной переменной. Слой ровно
   настолько выше окна, насколько ему ехать в обе стороны, — иначе
   у краёв показалась бы пустота.

   ⚠️ `--drift` наследуется от карточки, и в этом весь смысл: одна
   и та же коробка едет у одних быстрее, у других медленнее, а кому
   поставили ноль — стоит. Значение по умолчанию 7% — то же, с каким
   карточки жили до этого. */
.card-media-inner {
  position: absolute;
  top: calc(-1 * var(--drift, 7%));
  left: 0;
  right: 0;
  height: calc(100% + 2 * var(--drift, 7%));
}

/* Кадр занимает подвижный слой целиком. Скругление снимается:
   его держит коробка, а картинка внутри неё выше её же краёв —
   свои скруглённые углы уехали бы за обрез и просто пропали. */
.card-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  /* 500ms по outExpo — то же время и та же кривая, что у панелей
     в списке работ и у всех подчёркиваний сайта. */
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

/* Наезд — от ссылки, внутри которой лежит карточка: наводят
   на карточку целиком, а не на картинку в ней. */
a:hover .card-media img { transform: scale(1.03); }

/* Движение при прокрутке. Тот же приём и тот же диапазон, что
   у обложек кейсов, — прокрутка-таймлайн в CSS, ни строчки JS.
   Где браузер таймлайн не поддерживает, кадр просто стоит.

   Ловушка та же: overflow:hidden делает коробку самостоятельным
   контейнером прокрутки, и безымянный view() цепляется к ней,
   а она никуда не прокручивается. Поэтому таймлайн объявляется
   на коробке и берётся по имени. */
@supports (animation-timeline: view()) {
  .card-media { view-timeline-name: --card-media; }

  .card-media-inner {
    animation: card-drift linear both;
    animation-timeline: --card-media;
    animation-range: entry 0% exit 100%;
  }

  /* Кадры кейсов и карточки едут по разным наборам кадров нарочно:
     у первых ход зашит числом и общий на все шапки, у вторых он
     переменная. Один набор на двоих связал бы их намертво. */
  @keyframes card-drift {
    from { transform: translate3d(0, calc(-1 * var(--drift, 7%)), 0); }
    to   { transform: translate3d(0, var(--drift, 7%), 0); }
  }
}

@media (prefers-reduced-motion: reduce) {
  .card-media-inner { animation: none; }
  .card-media img { transition: none; }
  a:hover .card-media img { transform: none; }
}

/* ============================================================
   СТРОКА «SEE ALL» ПОД ЛЕНТОЙ

   Решение заказчика от 09.09.2026. Путь дальше у ленты
   «Playground & Inspiration» был обозначен строчкой `Read all`
   в правом верхнем углу шапки: мелкой, кеглем текста, и до самой
   ленты, а не после неё. Заказчик: «он незаметен и непонятно,
   что нажимать и куда ведёт».

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

   ⚠️ Ничего нового: это `.case-index-row` со страниц кейсов,
   с которой сняты колонки. Та же волосяная линия, та же заливка
   `cloud-white`, те же 500ms по outExpo, тот же отступ текста
   от края заливки. Заказчик назвал её же — «как у оглавлений
   на проектах». Значит и меняться они обязаны вместе.

   ⚠️ Подложка светлая, а не чёрная. Причина записана у
   `.case-index-row`: реакция здесь подсказка, а не событие.
   Возвращать чёрное нельзя.

   Разметку ставит `seeAllRow` в tools/lib/redesign.mjs.
   ============================================================ */

.see-all {
  position: relative;
  display: flex;
  justify-content: flex-end;
  align-items: baseline;
  gap: var(--_spacing---spacing-10);
  overflow: hidden;
  /* Отступ справа тот же, что у стрелки в строке оглавления: без него
     надпись прилипала бы к краю выехавшей плашки. */
  padding: var(--_spacing---spacing-15) var(--_spacing---spacing-15) var(--_spacing---spacing-15) 0;
  border-top: 1px solid var(--colors--matt-black);
  border-bottom: 1px solid var(--colors--matt-black);
  color: var(--colors--matt-black);
  text-decoration: none;
}

.see-all:before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--colors--cloud-white);
  transform: translate3d(-101%, 0, 0);
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.see-all:hover:before { transform: translate3d(0, 0, 0); }

/* Надпись и стрелка лежат поверх заливки — отсюда `position: relative`,
   как у ячеек строки индекса. Прижаты вправо самой строкой, поэтому
   своего выравнивания им не нужно. */
.see-all-text,
.see-all-arrow {
  position: relative;
  font-size: var(--_typography---font-size--heading-h3);
  line-height: var(--_typography---font-height--heading-height);
  letter-spacing: var(--_typography---letter-spacing--heading-h3);
}

/* Стрелка сдвигается на те же 6px и с той же кривой, что в строках
   оглавления кейсов: на сайте это уже язык «ведёт дальше». */
.see-all-arrow {
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

.see-all:hover .see-all-arrow { transform: translate3d(6px, 0, 0); }

@media (prefers-reduced-motion: reduce) {
  .see-all:before { transition: none; }
  .see-all-arrow { transition: none; }
  .see-all:hover .see-all-arrow { transform: none; }
}

/* ============================================================
   РАЗДЕЛ РАБОТ: ТРИ ИМЕНОВАННЫЕ ГРУППЫ

   Решение заказчика от 09.09.2026. `/projects` перестал быть
   списком панелей во весь экран: работы разбиты на группы —
   Fintech Cases, Wellness & Medical Applications, Personal
   Projects, — и у каждой свой заголовок со счётчиком.

   ⚠️ Раскладка не новая. Это лента «Playground & Inspiration»,
   которую заказчик и назвал образцом: заголовок, ряд обложек,
   подпись под каждой. Отличие одно и намеренное — ряд здесь
   сетка, а не flex-wrap: колонки обязаны быть равными, а высота
   карточек — разной.

   ⚠️ Наезда и движения при прокрутке здесь своих нет. Оба
   приходят коробкой `.card-media`, общей на все карточки сайта
   (раздел «карточки» выше). Своё правило разошлось бы с ними
   на первой же правке.

   Разметку ставит `rebuildWorkGroups` в tools/lib/redesign.mjs,
   состав групп — `PROJECT_GROUPS` в tools/build-local.mjs.
   ============================================================ */

/* Заголовок группы и счётчик — в одной строке по общей базовой
   линии: число читается как часть заголовка, а не как подпись
   под ним. Кегль у числа свой, мелкий: оно уточняет, а не
   объявляет.

   ⚠️ Число стоит вплотную к заголовку, а не у правого борта.
   Заказчик назвал группу целиком — «Fintech Cases (2)», — то есть
   счёт входит в имя. Прижатое к другому краю строки, оно читается
   как отдельная подпись неизвестно к чему. */
.proj-group-head {
  display: flex;
  align-items: baseline;
  justify-content: flex-start;
  gap: var(--_spacing---spacing-10);
}

.proj-group-title { margin: 0; }

.proj-group-count { font-size: var(--_typography---font-size--body); }

/* Серое продолжение заголовка группы и ленты Playground. Решение
   заказчика от 13.09.2026: цвет тот же, что у номеров и названий
   глав на страницах кейсов, — это пояснение на полях заголовка,
   а не второй заголовок. Кегль и начертание наследуются. */
.proj-group-head { flex-wrap: wrap; }
.proj-group-note { margin: 0; color: var(--colors--gray); }

.heading-note { color: var(--colors--gray); }
.heading-note-comma { color: var(--colors--matt-black); }

/* Центральная колонка блока услуг на главной: названия направлений
   жирным. Решение заказчика от 13.09.2026; подчёркивание в тот же
   день снято его же решением. Колонка опознаётся по компоненту
   Approach Block (его `data-w-id`) и месту в ряду — номер, название,
   описание, — отдельного класса у названия в разметке Webflow нет. */
[data-w-id="9dd068bd-3f5d-33ef-ba9a-41460c91a3e3"] > .grid-global > .section-title + .section-title {
  font-weight: 600;
}

/* ============================================================
   ОТЗЫВЫ НА ГЛАВНОЙ

   Решение заказчика от 13.09.2026: два настоящих отзыва, оба
   раскрыты. Разметку ставит `rebuildFeedbacks`.
   ============================================================ */

/* Первый отзыв: аватар вдвое крупнее — 100 против 50, пропорция
   та же, круг. Имя, подпись и ссылка стоят по середине высоты
   аватара — решение заказчика от 13.09.2026. */
.person-portrait--lead {
  width: calc(var(--_spacing---spacing-50) * 2);
  height: calc(var(--_spacing---spacing-50) * 2);
  flex: none;
}

.person-container--lead { align-items: center; }

.person-role { max-width: 60ch; }

/* Ссылка на профиль: подчёркнута всегда, стрелка ↗ говорит «уводит
   с сайта». При наведении серым становится всё сразу — текст,
   линия и стрелка: линия берёт цвет текста, стрелка — тоже. */
.person-link {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  margin-top: var(--_spacing---spacing-10);
  color: var(--colors--matt-black);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  transition: color 200ms ease-in-out;
}

.person-link:hover { color: var(--colors--granite-gray); }

.person-link-arrow { text-decoration: none; display: inline-block; }

/* Ряд карточек. Колонки равные, а высота — от пропорции каждой
   карточки, поэтому `align-items: start`: иначе сетка растянула
   бы низкую карточку до высоты соседней и «на 30% ниже»
   перестало бы существовать. */
.proj-row {
  display: grid;
  column-gap: var(--_spacing---spacing-15);
  row-gap: var(--_spacing---spacing-25);
  align-items: start;
}

.proj-row--duet { grid-template-columns: 1fr 1fr; }
.proj-row--trio { grid-template-columns: repeat(3, 1fr); }

/* Ведущая работа во всю ширину, под ней три соседа. Одна сетка,
   а не две: так у ведущей карточки и у ряда под ней общий левый
   и правый край, без подгонки. */
.proj-row--lead-trio { grid-template-columns: repeat(3, 1fr); }
.proj-row--lead-trio > .proj-item--wide { grid-column: 1 / -1; }

/* Личные проекты с идеей слева. Решение заказчика от 13.09.2026:
   Quify шириной ровно в две карточки нижнего ряда и стоит над HEROX
   и Obsify, а слева от него — текст «ECOSYSTEM concept» высотой
   в сам кадр Quify, не считая подписи под ним.

   Равенство высот держит подсетка, а не формула. Первая версия
   выводила высоту текста из ширины ряда — и работала, пока текст
   был короче кадра. Со списком приложений и плашкой примера он
   стал длиннее, и под Quify до нижнего ряда повисала дыра.

   Как теперь: у ряда три строки — кадр, подпись, нижний ряд.
   Карточка Quify занимает первые две и сама становится подсеткой,
   поэтому кадр лежит ровно в первой строке, а подпись во второй.
   Текст идеи стоит в первой строке и тянется на её высоту.
   Высота строки — большее из двух: кадр по пропорции или текст.
   Короче текст — он растягивается до кадра; длиннее — кадр
   дорастает до текста и режется по `object-fit: cover`.

   ⚠️ Нижний ряд прибит к третьей строке явно. Иначе Suppfy
   автоматически сел бы во вторую строку под текст, в ячейку
   подписи Quify.

   ⚠️ Междурядье ряда обнулено: между кадром и подписью Quify
   зазора быть не должно, у подписи свой отступ сверху. Отбивку
   перед нижним рядом держит поле самих карточек. */
.proj-row--concept {
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto auto auto;
  row-gap: 0;
}

.proj-row--concept > .proj-item--wide {
  grid-column: 2 / -1;
  grid-row: 1 / span 2;
  display: grid;
  grid-template-rows: subgrid;
  /* ⚠️ Колонка задана явно. Без неё ширина колонки берётся
     по содержимому, а кадр с высотой 100% и пропорцией 16/9 выводит
     ширину из высоты — вышло 937px в карточке шириной 935,
     кадр на 2px залезал за борт. */
  grid-template-columns: minmax(0, 1fr);
}

/* Кадр берёт высоту строки. Пропорция остаётся нижней границей:
   пока высота строки не определена, вклад кадра в неё считается
   по `aspect-ratio`. */
.proj-row--concept > .proj-item--wide > .card-media { width: 100%; height: 100%; }

.proj-row--concept > .proj-item:not(.proj-item--wide) {
  grid-row: 3;
  margin-top: var(--_spacing---spacing-25);
}

/* ⚠️ Пятая карточка (Healsify, 3.1.11 инструкции правок) — в четвёртой
   строке. Без явной строки автоматическая раскладка посадила бы её
   в первую свободную ячейку — под текст идеи, во вторую строку. */
.proj-row--concept > .proj-item:not(.proj-item--wide):nth-child(n+6) { grid-row: 4; }

.proj-concept {
  grid-column: 1;
  grid-row: 1;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--_spacing---spacing-25);
  padding-right: var(--_spacing---spacing-25);
}

.proj-concept-title { margin: 0; }

.proj-concept p {
  margin: 0;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--colors--matt-black);
}

.proj-concept strong { font-weight: 600; }

.proj-concept .proj-concept-label {
  margin-bottom: var(--_spacing---spacing-10);
  color: var(--colors--granite-gray);
}

/* Список приложений системы: номер, имя, статус. Волосяные линии
   между строками — тот же язык, что у строк оглавления и подвала. */
.proj-concept-apps {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--colors--matt-black);
}

.proj-concept-apps > li { border-bottom: 1px solid var(--colors--cloud-white); }

.proj-concept-app {
  display: flex;
  align-items: center;
  gap: var(--_spacing---spacing-10);
  padding: 6px 0;
}

.proj-concept-num {
  min-width: 2ch;
  color: var(--colors--granite-gray);
  font-size: var(--_typography---font-size--small);
  font-variant-numeric: tabular-nums;
}

.proj-concept-name { font-weight: 500; }

/* Чип в строке списка: отступ сверху, нужный под подписью
   карточки, здесь лишний, а прижат он к правому борту. */
.proj-concept-app .proj-status { margin: 0 0 0 auto; }

/* Пример — плашкой, чтобы читался отдельной сценой, а не третьим
   абзацем. Заливка `cloud-white` — та же, что выезжает у строк
   «See all» и оглавлений: новой краски не заводим. */
.proj-concept-example {
  padding: var(--_spacing---spacing-20);
  border-radius: var(--sizing--border-radius);
  background-color: var(--colors--cloud-white);
}

/* Реплика ассистента — крупнее и в кавычках: это кульминация
   примера, ради неё блок и стоит. */
.proj-concept .proj-concept-quote {
  font-size: var(--_typography---font-size--heading-h5);
  line-height: 1.25;
  font-weight: 500;
  letter-spacing: -.01em;
}

.proj-item {
  display: block;
  color: inherit;
  text-decoration: none;
}

/* Пропорция карточки приходит инлайновой переменной, а не готовым
   `aspect-ratio`: это раскладка, а не кадр, и на узком экране она
   обязана меняться.

   ⚠️ Переменных две, и это не лишнее. Инлайновый `--ratio` стоит
   на самой карточке, и правило из стиля его не перебьёт — инлайн
   старше. Поэтому узкий экран объявляет `--ratio-narrow`, а базовое
   значение остаётся запасным. */
.proj-item > .card-media,
.proj-item > .ph { aspect-ratio: var(--ratio-narrow, var(--ratio)); }

.proj-caption {
  display: flex;
  flex-direction: column;
  gap: var(--_spacing---spacing-5);
  padding-top: var(--_spacing---spacing-10);
}

/* Подчёркивание имени при наведении — тот же язык, что у строк
   оглавления и подвала: наводят на карточку, отвечает имя. */
.proj-name {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: transparent;
  transition: text-decoration-color 200ms ease;
}

.proj-item:hover .proj-name { text-decoration-color: var(--colors--gray); }

/* Заглушка в ряду. Пропорция приходит инлайном, как у визуалов
   кейсов; скругление — как у соседних карточек, иначе серый
   прямоугольник выглядит чужим в ряду. */
.proj-ph {
  height: auto;
  padding: var(--_spacing---spacing-15);
  border-radius: var(--sizing--border-radius);
}

/* Видео на обложке живёт по тем же правилам, что картинка:
   у панелей в списке работ обложка бывает лупом. */
.card-media video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  transition: transform 500ms cubic-bezier(.19, 1, .22, 1);
}

a:hover .card-media video { transform: scale(1.03); }

@media (prefers-reduced-motion: reduce) {
  .card-media video { transition: none; }
  a:hover .card-media video { transform: none; }
}

/* --- адаптив ------------------------------------------------ */

/* На планшете тройка остаётся тройкой — как `.listitem-2` в ленте
   Playground, у которой на этой ширине те же три колонки. */
@media screen and (max-width: 767px) {
  .proj-row--duet,
  .proj-row--trio,
  .proj-row--lead-trio,
  .proj-row--concept { grid-template-columns: 1fr; }

  /* В одну колонку текст идеи встаёт над Quify и высоты кадра
     не держит: равнять не с чем. Подсетка снимается, строки снова
     автоматические, междурядье возвращается. */
  .proj-row--concept { grid-template-rows: none; row-gap: var(--_spacing---spacing-25); }
  .proj-row--concept > .proj-item--wide { grid-column: auto; grid-row: auto; display: block; --ratio-narrow: 16/9; }
  .proj-row--concept > .proj-item--wide > .card-media { width: auto; height: auto; }
  .proj-row--concept > .proj-item:not(.proj-item--wide),
  .proj-row--concept > .proj-item:not(.proj-item--wide):nth-child(n+6) { grid-row: auto; margin-top: 0; }
  .proj-concept { grid-row: auto; padding-right: 0; }

  /* Ведущая работа задумана широкой полосой на всю ширину раздела.
     В одну колонку 16/5 превращается в полоску в 117px высотой —
     кадр в ней не читается вовсе. На узком экране полоса становится
     обычной карточкой. */
  .proj-row--lead-trio > .proj-item--wide { --ratio-narrow: 16/9; }
}

/* ============================================================
   НАВИГАЦИЯ ПО РЕЗЮМЕ

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

   Три части, и они разные по назначению:

     меню глав справа   — где я сейчас и куда можно уйти;
     две стрелки        — быстрый ход в оба конца;
     кнопка PDF         — то, ради чего рекрутер сюда пришёл.

   Разметку меню и кнопки ставит сборщик (`resumeIndex`, `resumeDock`),
   стрелки дорисовывает `content/resume-nav.js`.
   ============================================================ */

/* Коробка, внутри которой меню и живёт. Она обнимает сетку резюме
   целиком, поэтому её верх — уровень первой главы, а низ — конец
   списка. Оба края заказчик назвал сам, и оба берутся отсюда,
   а не из чисел. */
.resume-index-wrap { position: relative; }

/* Ширина полосы под меню. Объявлена один раз и на корне: её берут
   и меню внутри сетки, и кнопка со стрелками, которые висят
   на фиксированной позиции и до сетки не дотягиваются. Все трое
   обязаны стоять по одной левой линии — требование заказчика
   от 10.09.2026. */
:root { --resume-index-w: 200px; }

/* Прыжок по якорю не должен упирать название главы в край окна:
   шапка сайта липкая и накрыла бы его. Отступ тот же, что у якорей
   решений на страницах кейсов, — там это уже решено. */
.resume-rule[id] { scroll-margin-top: var(--_spacing---spacing-60); }

/* ⚠️ Меню лежит абсолютом и колонок сетки не занимает: полоса справа
   и так пустая, обложка кейса занимает половину своей колонки.

   ⚠️ Ширина именно числом, а не долей сетки. Свободная полоса —
   это половина колонки обложки, и она сжимается быстрее самой сетки:
   текстовая колонка держит свои 52ch на любой ширине, а остаток
   тает. Первая версия брала две колонки сетки, и на 1280 обложка
   заезжала под меню на 26px — замерено. Поэтому полоса объявлена
   числом, а обложке ниже запрещено в неё входить. */
.resume-index {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: var(--resume-index-w);
  pointer-events: none;
}

/* ⚠️ Меню начинается не от верха сетки, а от строки первой главы.
   Верх сетки — это волосяная линия над `Profile`, и меню, начатое
   от неё, висит выше подписи слева на всю её отбивку. Замечание
   заказчика от 10.09.2026: «навигация должна висеть на той же линии,
   что и Profile».

   Отступ собран из тех же токенов, из которых складывается эта
   отбивка, а не записан числом: линия 20, её нижнее поле 30,
   междурядье сетки 60. Замерено — ровно 110px, и по брейкпоинтам
   поедет вместе со шкалой. */
.resume-index { padding-top: calc(var(--_spacing---spacing-20) + var(--_spacing---spacing-30) + var(--_spacing---spacing-60)); }

/* ⚠️ Липким сделан слой внутри, а не сама коробка: у абсолютного
   элемента нет потока, внутри которого он мог бы прилипать.

   Отступ сверху тот же, что у подписи главы слева, — меню и подпись
   встают на одну линию, и это не совпадение, а условие: заказчик
   просил «на том же уровне, что и первый тайтл Profile». */
.resume-index-inner {
  position: sticky;
  top: var(--_spacing---spacing-90);
  pointer-events: auto;
}

.resume-index-list {
  display: flex;
  flex-direction: column;
  gap: var(--_spacing---spacing-10);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Реакция на наведение — только цвет, как у обычной ссылки. Ни наезда,
   ни подчёркивания: заказчик просил именно так, и это правильно —
   меню стоит рядом с текстом, а не поверх картинки, и любое движение
   здесь спорило бы с чтением. */
.resume-index-link {
  display: block;
  color: var(--colors--matt-black);
  font-size: var(--_typography---font-size--small);
  line-height: var(--_typography---font-height--body-height);
  text-decoration: none;
}

.resume-index-link:hover { color: var(--colors--gray); }

/* Текущая глава набрана чуть крупнее — решение заказчика
   от 10.09.2026: по правому списку должно быть видно, где мы.
   Растёт и уменьшается плавно, тем же временем, что и цвет:
   резкий скачок кегля читался бы как перерисовка, а не как отметка.

   ⚠️ Кегль, а не начертание и не цвет. Начертание сместило бы
   всю строку по ширине, цвет уже занят наведением — а его на этой
   же строке никто не отменял. */
.resume-index-link {
  transition:
    color 200ms ease-in-out,
    font-size 300ms cubic-bezier(.25, 1, .5, 1);
}

.resume-index-link.is-current {
  font-size: calc(var(--_typography---font-size--small) * 1.35);
}

/* Кнопка в правом нижнем углу — единственное, что там висит.
   Правый край — по правому краю сетки, то есть по общему полю
   страницы: тогда её левая граница совпадает с левой границей меню,
   потому что ширина у них одна. Требование заказчика от 10.09.2026 —
   «по одной линии».

   ⚠️ Стрелки быстрой прокрутки стояли здесь же и сняты решением
   заказчика в тот же день: на странице с оглавлением справа они
   ничего не добавляли. */
.resume-dock {
  position: fixed;
  right: var(--sizing--padding-global);
  bottom: 5%;
  z-index: 60;

  display: grid;
  justify-items: end;
}

/* Кнопка чёрная и единственная такая на странице: это главное
   действие рекрутера. Высота та же, что у кнопок сайта, — 48px
   при полях 15/25, как объявлено у `.about-btn`. */
/* Ширина та же, что у меню: по ней и совпадают левые границы. */
.resume-pdf {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--resume-index-w);
  padding: var(--_spacing---spacing-15) var(--_spacing---spacing-25);
  border-radius: var(--sizing--border-radius);
  background-color: var(--colors--matt-black);
  color: var(--colors--white);
  font-size: var(--_typography---font-size--body);
  /* Высота та же, что у всех кнопок сайта, — 48px. Она собирается
     из полей 15/25 и межстрочного расстояния текста, как объявлено
     у `.about-btn`; собственная `line-height: 1` дала бы 46
     и кнопка выбилась бы из ряда. */
  line-height: var(--_typography---font-height--body-height);
  text-decoration: none;
  white-space: nowrap;
  box-shadow: 0 2px 10px color-mix(in srgb, var(--colors--matt-black) 18%, transparent);
  transition: background-color 200ms ease-in-out;
}

.resume-pdf:hover { background-color: var(--colors--granite-gray); }

/* Полоса под меню отвоёвывается у обложки, а не у текста: текст
   держит свои 52ch везде, и трогать его нельзя.

   ⚠️ Верхняя граница обложки остаётся прежней — половина колонки.
   Это решение записано выше у `.resume-cover`: во всю ширину обложка
   весит больше текста рядом. `min` из двух правил и сохраняет старое
   поведение на широком экране, и не пускает обложку в полосу меню
   на узком. Замерено: на 1920 и 1440 обложка не изменилась вовсе,
   на 1280 стала меньше на 39px. */
@media screen and (min-width: 1280px) {
  .resume-cover {
    width: min(50%, calc(100% - var(--resume-index-w, 200px) - var(--_spacing---spacing-15)));
  }
}

/* Подвал на резюме встаёт по сетке самой страницы. Замечание
   заказчика от 10.09.2026: «разная сетка рубит глаз».

   Разница была настоящая и заметная: содержимое подвала начиналось
   с четвёртой колонки (371px на 1440), а колонка с описаниями глав —
   с пятой (490px). Два блока сдвинуты на одну колонку, ярлык
   и колонка Social расширены до четырёх — и левый край у всех четырёх
   блоков стал общим.

   ⚠️ Только на этой странице. На кейсах и на главной подвал стоит
   по своей сетке, и трогать её незачем: там нет колонки, к которой
   его надо равнять. Отсюда и класс на самом подвале, а не правка
   общего правила. */
.footer--resume .footer-contact .case-label,
.footer--resume .footer-social { grid-area: 1 / 1 / 2 / 5; }

.footer--resume .footer-contact .case-body,
.footer--resume .footer-menu { grid-area: 1 / 5 / 2 / 13; }

/* ⚠️ На узком экране остаётся только кнопка. Решение заказчика
   от 10.09.2026: «на мобилке кнопка скачать будет, но ни навигации,
   ни других кнопок не будет».

   Порог 1280, а не 991: между ними свободная полоса справа уже
   самого меню, и оно встало бы поверх обложек. */
@media screen and (max-width: 1279px) {
  .resume-index { display: none; }
}

/* ============================================================
   ПЛАВАЮЩЕЕ МЕНЮ РЕШЕНИЙ НА СТРАНИЦЕ КЕЙСА

   Решение заказчика от 10.09.2026: «то же меню справа сверху,
   только с одной разницей — появляется при прокрутке вверх
   и только между первой и последней секцией».

   Всё, кроме появления, общее с меню резюме: кегль, цвет,
   реакция на наведение, отметка текущей секции. Разметку ставит
   `caseNav`, показ держит `content/case-nav.js`.
   ============================================================ */

/* ⚠️ Подложка, в отличие от резюме, обязательна. Там меню стоит
   в пустой полосе справа; здесь под ним кадры во всю ширину,
   и без подложки строки читались бы как подпись к картинке. */
.case-nav {
  position: fixed;
  top: var(--_spacing---spacing-90);
  right: var(--sizing--padding-global);
  z-index: 60;

  width: 260px;
  max-height: calc(100vh - var(--_spacing---spacing-90) * 2);
  overflow-y: auto;
  padding: var(--_spacing---spacing-20);

  background-color: var(--colors--white);
  border-radius: var(--sizing--border-radius);
  box-shadow: 0 2px 16px color-mix(in srgb, var(--colors--matt-black) 12%, transparent);

  opacity: 0;
  transform: translateY(-12px);
  pointer-events: none;

  transition:
    opacity 260ms ease-in-out,
    transform 420ms cubic-bezier(.25, 1, .5, 1);
}

.case-nav.is-shown {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

.case-nav-list {
  display: flex;
  flex-direction: column;
  gap: var(--_spacing---spacing-10);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Строка та же, что в меню резюме, плюс номер слева. Номер серый
   и не растёт вместе с текущей строкой: он адрес, а не название. */
.case-nav-link {
  display: grid;
  grid-template-columns: 2.4em 1fr;
  gap: var(--_spacing---spacing-5);
  color: var(--colors--matt-black);
  font-size: var(--_typography---font-size--small);
  line-height: var(--_typography---font-height--body-height);
  text-decoration: none;
  transition:
    color 200ms ease-in-out,
    font-size 300ms cubic-bezier(.25, 1, .5, 1);
}

.case-nav-link:hover { color: var(--colors--gray); }

.case-nav-num { color: var(--colors--gray); }

.case-nav-link.is-current {
  font-size: calc(var(--_typography---font-size--small) * 1.35);
}

/* ⚠️ На узком экране меню не показывается вовсе — как и на резюме.
   Оно накрывает содержимое подложкой, и на телефоне это треть экрана
   поверх текста. Порог общий с резюме. */
@media screen and (max-width: 1279px) {
  .case-nav { display: none; }
}

/* --- чип статуса на карточке проекта -------------------------

   Решение заказчика от 10.09.2026: на три из четырёх личных
   продуктов перейти нельзя — они в разработке. Карточка обязана
   сказать это словом, а не только тем, что не нажимается.

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

   ⚠️ Цвет не новый. Зелёный и синий — те же `--case-tag--chosen`
   и `--case-tag--requested`, что у меток решений; третьей палитры
   на сайте заводить незачем.

   ⚠️ Заливка — светлый замес того же цвета, а не он сам. Текст
   чёрный, и на насыщенном #1d4ed8 он бы не читался: контраст
   чёрного на нём 2.6 к одному при пороге 4.5. На замесе в 18%
   получается 17 к одному, и цвет при этом остаётся узнаваемым. */
.proj-status {
  display: inline-block;
  /* ⚠️ Обязательно. Подпись карточки — флекс-колонка, и без этого
     пилюля растягивается на всю ширину строки: флекс тянет ребёнка
     по поперечной оси, что бы ни было написано в `display`. */
  align-self: flex-start;

  margin: var(--_spacing---spacing-10) 0 0;
  padding: 4px var(--_spacing---spacing-12);
  border: 0;
  border-radius: 999px;

  background-color: var(--proj-status-fill);
  color: var(--colors--matt-black);
  font-size: var(--_typography---font-size--small);
  line-height: var(--_typography---font-height--body-height);
  letter-spacing: normal;
  text-transform: uppercase;
}

.proj-status--released {
  --proj-status-fill: color-mix(in srgb, var(--case-tag--chosen) 18%, var(--colors--white));
}

/* 13.09.2026, решение заказчика: «в работе» — песочный, «в тесте» —
   синий. Песок — светлый замес тона метки компромисса `tradeoff`,
   тем же рецептом, что у соседних: палитра прежняя. У концепта —
   серый: он ни в работе, ни в тесте. */
.proj-status--progress {
  --proj-status-fill: color-mix(in srgb, var(--case-tag--tradeoff) 18%, var(--colors--white));
}

.proj-status--testflight {
  --proj-status-fill: color-mix(in srgb, var(--case-tag--requested) 18%, var(--colors--white));
}

.proj-status--concept {
  --proj-status-fill: var(--colors--cloud-white);
}

/* ============================================================
   ИНСТРУКЦИЯ ПРАВОК, СЕНТЯБРЬ 2026

   G6: на странице один <h1>. Строки, которые были вторым <h1>, стали
   абзацами с теми же классами оформления; у абзаца свои поля браузера,
   у заголовка их не было — снимаем, чтобы строка не сдвинулась.
   ============================================================ */
.hero-subtitle,
.resume-role { margin-top: 0; margin-bottom: 0; }

/* 3.12: страница 404. Раскладка кейсового блока, заголовок страницы
   и три строки со стрелкой — те же, что «Next case» на кейсах. */
.notfound-title { margin: 0; }
.notfound-text { margin: 0; color: var(--colors--granite-gray); }

/* 3.1.11: у концепта нет кадра — пустая серая коробка без подписей. */
.proj-ph:empty,
.proj-ph:not(:has(.ph-meta)) { justify-content: center; }

/* Profile в резюме — пунктами. Решение заказчика от 13.09.2026: текст
   тот же, что в PDF, но разбит на смысловые куски с точкой слева,
   ключевые слова жирным. Точка — та же, что у списков на страницах
   кейсов (`.case-list--dotted`), без волосяных линий: это абзац,
   разложенный по пунктам, а не перечень фактов. */
.resume-profile {
  margin: 0 0 var(--_spacing---spacing-20);
  padding: 0;
  list-style: none;
  max-width: 52ch;
}

.resume-profile li {
  position: relative;
  padding-left: var(--_spacing---spacing-20);
  color: var(--colors--matt-black);
  font-weight: 400;
  letter-spacing: normal;
}

.resume-profile li + li { margin-top: var(--_spacing---spacing-12); }

.resume-profile li:before {
  content: "";
  position: absolute;
  left: 0;
  top: .6em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background-color: var(--colors--matt-black);
}

.resume-profile strong { font-weight: 600; }

