/* ============================================================
   Прототип 5а — компоненты
   Значения только из tokens.css. Ни одного числа мимо канона.
   ============================================================ */

/* ---------- Двуязычность ----------
   Обе версии лежат в разметке, показывается одна.
   Так на прототипе видно, что русский длиннее английского
   на 10–15% и ничего от этого не ломается.                     */
[data-lang="ru"] [lang="en"],
[data-lang="en"] [lang="ru"] { display: none }

/* ---------- Каркас ---------- */
.wrap {
  max-width: calc(var(--col) + var(--page-pad) * 2);
  margin: 0 auto;
  padding: var(--page-top) var(--page-pad) var(--gap-section-lg);
}
.section       { margin-top: var(--gap-block);      position: relative }
.section--head { margin-top: var(--gap-head) }
.section--lg   { margin-top: var(--gap-section);    position: relative }
.section--xl   { margin-top: var(--gap-section-lg); position: relative }

.section__label {
  font-size: var(--text-body);
  line-height: var(--text-body-lh);
  font-weight: var(--text-strong-w);
  letter-spacing: var(--text-body-ls);
  color: var(--ink);
  margin: 0 0 var(--gap-label);
}
.measure { max-width: var(--measure) }
p { margin: 0 0 var(--text-body-mb) }
p:last-child { margin-bottom: 0 }

/* ---------- Левый рельс главной ----------
   Черточки-содержание сюда НЕ идут: по ревью они только на страницах
   кейсов. На главной эта зона — подписи к разделам.

   Позиционирование: метка прижата к левому краю окна на 40px при любой
   ширине. Формула считает расстояние от колонки до края и вычитает
   ширину метки — из-за clamp на широких экранах метка не уезжает
   к самому краю, а держится рядом с колонкой.                       */
.rail-mark {
  position: absolute; top: 0;
  width: var(--rail-mark-w, 143px);
  right: calc(100% + clamp(
    var(--s-6),
    (var(--layout-w, 100vw) - var(--col)) / 2 - var(--rail-mark-w, 143px) - var(--rail-left),
    232px));
}
/* --layout-w выставляет скрипт: 100vw включает полосу прокрутки,
   из-за чего метка промахивалась мимо левого края на половину её
   ширины. Без скрипта откатывается на 100vw — метка просто встанет
   на несколько пикселей левее, ничего не сломается.               */
/* Липкий блок компании: держится при скролле внутри своей группы
   и сменяется, когда начинается группа предыдущей компании.

   Липкость висит на вложенном элементе, а не на самой метке:
   `position: sticky` отменяет `absolute`, и метка уехала бы обратно
   в колонку. Поэтому снаружи абсолютный контейнер во всю высоту
   группы, внутри — липкое содержимое.                              */
/* Вытяжка ниже конца раздела — см. блок про плавные концы. */
.rail-mark--sticky { bottom: calc(-1 * var(--rail-tail)) }
.rail-mark--sticky > * { position: relative }
/* Прилипает к ВЕРХУ экрана с отступом 80, а не к середине.
   Про середину автор написал дважды — «ошибка сохранилась».        */
.rail-mark__inner { position: sticky; top: var(--rail-sticky) }
/* Подпись, которая гаснет к концу своего раздела — целиком, вместе
   со значком, названием и пояснением. Пока только у «Отзывов»:
   автор попросил именно её. Подписи компаний уезжают как раньше.

   Метка едет НИЖЕ конца блока: контейнер вытянут вниз на 140px
   отрицательным `bottom`. Раньше липкость отпускала ровно на
   последнем отзыве, и метка обрывалась вместе с блоком.

   140 — это один шаг вертикального ритма. Ниже метки остаётся ещё
   столько же до кромки листа (отступ снизу у колонки и у самого
   листа), так что запас на «ещё чуть ниже» есть.

   Гаснут и появляются ВСЕ липкие подписи одинаково — и у компаний,
   и у отзывов. Одиночное исключение для отзывов снято.

   Появление и гашение висят на РАЗНЫХ элементах: появление на
   контейнере, гашение на внутреннем блоке. Это не украшательство.
   Две анимации `opacity` на одном элементе не складываются: у той,
   что стоит в списке позже, заливка `both` держит своё крайнее
   значение и перебивает первую — появление просто не видно.
   А вложенные прозрачности перемножаются честно.

   ПОЯВЛЕНИЕ. Шкала — своя же, диапазон задан в пикселях, а не в
   процентах: `cover 0px → 280px` это первые 280px подъёма верхней
   кромки от низа экрана. В пикселях потому, что группы разной
   высоты, а проценты растянули бы появление у длинных групп
   и сжали у коротких.

   ГАШЕНИЕ. Шкала снята с самого раздела, а не с вытянутого
   контейнера метки: контейнер длиннее раздела на 140px, и на его
   шкале конец раздела приходился бы на 84% — подпись доходила бы
   до конца ещё видимой и уезжала за верх экрана, вместо того чтобы
   раствориться.

   ВЫТЯЖКА `--rail-tail`. Контейнер метки уходит на 200px ниже
   своего раздела. Это не «чтобы красивее проехала»: вытяжка
   отодвигает момент, когда липкость отпускает метку, за момент,
   когда метка уже невидима. Иначе подпись не растворяется, а
   уезжает за верх экрана недогасшей.

   Почему именно 200. Момент отпускания считается как
   (высота раздела + вытяжка − 80 − высота метки) / высота раздела,
   то есть у КОРОТКИХ разделов он наступает раньше в долях прохода.
   Самые короткие тут — Brand Scout, Nolim и Фриланс, по ~350px:
   при вытяжке 140 они отпускали на 87%, не дожив до конца гашения
   на 92%. При 200 отпускание уезжает за 100% у всех пяти разделов,
   то есть метка вообще не трогается с места, пока не погаснет.

   Если появится раздел короче 350px — пересчитать. Правило простое:
   вытяжка должна быть не меньше, чем 0.92 × высота раздела минус
   высота раздела плюс 182.

   Длина гашения — 42% прохода, около 320px. Скорость подтверждена
   автором, менять без спроса нельзя.                               */
.group, .section--mark-fade { view-timeline: --mark-out block }
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .rail-mark--sticky {
      animation: mark-in linear both;
      animation-timeline: view();
      animation-range: cover 0px cover 280px;
    }
    .rail-mark--sticky .rail-mark__inner {
      animation: mark-out linear both;
      animation-timeline: --mark-out;
      animation-range: exit 35% exit 70%;
    }
  }
}
@keyframes mark-in  { from { opacity: 0 } to { opacity: 1 } }
@keyframes mark-out { from { opacity: 1 } to { opacity: 0 } }

/* Логотип компании. Бокс 38 с полем 4 — та же величина, что у фото
   автора над содержанием кейса, и та же подложка, что у логотипа
   в блоке «Опыт». Пустым остаётся там, где логотипа нет. */
.rail-mark__icon {
  width: 38px; height: 38px; padding: var(--s-1); line-height: 0;
  border-radius: var(--r-md);
  background: var(--plate); box-shadow: var(--shadow-card);
  margin-bottom: var(--s-3);
}
.rail-mark__icon img {
  display: block; width: 30px; height: 30px;
  border-radius: var(--r-sm); object-fit: cover;
}
.rail-mark__title {
  color: var(--ink); font-weight: var(--text-strong-w);
  margin: 0 0 var(--s-1);
}
.rail-mark__sub {
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  color: var(--muted); margin: 0;
}
.rail-mark__sub + .rail-mark__sub { margin-top: var(--s-1) }
/* ---------- Метка на планшете ----------
   До 1279 рельса рядом с колонкой нет, места под него не остаётся.
   Метка не прячется, а уходит в поток над своей группой — так это
   и было описано в карте сайта и так ведёт себя референс noechague.

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

   В потоке метка горизонтальная: логотип слева, дальше название и
   через точку роль с периодом. Вертикальная колонка на всю ширину
   выглядела бы как отдельный раздел, а это подпись.               */
@media (max-width: 1279px) {
  .rail-mark {
    position: static; width: auto;
    margin-bottom: var(--gap-label);
    /* Гашение по прокрутке — механика липкого рельса. В потоке метка
       не липнет и гаснуть не должна. */
    animation: none;
  }
  .rail-mark--sticky { bottom: auto }
  .rail-mark__inner {
    position: static; animation: none;
    display: flex; align-items: center; flex-wrap: wrap; gap: 0 var(--s-2);
  }
  .rail-mark__icon { margin: 0 var(--s-1) 0 0 }
  .rail-mark__title { margin: 0 }
  /* Роль и период встают в ту же строку и отделяются точкой. */
  .rail-mark__sub { margin: 0 }
  .rail-mark__sub::before { content: "·"; color: var(--underline); margin-right: var(--s-2) }
}

/* ---------- Группа работ одной компании ---------- */
.group { position: relative }
.group + .group { margin-top: var(--gap-section) }
.group__body > * + * { margin-top: var(--gap-meta-next) }

/* ---------- Шапка ---------- */
.head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-6) }
.head__id { display: flex; align-items: center; gap: var(--s-5) }

/* Аватар — та же подложка, что и везде: 100×100, поле 4px, фото 92×92.
   Именно на него автор показал «серая на белом» — цвета были
   перевёрнуты. Теперь как все: серое поле, белая плашка поверх.     */
.avatar {
  width: 100px; height: 100px; flex: none;
  padding: var(--s-1); line-height: 0;
  border-radius: var(--r-lg); background: var(--plate);
  box-shadow: var(--shadow-card);
}
.avatar img, .avatar .ph {
  display: block; width: 92px; height: 92px;
  border-radius: var(--r-md); object-fit: cover;
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--hairline);
}
.name {
  font-size: var(--text-title); line-height: var(--text-title-lh);
  font-weight: var(--text-title-w); letter-spacing: var(--text-title-ls);
  color: var(--ink); margin: 0;
}
.role {
  font-size: var(--text-body); line-height: var(--text-body-lh);
  color: var(--muted); margin: var(--s-1) 0 0;
}

.switches { display: flex; align-items: center; gap: var(--s-4); flex: none; padding-top: var(--s-1) }
.lang { display: flex; align-items: center; gap: var(--s-1) }
.lang button {
  appearance: none; border: 0; background: none; cursor: pointer; padding: 0;
  font: inherit; font-size: var(--text-meta); line-height: var(--text-meta-lh);
  font-weight: var(--text-meta-w); color: var(--muted);
  transition: color var(--dur-fast) var(--ease);
}
.lang button[aria-pressed="true"] { color: var(--ink) }
.lang span { color: var(--underline); font-size: var(--text-meta) }

/* Переключатель темы — кружок 14px, залитый наполовину.
   Заливка градиентом, а не тенью: тень из var() застревает при
   смене темы, градиент от currentColor — нет.                      */
.theme-toggle {
  appearance: none; padding: 0; cursor: pointer;
  width: 14px; height: 14px; border: 0; background: none;
  color: var(--muted); display: grid; place-items: center;
  transition: color var(--dur-fast) var(--ease);
}
.theme-toggle:hover { color: var(--ink) }
.theme-toggle i {
  display: block; width: 13.5px; height: 13.5px;
  border: 1.5px solid currentColor; border-radius: var(--r-full);
  background: linear-gradient(90deg, currentColor 0 50%, transparent 50% 100%);
  view-transition-name: theme-icon;
  transition: transform .12s cubic-bezier(.23, 1, .32, 1);
}
.theme-toggle:active i { transform: scale(.86) rotate(16deg) }

/* ---------- Текст и ссылки ---------- */
a { color: var(--ink) }
.link {
  text-decoration: underline;
  text-decoration-color: var(--underline);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--dur-fast) var(--ease);
}
.link:hover { text-decoration-color: var(--ink) }
strong, .ink { color: var(--ink); font-weight: var(--text-strong-w) }

/* ---------- Подложка ----------
   Железное правило из ревью: изображение никогда не лежит голым.
   Под ним подложка, поверх — плашка с изображением.

   Порядок цветов именно такой: СЕРАЯ подложка, БЕЛАЯ плашка поверх.
   Обратный порядок автор забраковал дважды. Замерено на
   noechague-site.vercel.app: поле 4px, внешняя тень, у внутренней
   плашки волосяная линия. Автор подтвердил этот вид на архиве.     */
.plate {
  padding: var(--s-1);
  border-radius: var(--r-lg);
  background: var(--plate);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}
.plate__in {
  display: block;
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--hairline);
  overflow: hidden;
}
.plate__in img, .plate__in video { display: block; width: 100%; height: 100%; object-fit: cover }

/* ---------- Мокапы ----------
   Автор попросил показывать экраны в мокапах. Рамка тут не украшение:
   она говорит, что перед вами интерфейс, а не картинка, и сразу
   отвечает, веб это или приложение.

   Собраны на той же подложке, что и остальные изображения, чтобы не
   заводить вторую породу рамок.                                    */
.plate--browser { padding-top: 0 }
.plate__bar {
  display: flex; align-items: center; gap: 6px;
  height: 28px; padding: 0 var(--s-3);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.plate__bar::before {
  content: ""; flex: none;
  width: 8px; height: 8px; border-radius: var(--r-full);
  background: var(--underline);
  /* Три кружка одной тенью: отдельные элементы ради декора заводить
     незачем, а разметка остаётся пустой и немой для скринридера. */
  box-shadow: 14px 0 0 var(--underline), 28px 0 0 var(--underline);
}
.plate--browser .plate__in { border-radius: 0 0 var(--r-md) var(--r-md) }

/* Телефон. Поле шире обычного, скругление больше, сверху островок. */
.plate--phone {
  position: relative;
  width: min(375px, 100%); margin-inline: auto;
  padding: var(--s-2);
  border-radius: var(--r-xl);
}
.plate--phone .plate__in { border-radius: var(--r-md) }
/* Островок соразмерен рамке: телефон в кейсе меньше настоящего,
   и остров в натуральную величину смотрелся бы нашлёпкой.        */
.plate--phone::after {
  content: ""; position: absolute; z-index: 1;
  top: 15px; left: 50%; transform: translateX(-50%);
  width: 44px; height: 11px; border-radius: var(--r-full);
  background: var(--ink); opacity: .85;
}

/* ---------- Кнопки ----------
   Снято с макета, узел 2009:498. Кегль 14/20 — исключение из шкалы. */
.buttons { display: flex; flex-wrap: wrap; gap: var(--gap-inline); margin-top: var(--gap-text) }
/* Контуры сделаны внутренними тенями, а не border: с настоящей рамкой
   кнопка вырастала до 39–40px вместо 36 из макета, и основная
   с второстепенной расходились по высоте.                          */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  padding: var(--s-2) 14px; border: 0;
  border-radius: var(--r-sm);
  font: inherit; font-size: var(--text-btn); line-height: var(--text-btn-lh);
  font-weight: var(--text-btn-w); letter-spacing: 0;
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease),
              box-shadow      var(--dur-fast) var(--ease),
              color           var(--dur-fast) var(--ease);
}
.btn--primary {
  background: var(--ink); color: var(--canvas);
  box-shadow: var(--shadow-btn), inset 0 0 0 2px rgba(255, 255, 255, .2);
}
.btn--primary:hover { background: var(--ink-hover) }
.btn--ghost {
  background: var(--surface); color: var(--muted);
  box-shadow: var(--shadow-btn), inset 0 0 0 1px var(--btn-border);
}
.btn--ghost:hover { background: var(--hover); color: var(--ink) }
.btn[disabled] { opacity: .4; cursor: default; pointer-events: none }

/* ---------- Превью ссылки при наведении ----------
   Карточка с фавиконкой, доменом и снимком страницы. Пока снимка нет —
   серая плашка, как в референсе. Все внутренние части блочные: раньше
   были инлайновыми, и высота у плашки не применялась — превью выглядело
   пустым.                                                            */
.lnk { position: relative; white-space: nowrap }
.lnk__prev {
  position: absolute; bottom: calc(100% + var(--s-2)); left: 0; z-index: 20;
  display: block; width: 232px; padding: var(--s-1);
  border-radius: var(--r-lg); background: var(--plate);
  box-shadow: var(--shadow-card); white-space: normal;
  opacity: 0; transform: translateY(4px); pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.lnk:hover .lnk__prev, .lnk:focus-within .lnk__prev { opacity: 1; transform: none }
/* Превью не показывается там, где нет курсора, и на узкой колонке.

   Дело не только в том, что наводить нечем. Плашка шириной 232
   позиционирована от начала ссылки, а ссылка стоит в середине
   строки — на 375 она вылезала за правый край и распирала страницу
   до 451. Горизонтальная прокрутка на телефоне тянула за собой
   всё остальное: модалка и лайтбокс считают ширину от окна и
   становились 410 при экране 375.                                 */
@media (hover: none), (max-width: 767px) {
  .lnk__prev { display: none }
}
.lnk__host {
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--text-meta); line-height: var(--text-meta-lh); color: var(--muted);
  padding: var(--s-1) var(--s-2) var(--s-2);
}
/* Иконка планеты, а не белый квадрат: пока у сайта нет фавиконки,
   в референсе на её месте стоит глобус.                            */
.lnk__fav { width: 14px; height: 14px; flex: none; display: block; color: var(--muted) }
.lnk__fav svg { display: block; width: 14px; height: 14px }
.lnk__shot {
  display: block; height: 116px; border-radius: var(--r-md); overflow: hidden;
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--hairline);
}
/* Скриншот сайта. Прижат к верху: снимок высокий, и обрезаться должен
   низ страницы, а не шапка, ради которой превью и смотрят. */
.lnk__shot img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: top;
}

/* ---------- Ссылка с иконкой ----------
   Правило: у ссылки с иконкой есть поле по обе стороны, иначе иконка
   слипается с предыдущим словом, а подчёркивание — со следующим.     */
.link--icon {
  display: inline-flex; align-items: center; gap: var(--gap-icon);
  margin: 0 var(--gap-icon);
}
.logo {
  display: inline-block; width: 16px; height: 16px; border-radius: 4px;
  background: var(--hover); box-shadow: inset 0 0 0 1px var(--hairline);
  flex: none;
}
/* Логотип внутри ссылки. Плашка выше остаётся фоном под ним, чтобы
   строка не дёргалась, пока файл грузится. */
.logo img {
  display: block; width: 16px; height: 16px;
  border-radius: 4px; object-fit: cover;
}

/* ---------- Строка-рамка ---------- */
.frame-line {
  max-width: var(--measure); color: var(--muted);
  margin: 0 0 var(--gap-label);
}

/* ---------- Верх внутренней страницы ----------
   Шапки с фото и именем здесь нет, её убрали по доске ревью 2079:189.
   Осталась строка возврата слева и переключатели справа — по той же
   базовой линии, что и на странице кейса.                          */
.page-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4);
}
.page-top .back { margin-bottom: 0 }
.page-top .switches { padding-top: 0 }

/* ---------- Страница работы со старого сайта ----------
   Кейсы с mursal.framer.website перенесены как есть: те же картинки
   в том же порядке. Колонка здесь шире обычной — на старом сайте
   медиа шло в 880px, и ужимать его до 610 значит не показать
   интерфейс, ради которого кейс и открывают.

   Текст, где он был, оставлен по-английски и языком не переключается.
   Переводить надписи внутри картинок всё равно нечем, а половина
   русского рядом с английскими скриншотами читалась бы хуже, чем
   честный английский целиком.                                      */
/* Колонка страниц работ больше не отличается от остальных. Была 880,
   столько занимало медиа на старом сайте, и подложки с кадрами там
   выходили шире, чем на странице Borrow. */
.work-media > * + * { margin-top: var(--gap-card) }
/* Кадр здесь figure, а у него в браузерных стилях свои поля
   по 40 пикселей с боков. Из-за них подложка стояла уже колонки. */
.work-media .shot { margin-inline: 0; margin-bottom: 0; width: 100% }

/* Бесшовная лента. Кадры идут встык, подложка одна на всю ленту.
   line-height обнуляется у обоих уровней: у строчных картинок
   иначе остаётся зазор в несколько пикселей от базовой линии,
   и «бесшовно» перестаёт быть бесшовным.                          */
.seam__in { display: block; line-height: 0; font-size: 0 }
.seam__in img, .seam__in video {
  display: block; width: 100%; height: auto;
  margin: 0; border-radius: 0;
}

/* Кадр работы. Та же подложка, что у обложки карточки и у аватара:
   поле 4, плашка, тень. По железному правилу консистентности.     */
.shot {
  padding: var(--s-1); line-height: 0;
  border-radius: var(--r-lg);
  background: var(--plate); box-shadow: var(--shadow-card);
}
.shot img, .shot video {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-md);
}
/* Потолка высоты у кадра нет. Он вводился, когда колонка была
   880 и вертикальная запись растягивалась на два экрана. В колонке
   610 такая запись занимает 1246, и её наконец видно целиком,
   а не сжатой до половины ширины. */

/* Мера строки та же, что везде, но колонка здесь шире обычной,
   поэтому текст центрируется, а не жмётся к левому краю кадров. */
/* Текст относится к кадру под ним, поэтому снизу отступ меньше,
   чем сверху. Было 80 и 20, разница вчетверо читалась как ошибка.
   Стало 64 и 32, связь та же, скачок мягче.

   По левому краю, а не по центру. Мера строки уже колонки, и если
   текст центрировать, он перестаёт совпадать с краем кадров. На
   странице Borrow он всегда стоял по левому краю.               */
.work-copy {
  max-width: var(--measure);
  margin: var(--s-9) 0 var(--s-7);
}
.work-copy:first-child { margin-top: 0 }
.work-copy h2 {
  font-size: var(--text-title); line-height: var(--text-title-lh);
  font-weight: var(--text-title-w); letter-spacing: var(--text-title-ls);
  color: var(--ink); margin: 0 0 var(--text-title-mb);
}
.work-copy h3 {
  font-size: var(--text-body); line-height: var(--text-body-lh);
  font-weight: var(--text-strong-w); color: var(--ink);
  margin: var(--s-6) 0 var(--text-strong-mb);
}
.work-copy h2 + h3 { margin-top: 0 }
.work-note { color: var(--muted) }
/* Подпись под текстом перед кадром — как мета-строка карточки. */
.work-cap {
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  font-weight: var(--text-meta-w); color: var(--muted);
  margin: var(--s-6) 0 0;
}

/* Кегль страницы — тот же, что у названия кейса. Раздел работ
   такой же по весу шаг, как кейс, и мельче него быть не должен.   */
.page-title {
  font-size: var(--text-display); line-height: var(--text-display-lh);
  font-weight: var(--text-display-w); letter-spacing: var(--text-display-ls);
  color: var(--ink); margin: 0;
}
.back {
  display: inline-block; margin: 0 0 var(--s-3);
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  font-weight: var(--text-meta-w); color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.back:hover { color: var(--ink) }
.page-lead { max-width: var(--measure); margin: var(--s-3) 0 0; color: var(--body-c) }

/* ---------- Карточки работ ---------- */
.work-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-card) }
.work-row + .work-row,
.work-row + .work-wide,
.work-wide + .work-row { margin-top: var(--gap-meta-next) }
.work-wide { display: grid; grid-template-columns: 1fr }
/* Две карточки подряд. Раньше такого сочетания не было — ряды всегда
   чередовались с широкими, и правил хватало. В блоке «Остальные
   проекты» карточки идут одна за другой, и без этого правила мета
   первой прилипала к обложке второй. Отступ тот же, что везде. */
.work-wide + .work-wide { margin-top: var(--gap-meta-next) }

.card { display: block; text-decoration: none; color: inherit }
.card__cover {
  padding: var(--s-1);
  border-radius: var(--r-lg);
  background: var(--plate);
  box-shadow: var(--shadow-card);
  transition: transform var(--dur-enter) var(--ease-enter);
}
.card__cover > span {
  display: block; aspect-ratio: 295 / 316;
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.work-wide .card__cover > span { aspect-ratio: 610 / 316 }
/* Настоящая обложка вместо плашки. Плашка остаётся фоном под ней:
   пока картинка грузится, карточка не схлопывается и не мигает. */
.card__cover > span > img {
  display: block; width: 100%; height: 100%;
  border-radius: inherit; object-fit: cover;
}
.card:hover .card__cover { transform: translateY(-4px) }

/* По ревью: подзаголовок переехал в заголовок, отдельного названия
   проекта в мета-строке больше нет. Было: EMCD · P2P · 2024 плюс
   строка «Полный редизайн». Стало: EMCD · Полный редизайн · 2024.  */
.card__meta {
  display: flex; align-items: baseline; gap: var(--s-2);
  margin-top: var(--gap-card-meta);
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  font-weight: var(--text-meta-w); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.card__meta .sep { width: 1px; height: 14px; background: var(--hairline); align-self: center }
.card__meta .title { color: var(--ink) }
.card__meta .year { margin-left: auto }

/* ---------- Портфолио: три раздела работ ----------
   Тот же компонент карточки, что и у кейсов. Отличий два, оба сняты
   с макета 2078:96: обложка ниже (598×178 и 283×178 против 610×316
   и 295×316) и подпись крупнее — под карточкой стоит имя раздела,
   а не мета-строка «компания · кейс · год».                        */
.pf .card__cover > span            { aspect-ratio: 283 / 178 }
.pf .work-wide .card__cover > span { aspect-ratio: 598 / 178 }
.card__meta--name {
  font-size: var(--text-body); line-height: var(--text-body-lh);
  font-weight: var(--text-strong-w); letter-spacing: var(--text-body-ls);
}

/* ---------- Опыт списком ----------
   Компоновка из референса, который автор положил на макет 2078:96.
   Просьба была дословная: «повторить этот блок, только без фонового
   цвета и в рамках нашей сетки». Поэтому тёмной подложки нет, ширина
   равна колонке, а строки разделены волосяной линией — тем же
   приёмом, что и отзывы.                                           */
.cpath__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-4);
}
.cpath__head .section__label { margin-bottom: 0 }
.cpath__count {
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  font-weight: var(--text-meta-w); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.cpath { margin-top: var(--gap-label) }
.cpath__row {
  display: flex; align-items: flex-start; gap: var(--s-5);
  padding: var(--gap-tst) 0;
}
.cpath__row + .cpath__row { border-top: 1px solid var(--hairline) }
.cpath__row:first-child { padding-top: 0 }
.cpath__row:last-child  { padding-bottom: 0 }

/* Ярлык и значение — та же пара, что в мета-таблице кейса.
   Колонка ярлыка 76px: по-русски «Компания» длиннее английского
   Company, и по содержимому она тянуться не должна — иначе строки
   разъедутся между языками.                                        */
.cpath__facts {
  flex: 1 1 auto; margin: 0;
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  font-variant-numeric: tabular-nums;
}
.cpath__facts > div { display: grid; grid-template-columns: 76px 1fr; gap: var(--s-4) }
.cpath__facts > div + div { margin-top: var(--s-2) }
.cpath__facts dt { color: var(--muted) }
.cpath__facts dd { margin: 0; color: var(--ink) }
.cpath__name { font-weight: var(--text-strong-w) }

/* Логотип. Файлы забраны со старого сайта автора 21.08.2026,
   лежат в assets/logos. Лежит на подложке, как аватар и как обложка
   карточки: по железному правилу консистентности, если изображение
   на подложке — на подложке все изображения. Поле 4, внутри 40×40. */
.cpath__logo {
  flex: none; width: 48px; height: 48px;
  padding: var(--s-1); line-height: 0;
  border-radius: var(--r-md);
  background: var(--plate); box-shadow: var(--shadow-card);
}
.cpath__logo img {
  display: block; width: 40px; height: 40px;
  border-radius: var(--r-sm); object-fit: cover;
}

/* ---------- Отзывы ---------- */
.tst { padding-top: var(--gap-tst); padding-bottom: var(--gap-tst) }
.tst + .tst { border-top: 1px solid var(--hairline) }
.tst:first-of-type { padding-top: 0 }
.tst:last-of-type { padding-bottom: 0 }

/* Текст занимает ровно четыре строки и обрывается на многоточии,
   сразу за которым идёт «Читать целиком». Ровно четыре строки
   отрезает скрипт по словам: у line-clamp кнопку нечем поставить
   в конец последней строки, она вставала в случайное место.        */
.tst__quote { max-width: var(--measure); margin: 0 }
.tst__quote.is-clamped { visibility: hidden }
.tst__more {
  appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
  font: inherit; color: var(--ink); font-weight: var(--text-strong-w);
  text-decoration: underline; text-decoration-color: var(--underline);
  text-underline-offset: 3px; white-space: nowrap;
  transition: text-decoration-color var(--dur-fast) var(--ease);
}
.tst__more:hover { text-decoration-color: var(--ink) }
/* В модалке текст целиком. Поле справа только у первого абзаца —
   под крестик подлезает именно первая строка.                     */
.modal__body > p:first-child { padding-right: var(--s-8) }

.tst__author {
  display: flex; align-items: center; gap: var(--s-3);
  margin: var(--gap-tst) 0 0;
}
.tst__author .who { display: flex; align-items: center; gap: var(--s-3) }
/* Ссылка на оригинал — напротив данных автора, а не под текстом. */
.tst__author .src { margin-left: auto; font-size: var(--text-meta); line-height: var(--text-meta-lh) }
.tst__author .ph {
  width: 32px; height: 32px; border-radius: var(--r-full); flex: none;
  background: var(--hover); box-shadow: inset 0 0 0 1px var(--hairline);
}
/* Фото автора. Заглушка выше остаётся фоном под ним, поэтому пока
   файл грузится, строка не прыгает. */
img.ph { object-fit: cover }
.tst__author .n { color: var(--ink); font-size: var(--text-meta); line-height: var(--text-meta-lh); font-weight: var(--text-meta-w) }
.tst__author .r { color: var(--muted); font-size: var(--text-meta); line-height: var(--text-meta-lh) }

.tst__src {
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  color: var(--muted); margin: 0 0 var(--s-4);
}

/* ---------- Experiments and Lab ----------
   Блок шире колонки — 990px, и открывается переходом из Marvin.

   Механика снята с marvinschwaibold.com замером, а не на глаз: тёмный
   фон — отдельный слой во весь экран с `position: fixed`, он не
   двигается вообще. Светлый лист со скруглённым низом уезжает вверх
   и открывает его. Содержимое блока едет обычным потоком поверх.

   Прошлая версия делала это через `position: sticky` на самом блоке.
   Она и тормозила: липкий блок в полтора экрана браузер перерисовывает
   на каждом кадре прокрутки, а когда он выше экрана — прилипает
   верхом, из-за чего блок и «прикреплялся к верху». Фиксированный
   слой не перерисовывается никогда: композитор двигает лист поверх
   неподвижной подложки.                                            */
.reveal { position: relative; background: var(--lab); timeline-scope: --sheet-edge }
.reveal__bg {
  position: fixed; inset: 0; z-index: 0;
  background: var(--lab); pointer-events: none;
}
.sheet {
  position: relative; z-index: 1;
  background: var(--canvas);
  border-bottom-left-radius: var(--s-7);
  border-bottom-right-radius: var(--s-7);
  padding-bottom: var(--gap-section-lg);
}
/* Тёмная полоса под блоком Lab и подвалом. На главной и страницах
   разделов её давала обёртка .reveal, но у страниц кейса такой
   обёртки нет, и белые ссылки подвала оказывались на белом. Цвет
   тот же самый, поэтому там, где .reveal есть, ничего не меняется. */
.lab-wrap { position: relative; z-index: 1; background: var(--lab) }

/* Лист не просто уезжает — он ещё и СУЖАЕТСЯ, а углы округляются.
   Снято с раскадровки автора: на первом кадре лист во всю ширину
   с прямыми углами, дальше он поджимается с боков и по бокам
   открывается тёмное. Содержимое при этом не масштабируется,
   поэтому это обрезка (clip-path), а не transform.

   Поджатие 8% — на кадрах вышло около 5%, автор попросил заметнее.

   Шкалу задаёт маркер нулевой высоты у нижней кромки листа: прогресс
   идёт ровно от «кромка вошла снизу» до «кромка ушла вверх».
   Диапазоны entry/exit для элемента выше экрана считаются неочевидно,
   а с маркером считать нечего.

   Маркер прижат к самому низу листа абсолютом, а не стоит последним
   в потоке. В потоке он оказывался на 140px выше настоящей кромки
   (`padding-bottom` листа), и поджатие успевало закончиться, пока
   кромка ещё шла по экрану. Теперь оно доходит до предела ровно
   в тот момент, когда лист уходит целиком.

   Кривая — разгон `--ease-in`, а не равномерно: поджатие долго
   держится маленьким и добирает в конце. По просьбе автора —
   «медленнее, заканчивалось чуть позже, чуть выше».                */
.sheet__edge {
  position: absolute; bottom: 0; left: 0; right: 0; height: 0;
  view-timeline: --sheet-edge block;
}
/* На телефоне поджатия нет. По просьбе автора: на узком экране лист
   и так занимает всю ширину, боковые 8% съедают заметную часть
   колонки, а тёмные поля по краям читаются как сбой вёрстки.
   Поэтому анимация только от 768.                                  */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-width: 768px) {
    .sheet {
      border-radius: 0;
      animation: sheet-away var(--ease-in) both;
      animation-timeline: --sheet-edge;
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes sheet-away {
  from { clip-path: inset(0 0 0 0 round 0 0 0 0) }
  to   { clip-path: inset(0 8% 0 8% round 0 0 var(--s-8) var(--s-8)) }
}
.lab {
  width: min(var(--lab-w), 100% - var(--page-pad) * 2);
  margin: 0 auto;
  /* Снизу 40, а не 140: по ревью «этот отступ в конце сайта сделай
     40px». Сверху ритм крупных разделов остаётся.                  */
  padding: var(--gap-section-lg) 0 var(--gap-page-end);
  color: var(--lab-ink);
}

/* Появление плиток — по решению автора, поверх механики листа.
   На раскадровке его не было, но автор попросил вернуть.

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

   Где шкалы прокрутки нет (Safari, Firefox), правило не применяется
   и плитки просто видны. Прятать то, показ чего зависит от
   поддержки, нельзя: осечка оставит блок пустым навсегда.          */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .lab__label, .lab__tile, .lab__cap, .lab .foot__links {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
    }
  }
}
@keyframes rise-in {
  from { opacity: 0; transform: translateY(24px) }
  to   { opacity: 1; transform: none }
}
.lab__label {
  font-size: var(--text-body); line-height: var(--text-body-lh);
  font-weight: var(--text-strong-w); color: var(--lab-ink);
  margin: 0 0 var(--gap-label);
}
.lab__grid { columns: 4; column-gap: 10px }
/* Плитка ведёт себя как обложка кейса: та же подложка с полем 4,
   тот же подъём на ховере. По ревью — «при наведении сделать такой
   же эффект, как и для карточек кейсов».

   Кнопка, а не ссылка: плитка открывает лайтбокс, а не переход.   */
.lab__tile {
  display: block; width: 100%; padding: var(--s-1); margin-bottom: 10px;
  border: 0; cursor: pointer;
  border-radius: var(--r-lg); background: var(--lab-plate);
  box-shadow: var(--shadow-dark);
  break-inside: avoid; -webkit-column-break-inside: avoid;
  transition: transform var(--dur-enter) var(--ease-enter);
}
/* Пустая плитка. Остаётся для тех мест, куда работа ещё не легла. */
.lab__tile:empty::before {
  content: ""; display: block; height: 100%;
  border-radius: var(--r-md); background: var(--lab-media);
}
/* Плитка с работой. Высоту задаёт сам файл, поэтому кладка
   складывается из настоящих пропорций, а не из чисел в разметке.
   Анимации живут здесь на тех же правах, что и кадры. */
.lab__tile img, .lab__tile video {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-md); background: var(--lab-media);
}
.lab__tile:hover { transform: translateY(-4px) }
.lab__cap { font-size: var(--text-meta); line-height: var(--text-meta-lh); color: var(--lab-muted); margin: var(--s-2) 0 0 }
.lab .foot { margin-top: var(--gap-section) }
/* Подвал страницы раздела. Тот же тёмный блок, но плиток в нём нет,
   поэтому верхний отступ раздела ему велик, а собственный отступ
   подвала не нужен вовсе.                                          */
.lab--foot { padding-top: var(--gap-section) }
.lab--foot .foot { margin-top: 0 }
.lab .foot__links a { color: var(--lab-ink) }

/* ---------- Архив ----------
   По ревью: мета уходит вправо, при наведении всплывает баннер.    */
.arch { display: flex; flex-direction: column; gap: var(--s-1) }
.arch__row {
  position: relative; display: flex; align-items: baseline; gap: var(--s-4);
  margin: 0 calc(var(--s-2) * -1); padding: var(--s-2);
  border-radius: var(--r-lg); text-decoration: none;
  font-size: var(--text-meta); line-height: var(--text-meta-lh);
  color: var(--muted); font-variant-numeric: tabular-nums;
  transition: background-color var(--dur-base) var(--ease);
}
.arch__row:hover { background: var(--hover) }
.arch__row .dash { flex: none; width: 20px }
.arch__row .n { color: var(--ink); font-weight: var(--text-meta-w) }
.arch__row .meta { margin-left: auto; text-align: right; white-space: nowrap }

.arch__banner {
  position: absolute; right: var(--s-2); bottom: calc(100% - var(--s-2)); z-index: 20;
  width: 260px; padding: var(--s-1);
  border-radius: var(--r-lg); background: var(--plate);
  box-shadow: var(--shadow-card);
  opacity: 0; transform: translateY(6px) scale(.98); pointer-events: none;
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease-enter);
}
.arch__banner span {
  display: block; height: 132px; border-radius: var(--r-md);
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--hairline);
}
.arch__banner span > img {
  display: block; width: 100%; height: 100%;
  border-radius: inherit; object-fit: cover;
}
.arch__row:hover .arch__banner,
.arch__row:focus-visible .arch__banner { opacity: 1; transform: none }

/* ---------- Подвал ---------- */
.foot { margin-top: var(--gap-section-lg); text-align: center }
.foot__links { display: flex; justify-content: center; gap: var(--s-5); font-size: var(--text-meta); line-height: var(--text-meta-lh) }
/* Шар — WebGL-эмбед автора, сам держит квадрат и сам себя гасит,
   когда уходит с экрана.

   Липнет к низу экрана на ВСЁМ сайте, а не только в подвале: по
   ревью «сделай этот шар липким на протяжении всего сайта». Прошлый
   вариант с `sticky` внутри подвала показывал его только в конце.

   Док лежит на уровне страницы, вне `.sheet`: у листа анимируется
   `clip-path`, а он обрезает потомков с `position: fixed`.

   `pointer-events: none` на доке и `auto` на самом шаре — док во всю
   ширину не должен перехватывать клики по странице.                */
.orb-dock {
  position: fixed; left: 0; right: 0; bottom: var(--s-6); z-index: 40;
  display: grid; place-items: center; pointer-events: none;
}
.orb-dock .orb { width: 60px; aspect-ratio: 1; pointer-events: auto }

/* ---------- Модалка отзывов ---------- */
.modal[hidden] { display: none }
.modal {
  position: fixed; inset: 0; z-index: 50;
  display: grid; place-items: center; padding: var(--s-5);
}
.modal__backdrop {
  position: absolute; inset: 0; background: var(--lab); opacity: .6;
  animation: fade var(--dur-enter) var(--ease-enter);
}
.modal__panel {
  position: relative; width: min(560px, 100%);
  max-height: calc(100vh - var(--s-7));
  overflow-y: auto;
  background: var(--surface); border-radius: var(--r-xl);
  box-shadow: var(--shadow-card);
  padding: var(--s-6);
  animation: rise var(--dur-enter) var(--ease-enter);
}
@keyframes fade { from { opacity: 0 } }
@keyframes rise { from { opacity: 0; transform: translateY(12px) } }

.modal__body { transition: opacity var(--dur-base) var(--ease) }
/* Высота панели едет, а не прыгает при смене отзыва. Значение
   выставляет скрипт, здесь только сам переход.                    */
.modal__panel { transition: height var(--dur-enter) var(--ease-enter) }
.modal.is-swapping .modal__body { opacity: 0 }

/* Выбран вариант C: таблетка под панелью, счётчик между стрелками.
   Стрелки внутри панели убраны — по ревью были лишними.

   z-index обязателен: затемнение позиционировано, а стек нет, и по
   правилам покраски затемнение ложилось поверх — стрелки не нажимались. */
.modal__stack { position: relative; z-index: 1; display: grid; gap: var(--s-4); width: min(560px, 100%) }
.modal__bar {
  display: flex; align-items: center; gap: var(--s-4); justify-self: center;
  background: var(--surface); box-shadow: var(--shadow-card);
  border-radius: var(--r-full); padding: var(--s-1) var(--s-2);
}
.modal__count {
  font-size: var(--text-meta); line-height: var(--text-meta-lh); color: var(--muted);
  font-variant-numeric: tabular-nums; order: 2; min-width: 40px; text-align: center;
}
.modal__bar .icon-btn { border: 0 }
.modal__bar #prev { order: 1 }
.modal__bar #next { order: 3 }
.icon-btn {
  appearance: none; cursor: pointer;
  width: 32px; height: 32px; border-radius: var(--r-sm);
  border: 1px solid var(--hairline); background: transparent; color: var(--ink);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background-color var(--dur-fast) var(--ease);
}
.icon-btn:hover { background: var(--hover) }
.modal__close { position: absolute; top: var(--s-4); right: var(--s-4); border-color: transparent }

/* ---------- Вход страницы ----------
   Лесенка 30/40/50/60, шаг задержки 60ms, только первая загрузка,
   только то, что видно без прокрутки, не больше четырёх элементов.

   Условие инвертировано намеренно: прячем и анимируем ТОЛЬКО там,
   где движение разрешено. Если написать наоборот — `opacity: 0`
   в базовом правиле и глушить анимацию при reduced-motion, — то
   у человека с отключённым движением шапка останется прозрачной
   навсегда. Проверено, ровно так и происходило.                  */
@media (prefers-reduced-motion: no-preference) {
  .enter { opacity: 0; animation: enter var(--dur-enter) var(--ease-enter) forwards }
  .enter:nth-child(1) { --y: 30px; animation-delay:   0ms }
  .enter:nth-child(2) { --y: 40px; animation-delay:  60ms }
  .enter:nth-child(3) { --y: 50px; animation-delay: 120ms }
  .enter:nth-child(4) { --y: 60px; animation-delay: 180ms }
}
@keyframes enter { from { opacity: 0; transform: translateY(var(--y, 30px)) } to { opacity: 1; transform: none } }

/* ---------- Адаптив ---------- */
@media (max-width: 767px) {
  .avatar { width: 72px; height: 72px }
  .avatar img, .avatar .ph { width: 64px; height: 64px }
  .head__id { gap: var(--s-4) }
  .work-row { grid-template-columns: 1fr }
  .work-row + .work-row, .work-row + .work-wide, .work-wide + .work-row { margin-top: var(--gap-section) }
  /* Пропорцию держит плашка внутри рамки, а не сама рамка. Раньше
     16:10 стояло на .card__cover — рамка вытягивалась до 209, плашка
     оставалась 169 по своей пропорции, и снизу внутри карточки
     висела пустая полоса в 32 пикселя. Автор её и заметил.        */
  .card__cover { aspect-ratio: auto }
  .card__cover > span,
  .work-wide .card__cover > span,
  .pf .card__cover > span,
  .pf .work-wide .card__cover > span { aspect-ratio: 16 / 10 }

  .cpath__facts > div { grid-template-columns: 68px 1fr; gap: var(--s-3) }

  /* Логотип в блоке «Опыт» переезжает наверх строки и уменьшается.
     Справа он отнимал у значений треть ширины, а «Product Designer»
     и так переносится на 335.                                      */
  .cpath__row { flex-direction: column; align-items: flex-start; gap: var(--s-5) }
  .cpath__logo { order: -1; width: 36px; height: 36px }
  .cpath__logo img { width: 28px; height: 28px; border-radius: var(--r-sm) }

  /* Поля строки на телефоне крупнее, чем на десктопе: строка стала
     выше, логотип встал отдельной строкой сверху, и прежние 24
     переставали отделять места работы друг от друга. Значения
     задал автор.                                                   */
  .cpath__row { padding: var(--s-7) 0 }

  /* Хвост белого листа. 80 от колонки плюс 80 от самого листа давали
     160 пустоты между последним отзывом и тёмным блоком. */
  .wrap { padding-bottom: var(--s-9) }
  .sheet {
    padding-bottom: 0;
    /* Углы листа мельче. На широком экране их скругляет анимация
       ухода и там они крупные; на телефоне анимации нет, и те же 32
       на узкой полосе читались тяжело. */
    border-bottom-left-radius: var(--s-4);
    border-bottom-right-radius: var(--s-4);
  }
  /* Ритм страницы работы. 80 между текстом и кадрами — шаг широкой
     колонки; на 335 он рвёт рассказ на несвязанные куски. */
  .work-copy { margin-top: var(--s-8) }
  .lab__grid { columns: 2 }
  .arch__row { flex-wrap: wrap }
  .arch__row .n { width: auto }
  .head { flex-direction: column; gap: var(--s-4) }
}

/* ---------- Лайтбокс работ Lab ----------
   Шире модалки отзывов: там текст в меру 545, здесь изображение,
   и его смысл в том, чтобы посмотреть крупно.                     */
/* Ширину задаёт само изображение, потолок прежний. Раньше рамка
   всегда была 990×16:10, и вертикальная работа висела в ней с
   тёмными полями сверху и снизу. Теперь панель обнимает картинку,
   но не вырастает ни шире 990, ни выше экрана.                     */
.modal__stack--wide { width: auto; max-width: min(var(--lab-w), 100%) }
.lb__frame {
  position: relative;
  background: var(--lab-plate); border-radius: var(--r-xl);
  padding: var(--s-1); box-shadow: var(--shadow-dark);
  animation: rise var(--dur-enter) var(--ease-enter);
}
.lb__media {
  border-radius: var(--r-lg); background: var(--lab-media);
  overflow: hidden; line-height: 0;
  transition: opacity var(--dur-base) var(--ease);
}
/* 982 — это 990 минус поле рамки. По высоте оставлено место на
   таблетку со стрелками и на воздух сверху.                        */
.lb__media img, .lb__media video {
  display: block; width: auto; height: auto;
  max-width: min(982px, 100%);
  max-height: calc(100vh - 200px);
}

/* Переход в работу поверх изображения. */
.lb__go { position: absolute; right: var(--s-5); bottom: var(--s-5); z-index: 2 }
.lb__go[hidden] { display: none }
@media (max-width: 767px) {
  .lb__go { right: var(--s-3); bottom: var(--s-3) }
  .lb__media img, .lb__media video { max-height: calc(100vh - 160px) }
}
.modal.is-swapping .lb__media { opacity: 0 }
/* Крестик на тёмном — светлый. */
.lb__frame .modal__close { color: var(--lab-ink) }
.lb__frame .modal__close:hover { background: rgba(255, 255, 255, .1) }

/* ---------- Модалка резюме ----------
   По вайрфрейму автора: два листа рядом, подпись над каждым, поле
   почты во всю ширину под ними.

   Лист берёт пропорции А4 и стоит на той же подложке, что все
   изображения на сайте. Отдельной породы карточек не заводим.     */
.cvm { width: min(560px, 100%) }
.cvm .section__label { margin-bottom: var(--gap-label); padding-right: var(--s-8) }

.cvm__pick { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-card) }
/* display у флекса и грида сильнее, чем hidden из браузерных стилей.
   Без этого правила выбор и форма остаются на экране под галочкой. */
/* Стопка состояний. Оба ребёнка стоят в одной ячейке, поэтому
   высота держится по самому высокому и не скачет. */
.cvm__stack { display: grid }
.cvm__state, .cvm__done { grid-area: 1 / 1 }
.cvm__state, .cvm__done, .cvm .section__label {
  transition: opacity var(--dur-base) var(--ease);
}
/* Спрятанное состояние остаётся в потоке и держит высоту. */
.cvm.is-sent .cvm__state,
.cvm:not(.is-sent) .cvm__done,
.cvm.is-sent .section__label { opacity: 0; visibility: hidden; pointer-events: none }
.cvm__card { display: block; text-decoration: none; color: inherit }
.cvm__sheet {
  position: relative; display: block;
  padding: var(--s-1); line-height: 0;
  border-radius: var(--r-lg);
  background: var(--plate); box-shadow: var(--shadow-card);
  transition: transform var(--dur-enter) var(--ease-enter);
}
.cvm__card:hover .cvm__sheet { transform: translateY(-4px) }
.cvm__sheet img {
  display: block; width: 100%; aspect-ratio: 796 / 1126;
  border-radius: var(--r-md); object-fit: cover; object-position: top;
  background: var(--surface); box-shadow: inset 0 0 0 1px var(--hairline);
}
/* Затемнение поверх листа. Резюме — светлый документ с мелким
   текстом, и подпись на нём иначе не читается. Слой ровный, а не
   градиентом: подпись стоит по центру, и градиент от низа её бы
   не накрыл.                                                      */
.cvm__sheet::after {
  content: ""; position: absolute; inset: var(--s-1);
  border-radius: var(--r-md);
  background: rgba(0, 0, 0, .48);
  transition: background-color var(--dur-base) var(--ease);
}
.cvm__card:hover .cvm__sheet::after { background: rgba(0, 0, 0, .58) }
.cvm__label {
  position: absolute; z-index: 1; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0 var(--s-4); text-align: center;
  font-size: var(--text-body); line-height: var(--text-body-lh);
  font-weight: var(--text-strong-w); letter-spacing: var(--text-body-ls);
  /* Цвет на затемнении, а не цвет темы: под подписью всегда тёмное,
     каким бы ни был фон страницы.                                  */
  color: var(--lab-ink);
}

.cvm__mail { display: flex; gap: var(--s-2); margin-top: var(--gap-text) }
/* Контур полем сделан внутренней тенью, как у второстепенной кнопки:
   с настоящим border поле вырастает выше кнопки рядом. */
.cvm__input {
  flex: 1 1 auto; min-width: 0;
  padding: var(--s-2) 14px;
  border: 0; border-radius: var(--r-sm);
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: var(--text-btn); line-height: var(--text-btn-lh);
  box-shadow: inset 0 0 0 1px var(--btn-border);
  transition: box-shadow var(--dur-fast) var(--ease);
}
.cvm__input::placeholder { color: var(--muted) }
.cvm__input:focus { outline: 0; box-shadow: inset 0 0 0 2px var(--focus) }
.cvm__input.is-bad { box-shadow: inset 0 0 0 2px #E5484D }
.cvm__note {
  margin: var(--s-3) 0 0;
  font-size: var(--text-meta); line-height: var(--text-meta-lh); color: var(--muted);
}

/* Состояние «отправлено». Высота примерно равна высоте выбора,
   поэтому модалка не прыгает, когда одно сменяет другое. */
/* Содержимое стоит по центру своей ячейки, а ячейка ростом с выбор. */
.cvm__done {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: var(--s-6) 0;
}
.cvm__tick { display: block; width: 48px; height: 48px; margin: 0 auto var(--s-5); color: var(--ink) }
.cvm__tick svg { display: block; width: 48px; height: 48px }
.cvm__done-h {
  margin: 0; color: var(--ink);
  font-size: var(--text-lead); line-height: var(--text-lead-lh);
  font-weight: var(--text-lead-w); letter-spacing: var(--text-lead-ls);
}
/* Адрес переносится целиком, а не рвётся посередине. */
.cvm__done-h b { font-weight: inherit; white-space: nowrap }
.cvm__done-p {
  max-width: 34ch; margin: var(--s-3) auto 0; color: var(--muted);
}

/* ---------- Модалка резюме на телефоне ----------
   Шторка снизу вместо панели по центру. Вертикальные листы в две
   колонки на 375 давали панель в 780 пикселей при экране 812:
   она упиралась в оба края, и было непонятно, есть ли что-то ниже.

   Листы легли в одну колонку и стали горизонтальными. Пропорция
   16:9 показывает шапку резюме, имя и контакты — по ней документ
   и узнают. Высота панели при этом падает примерно вдвое.        */
@media (max-width: 767px) {
  #cvmodal { align-items: end; padding: 0 }
  #cvmodal .modal__panel {
    width: 100%; max-width: none;
    max-height: 88vh;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    padding: var(--s-6) var(--page-pad) var(--s-8);
    /* Выезд делает та же rise, что у обычной модалки. Сдвиг на всю
       высоту панели был бы красивее, но если анимация почему-то не
       стартует, панель остаётся за нижним краем экрана и человек
       видит пустоту. Проверено, такое бывает. */
    animation: rise var(--dur-enter) var(--ease-enter);
  }

  .cvm__pick { grid-template-columns: 1fr; gap: var(--s-4) }
  .cvm__sheet img { aspect-ratio: 16 / 9 }
  /* Подпись на горизонтальном листе занимает всю его площадь, как
     и на вертикальном, поэтому правило не меняется. */

  .cvm__mail { flex-wrap: wrap }
  .cvm__mail .btn { width: 100% }
}

/* ---------- Сайдбар страницы работы ----------
   Фото автора слева, возврат на главную. Раньше на страницах работ
   вместо него была строка «Все работы» сверху, и кейс Borrow с его
   сайдбаром выглядел отдельной породой страниц.

   Ниже 1280 сайдбар уходит: места слева не остаётся. Возврат тогда
   живёт строкой в конце страницы, как и на Borrow.                */
.toc {
  position: fixed; top: var(--rail-sticky); z-index: 30;
  left: min(var(--toc-left), calc((var(--layout-w, 100vw) - var(--col)) / 2 - 224px));
  width: 200px;
}
.toc__home { display: block; width: 30px; height: 30px; margin: 0 var(--s-4) var(--s-5) }
.toc__home img, .toc__home .ph {
  display: block; width: 30px; height: 30px;
  object-fit: cover; background: var(--hover);
  border-radius: var(--r-sm);
  -webkit-mask-image: var(--squircle); -webkit-mask-size: 100% 100%;
  mask-image: var(--squircle); mask-size: 100% 100%;
}
@media (max-width: 1279px) { .toc { display: none } }

/* Переключатели языка и темы встают в правый верхний угол колонки,
   на базовую линию заголовка. */
.page-switches { position: absolute; top: 0; right: 0; padding-top: 0 }
.section--head { position: relative }
@media (max-width: 1279px) {
  .page-switches { position: static; margin-bottom: var(--s-6) }
}

/* Возврат в конце страницы. Дублирует сайдбар для тех ширин, где
   его нет, и заодно закрывает страницу. */
.work-next { margin-top: var(--gap-section); padding-bottom: var(--s-8) }

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

   Курсор подсказывает, что кадр кликабельный. Без него приближение
   находят только те, кто привык его пробовать.                    */
.is-zoomable .plate__in img,
.is-zoomable .plate__in video,
.seam__in img, .seam__in video { cursor: zoom-in }

.zoom {
  position: fixed; inset: 0; z-index: 70;
  display: grid; place-items: center;
  padding: var(--s-7);
}
.zoom[hidden] { display: none }
.zoom__backdrop {
  position: absolute; inset: 0;
  background: var(--lab); opacity: .82;
  animation: fade var(--dur-enter) var(--ease-enter);
}
.zoom__stage {
  position: relative; z-index: 1; line-height: 0;
  animation: rise var(--dur-enter) var(--ease-enter);
}
.zoom__stage img, .zoom__stage video {
  display: block;
  max-width: min(1280px, 100%);
  max-height: calc(100vh - var(--s-9));
  width: auto; height: auto;
  border-radius: var(--r-md);
}
/* Крестик стоит в углу экрана, а не панели: панель здесь по размеру
   кадра, и на узком кадре крестик наезжал бы на изображение. */
.zoom__close {
  position: absolute; z-index: 2; top: var(--s-5); right: var(--s-5);
  color: var(--lab-ink);
}
.zoom__close:hover { background: rgba(255, 255, 255, .1) }

@media (max-width: 767px) {
  .zoom { padding: var(--s-4) }
  .zoom__stage img, .zoom__stage video { max-height: calc(100vh - var(--s-8)) }
  .zoom__close { top: var(--s-3); right: var(--s-3) }
}

/* ---------- Ссылки подвала ----------
   По ревью «скучное перечисление ссылок». Сделаны строками с
   разделителями и стрелкой, как в архиве, а не рядом слов.        */
.foot__links {
  display: grid; gap: 0; justify-content: stretch;
  max-width: 320px; margin: 0 auto; text-align: left;
}
.foot__links a {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--s-3) var(--s-2); text-decoration: none;
  border-top: 1px solid rgba(255, 255, 255, .11);
  transition: background-color var(--dur-base) var(--ease);
}
.foot__links a:last-child { border-bottom: 1px solid rgba(255, 255, 255, .11) }
.foot__links a::after { content: "↗"; color: var(--lab-muted) }
.foot__links a:hover { background: rgba(255, 255, 255, .06) }


/* ---------- Знак, который зависит от темы ----------
   Блок стоит в конце файла намеренно. У .logo img и .cpath__logo img
   та же специфичность, что у img.on-dark, и при равной специфичности
   решает порядок. Стоя выше, правило проигрывало и в блоке «Опыт»
   показывались оба знака сразу.
   Тёмный квадрат с белой монограммой на светлой теме, белый квадрат
   с тёмной монограммой на тёмной. Одиночный тёмный знак сливался
   и с тёмным фоном строки, и с тёмной подложкой в блоке «Опыт»,
   и в липкой подписи компании.

   Переключение и по явному выбору темы, и по системной настройке,
   как у цветовых токенов.

   Селекторы с тегом: у правил вроде .logo img специфичность выше,
   и без тега спрятанный знак всё равно показывался.              */
img.on-dark { display: none }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) img.on-light { display: none }
  :root:not([data-theme="light"]) img.on-dark { display: block }
}
:root[data-theme="dark"] img.on-light { display: none }
:root[data-theme="dark"] img.on-dark { display: block }
