/* ============================================================
   proektone — стили сайта
   Golos Text подключён локально, палитра построена от логотипа
   ============================================================ */

/* --- Шрифт ------------------------------------------------ */

@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('assets/fonts/golos-cyrillic-ext.woff2') format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('assets/fonts/golos-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('assets/fonts/golos-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Golos Text';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('assets/fonts/golos-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Токены ----------------------------------------------- */

:root {
  --bg: #121212;
  --bg-deep: #0C0C0C;
  --surface: #1A1A1A;
  --surface-raised: #232323;
  --tray: rgba(255, 255, 255, 0.035);

  --hairline: rgba(255, 255, 255, 0.09);
  --hairline-strong: rgba(255, 255, 255, 0.18);

  --text: #F5F4F1;
  /* Серых ступеней было две — #9C9A94 и #8A887F. Между собой они давали
     1,26:1, то есть на глаз один цвет: различие несли кегль и начертание,
     а не он. Развести их нельзя — обе сидят на одном тёмном фоне, и любая
     пара с запасом по AA укладывается в 1,7:1. Поэтому ступень одна,
     а уровень задают размер и регистр, как и было на деле */
  --text-muted: #9C9A94;

  --accent: #E0A44C;
  --accent-soft: rgba(224, 164, 76, 0.12);
  --accent-line: rgba(224, 164, 76, 0.35);

  --shell: 1280px;
  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --section-y: clamp(6rem, 12vh, 10rem);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-press: cubic-bezier(0.32, 0.72, 0, 1);

  --r-lg: 24px;
  --r-md: 18px;
  --r-sm: 12px;

  color-scheme: dark;
}

/* --- Сброс ------------------------------------------------ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Golos Text', 'Segoe UI', system-ui, sans-serif;
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
p,
figure,
dl,
dd {
  margin: 0;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

::selection {
  background: var(--accent);
  color: #121212;
}

/* Полоса прокрутки. По умолчанию она широкая и светлая, и на тёмной странице
   оказывается самым ярким пятном в кадре. Здесь от неё остаётся тонкая
   приглушённая черта, которая становится заметнее только под курсором */
* {
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.13) transparent;
}

::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.13);
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.24);
}

/* Указатель фокуса показываем, только когда человек взялся за клавиатуру.
   Признак html.kbd ставит скрипт по первой же табуляции и снимает по касанию:
   в Safari на айфоне :focus-visible срабатывает и после касания — при выборе
   проекта по карточке разливалось янтарное выделение, которого там не место.
   Собственное кольцо показывается по признаку, а стандартное браузерное
   снимается всегда, иначе на касании осталось бы оно */
:focus-visible {
  outline: none;
}

html.kbd :focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* --- Фоновая атмосфера ------------------------------------ */

.ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}

.ambient::before {
  content: '';
  position: absolute;
  top: -30vh;
  right: -15vw;
  width: 70vw;
  height: 70vw;
  max-width: 900px;
  max-height: 900px;
  background: radial-gradient(circle, rgba(224, 164, 76, 0.07) 0%, rgba(224, 164, 76, 0) 62%);
}

.ambient::after {
  content: '';
  position: absolute;
  bottom: -25vh;
  left: -20vw;
  width: 60vw;
  height: 60vw;
  max-width: 760px;
  max-height: 760px;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0) 65%);
}

.grain {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* --- Каркас ----------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

main {
  position: relative;
  z-index: 2;
}

.section {
  padding-block: var(--section-y);
}

.section--tight {
  padding-block: clamp(4rem, 8vh, 6rem);
}

.rule {
  height: 1px;
  background: var(--hairline);
  border: 0;
  margin: 0;
}

/* --- Надзаголовок ----------------------------------------- */

.eyebrow {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.6em;
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  color: var(--text-muted);
}

.eyebrow::before {
  content: '';
  flex: none;
  width: 24px;
  height: 1px;
  /* держит чёрточку на оптической середине первой строки, когда текст переносится */
  margin-top: 0.75em;
  background: var(--accent-line);
}

.section__head {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  margin-bottom: clamp(3rem, 6vw, 5rem);
}

.section__title {
  font-size: clamp(2rem, 1.3rem + 3vw, 3.25rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.025em;
  max-width: 20ch;
}

/* Для заголовков, которые должны держаться одной строкой */
.section__title--wide {
  max-width: none;
}

/* --- Навигация -------------------------------------------- */

/* Ширина задана, а не набрана содержимым. Иначе капсула меряется по длине
   пунктов, и при переключении языка она прыгает: «Как это работает» длиннее,
   чем «How it works». Фиксированная ширина держит шапку одинаковой в обоих
   языках, а лишнее место уходит в воздух вокруг пунктов.

   Сетка 1fr auto 1fr, а не флексбокс: у боковых блоков разная ширина — знак
   бренда 127 px, переключатель языков 91, — и при простом выравнивании пункты
   съезжали с середины на два десятка пикселей. Крайние колонки равны по
   построению, поэтому средняя стоит ровно по центру капсулы */
.nav {
  position: fixed;
  top: 1.5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
  /* Шапка встаёт ровно над колонкой содержимого: та же ширина, что у секций
     страницы за вычетом их полей. Кромки капсулы и кромки текста совпадают,
     и шапка перестаёт выглядеть отдельной деталью поверх сайта */
  width: min(calc(100vw - 2rem), calc(var(--shell) - 2 * var(--gutter)));
  /* Поля симметричны намеренно: при разных левом и правом средняя колонка
     сетки съезжает с середины капсулы на половину разницы. Знаку бренда
     нужен отступ побольше — он берёт его своим полем, а не полем шапки */
  padding: 0.7rem 1.1rem;
  border: 1px solid transparent;
  border-radius: 999px;
  /* Наверху страницы капсулы нет: шапка висит прямо над сценой и не режет её
     пополам. Она собирается, как только страница тронулась с места, — тогда
     под ней уже идёт текст, и подложка нужна, чтобы пункты читались */
  background: transparent;
  box-shadow: none;
  transition: background-color 320ms var(--ease), border-color 320ms var(--ease),
    box-shadow 320ms var(--ease), backdrop-filter 320ms var(--ease);
}

/* Стекло вернулось и на телефон. Рывки при прокрутке давали три причины
   разом: размытие подложки, скачки высоты из-за dvh и проявление блоков.
   Две последние вылечены, поэтому размытие можно оставить — но радиус
   на сенсорных экранах вдвое меньше: двадцать пикселей заставляли Safari
   пересчитывать слишком большую область каждый кадр */
.nav.is-stuck {
  border-color: var(--hairline);
  background: rgba(18, 18, 18, 0.78);
  backdrop-filter: blur(10px) saturate(130%);
  -webkit-backdrop-filter: blur(10px) saturate(130%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

@media (hover: hover) and (pointer: fine) {
  .nav.is-stuck {
    background: rgba(18, 18, 18, 0.72);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
  }
}

/* flex: none обязателен — иначе картинка сжимается как flex-элемент,
   высота остаётся прежней, и знак растягивается по горизонтали */
.nav__logo {
  grid-column: 1;
  justify-self: start;
  margin-left: 0.4rem;
  flex: none;
  display: flex;
  /* сам знак 22 px по высоте, поле добирает цель нажатия до нормы в 24 */
  padding-block: 4px;
}

.nav__logo img {
  flex: none;
  /* Высота считана от оптики, а не от прежнего числа: у знака есть выносной
     элемент у «p», и плотная часть букв занимает 57 % полотна против 70 %
     у прежнего. При равной высоте буквы казались бы мельче, поэтому 22 → 27:
     ширина при этом почти не изменилась, 128 px против 127, и шапка не поехала */
  height: 27px;
  width: auto;
  opacity: 0.95;
}

.nav__links {
  grid-column: 2;
  display: flex;
  align-items: center;
  gap: 0.2rem;
  font-size: 1rem;
  color: var(--text-muted);
}

/* Наведение подсвечивает пункт капсулой — тем же приёмом,
   что и активный язык в переключателе справа */
.nav__links a {
  display: block;
  padding: 0.55rem 1.05rem;
  border-radius: 999px;
  white-space: nowrap;
  transition: color 240ms var(--ease), background-color 240ms var(--ease);
}

.nav__links a:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.08);
}

/* Заголовков секций на странице нет — ориентир даёт шапка:
   пункт текущей секции горит акцентным цветом */
.nav__links a[aria-current='true'] {
  color: var(--accent);
  background: var(--accent-soft);
}

.lang {
  grid-column: 3;
  justify-self: end;
  flex: none;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.08);
  transition: background-color 320ms var(--ease);
  font-size: 0.8125rem;
  font-weight: 500;
}

.lang a {
  padding: 0.45rem 0.85rem;
  border-radius: 999px;
  color: var(--text-muted);
  transition: color 240ms var(--ease), background-color 240ms var(--ease);
}

.lang a:hover {
  color: var(--text);
}

/* Активный язык — белой пилюлей, чтобы блок читался переключателем,
   а не парой ссылок, в том числе над сценой */
.lang a[aria-current='true'] {
  background: var(--text);
  color: #121212;
  transition: background-color 320ms var(--ease), color 320ms var(--ease);
}

/* --- Боковое меню ------------------------------------------ */

/* Ниже 860 px пять пунктов в капсулу не влезают, как их ни ужимай:
   либо они мельчают до нечитаемого, либо шапка занимает две трети экрана.
   Поэтому они уезжают в ящик слева, а в шапке остаётся знак меню */
.nav__burger {
  grid-column: 1;
  justify-self: start;
  display: none;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 240ms var(--ease), background-color 240ms var(--ease);
}

.nav__burger:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.08);
}

.nav__burger svg {
  width: 20px;
  height: 20px;
}

/* dialog от браузера прижат к блочным краям, а по инлайновым у него
   margin: auto — обнуляем левый, и ящик встаёт к левому краю экрана */
.drawer {
  inset-block: 0;
  width: min(19rem, 82vw);
  max-width: none;
  height: 100svh;
  max-height: none;
  margin: 0 auto 0 0;
  padding: 1.25rem 1.25rem 1.75rem;
  border: 0;
  border-right: 1px solid var(--hairline);
  background: var(--surface);
  color: var(--text);
  overflow-y: auto;
  /* display включается вместе с [open] и гасится обратно правилом браузера.
     Пока он стоял здесь, закрытый ящик оставался размеченным и висел слева
     за кромкой экрана, а на закрытии выпадал из верхнего слоя рывком вместо
     того, чтобы уехать плавно. display в списке переходов с allow-discrete,
     поэтому уход остаётся плавным */
  flex-direction: column;
  gap: 1.5rem;

  transform: translateX(-100%);
  transition: transform 320ms var(--ease), overlay 320ms allow-discrete,
    display 320ms allow-discrete;
}

.drawer[open] {
  display: flex;
  transform: none;
}

@starting-style {
  .drawer[open] {
    transform: translateX(-100%);
  }
}

.drawer::backdrop {
  background: rgba(7, 7, 7, 0.78);
  opacity: 0;
  transition: opacity 320ms var(--ease), overlay 320ms allow-discrete,
    display 320ms allow-discrete;
}

.drawer[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .drawer[open]::backdrop {
    opacity: 0;
  }
}

.drawer__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.drawer__logo {
  height: 25px;
  width: auto;
  opacity: 0.95;
}

.drawer__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 240ms var(--ease);
}

.drawer__close:hover {
  color: var(--text);
}

.drawer__close svg {
  width: 20px;
  height: 20px;
}

/* Список забирает свободную высоту, кнопка остаётся внизу: иначе на
   высоком экране она висит сразу под пунктами, а под ней полэкрана пустоты */
.drawer__links {
  flex: 1 1 auto;
  display: grid;
  align-content: start;
  gap: 0.25rem;
}

.drawer__links a {
  display: block;
  padding: 0.75rem 0.9rem;
  border-radius: 12px;
  color: var(--text-muted);
  font-size: 1.125rem;
  transition: color 240ms var(--ease), background-color 240ms var(--ease);
}

.drawer__links a:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}

/* Призыв к действию на сайте один, и в меню он под рукой:
   иначе до него нужно пролистать всю страницу */
.drawer__cta {
  flex: none;
  justify-content: space-between;
}

/* --- Кнопка ----------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.5rem 0.5rem 0.5rem 1.6rem;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--text);
  color: #121212;
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 500;
  cursor: pointer;
  transition: transform 420ms var(--ease-press), background-color 420ms var(--ease);
}

.btn:active {
  transform: scale(0.98);
}

.btn__icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  background: rgba(18, 18, 18, 0.1);
  transition: transform 420ms var(--ease-press), background-color 420ms var(--ease);
}

.btn:hover .btn__icon {
  transform: translate(3px, -3px);
  background: rgba(18, 18, 18, 0.18);
}

.btn__icon svg {
  width: 15px;
  height: 15px;
}

.btn--ghost {
  background: transparent;
  border-color: var(--hairline-strong);
  color: var(--text);
}

.btn--ghost .btn__icon {
  background: rgba(255, 255, 255, 0.07);
}

.btn--ghost:hover .btn__icon {
  background: rgba(255, 255, 255, 0.13);
}

/* --- Первый экран ----------------------------------------- */

/* svh, а не dvh. В Safari на айфоне при прокрутке прячется адресная строка,
   dvh тут же меняется, у холста срабатывает наблюдатель размера, буфер
   пересоздаётся и очищается — планеты моргали на каждом движении пальца.
   svh считает высоту так, будто строка на месте всегда, и не меняется */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding-block: clamp(8rem, 18vh, 11rem) clamp(4rem, 10vh, 7rem);
}

/* Сцена «планетная система» за первым экраном.
   Небо рисует контейнер, canvas прозрачный. Звезда в кадре смещена вправо,
   и градиент повторяет это смещение — иначе подсветка не совпадёт со звездой. */
.hero__sky {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: radial-gradient(78% 60% at 76% 46%, #171c27 0%, #10131b 48%, #0b0c10 100%);
}

.hero__sky canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Холст занимает первый экран целиком. Прежде он был сдвинут вправо и шире
   кадра, и лишнее срезал overflow — но система и без того выходит за свой
   холст на 15 % ширины, так что срезалось много: на 1512 px за правым краем
   оставалась четверть внешней орбиты. Теперь положение системы внутри кадра
   задаётся долями в разметке, а холсту хитрости не нужны */

/* Приглушает сцену под текстом слева и сводит нижнюю кромку к фону страницы,
   чтобы первый экран переходил в остальной сайт без шва */
.hero__sky::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(18, 18, 18, 0.96) 0%, rgba(18, 18, 18, 0.88) 32%, rgba(18, 18, 18, 0.3) 58%, rgba(18, 18, 18, 0) 82%),
    linear-gradient(180deg, rgba(18, 18, 18, 0.85) 0%, rgba(18, 18, 18, 0) 24%, rgba(18, 18, 18, 0) 58%, var(--bg) 100%);
}

.hero__grid {
  position: relative;
  z-index: 1;
  display: grid;
  gap: clamp(2.5rem, 6vw, 5rem);
  width: 100%;
}

.hero__title {
  font-size: clamp(2.75rem, 1.2rem + 7vw, 6.5rem);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: -0.035em;
  margin-block: 0;
}

.hero__title span {
  display: block;
}

.hero__title .accent {
  color: var(--accent);
}

.hero__lead {
  max-width: 54ch;
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
  color: var(--text-muted);
  font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.3125rem);
  line-height: 1.55;
}

/* Кнопки одной ширины: «Смотреть проекты» длиннее «Обсудить задачу»
   на 12 px, и рядом это читалось как небрежность. Колонки 1fr в контейнере
   по содержимому берут ширину большей из двух — растягивать на всю строку
   при этом не начинают */
.hero__actions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  width: max-content;
  max-width: 100%;
  gap: 0.875rem;
  margin-top: clamp(2.25rem, 4vw, 3rem);
}

/* Ниже 640 px в строку они не помещаются: две по 221 px плюс промежуток —
   это 456 px, а в английской версии и вовсе 472. Колонка их не переносит,
   поэтому на телефоне вторая кнопка вылезала за край экрана, появлялась
   вторая ось прокрутки и всё дёргалось. Здесь они встают друг под друга,
   ширину по-прежнему берут по большей из двух и остаются одинаковыми */
@media (max-width: 640px) {
  .hero__actions {
    grid-auto-flow: row;
  }
}

/* --- Как это работает -------------------------------------- */

.steps {
  display: grid;
  /* Две, а не четыре. В четыре колонки текст шага сжимался до 23–28 знаков
     в строке и ломался на три-четыре строки: читается рвано. Шире это было
     не растянуть — оболочка упирается в 1280 px, и даже там четвёртая доля
     давала 31 знак. В две колонки выходит 60–66 знаков, то есть нормальная
     строка, а четыре шага ложатся парами */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 2.75rem);
}

/* Полоса сверху у каждого шага складывается в общую линию хода,
   крупный номер держит порядок без дополнительных стрелок */
.step {
  padding-top: 1.75rem;
  border-top: 2px solid var(--accent);
}

.step__num {
  display: block;
  /* Нижняя граница была 2rem, и на телефоне цифра вставала в 32 px — вплотную
     к заголовкам секций в 33. Три кегля в трёх пикселях — не ступень шкалы,
     а дребезг. Цифра — подпись, а не заголовок, и стоит этажом ниже */
  font-size: clamp(1.75rem, 1.5rem + 1.4vw, 2.75rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--accent);
}

.step__name {
  margin-top: 1.1rem;
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.step__text {
  margin-top: 0.8rem;
  color: var(--text-muted);
  font-size: 0.9375rem;
  line-height: 1.6;
}

/* --- Стек ------------------------------------------------- */

/* Стек был одним полотном в 32 одинаковые пилюли: «CustDev» весил столько же,
   сколько «SQLite», глазу не за что было зацепиться, и процесс не отличался
   от среды исполнения. Порядок в списке и так шёл от постановки задачи
   к эксплуатации — здесь он просто назван вслух, четырьмя ориентирами */
.stack {
  display: grid;
  gap: clamp(1.75rem, 3.5vw, 2.75rem);
}

.stack__group {
  display: grid;
  gap: 0.9rem;
}

.stack__label {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 500;
  /* letter-spacing растягивает и межбуквенный, и межсловный интервал разом:
     на «Постановка задачи» и «Сборка и выкладка» пробел между словами
     раздувался заметнее самих букв. У .eyebrow — та же роль надзаголовка —
     letter-spacing нет вовсе, здесь для единообразия тоже снят */
  text-transform: uppercase;
  color: var(--text-muted);
}

/* На широком экране подпись уходит в боковую колонку: секция становится
   указателем, а не ещё одной стопкой блоков, как две соседние */
@media (min-width: 900px) {
  .stack__group {
    grid-template-columns: 13rem minmax(0, 1fr);
    align-items: start;
    gap: 2rem;
  }

  .stack__label {
    padding-top: 0.55rem;
  }
}

.stack__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.stack__list .tag {
  padding: 0.6rem 1.15rem;
  font-size: 0.9375rem;
  color: var(--text);
}

/* --- О себе ----------------------------------------------- */

.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: clamp(2rem, 6vw, 6rem);
  align-items: start;
}

.about__body {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  max-width: 62ch;
  color: var(--text-muted);
  font-size: clamp(1.0625rem, 1rem + 0.3vw, 1.1875rem);
}

.about__body strong {
  color: var(--text);
  font-weight: 500;
}

.note {
  padding: clamp(1.25rem, 2.5vw, 1.75rem) clamp(1.25rem, 2.5vw, 2rem);
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--text);
}

/* --- Проекты ---------------------------------------------- */

/* Карусель: горизонтальная прокрутка с прилипанием. Свайп, колесо
   и стрелки на клавиатуре работают силами браузера, кнопки — сверху */
.projects {
  display: flex;
  height: 100%;
  gap: clamp(2rem, 5vw, 4rem);
  overflow-x: auto;
  /* Явный hidden обязателен: при overflow-x: auto браузер сам делает
     вторую ось прокручиваемой, и лента начинает спорить со страницей */
  overflow-y: hidden;
  /* Докрутив до края, лента не передаёт прокрутку странице и не дёргает её */
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.projects::-webkit-scrollbar {
  display: none;
}

/* Лента — контейнер прокрутки, а не орган управления, поэтому `tabindex`
   с неё снят и в обход клавиатурой она не попадает: листают стрелки
   и точки, у них указатель фокуса свой */

/* Лента тянется мышью, поэтому курсор об этом и говорит */
.projects {
  cursor: grab;
}

.projects.is-dragging {
  cursor: grabbing;
  user-select: none;
}

/* Карточка проекта: слева текст, справа скриншот в край карточки.
   Порядок узлов в разметке остался прежним — раскладку задают области сетки */
.project {
  flex: 0 0 100%;
  scroll-snap-align: start;
  display: grid;
  /* Снимок лежит сверху во всю ширину карточки, текст под ним.
     Боковая полоса из образца не подошла: туда ставили макеты телефонов,
     а сюда идут широкие снимки рабочего стола — в вертикальном окне
     от них оставалась треть кадра, интерфейс резало посередине */
  /* Заголовок и факты берут ровно столько, сколько нужно тексту; снимку
     достаётся весь остаток высоты экрана. Поэтому карточка одинаково
     укладывается и в широкий монитор, и в узкий телефон, и в низкое окно —
     подстраивается панель со снимком, а не читатель прокруткой */
  grid-template-rows: minmax(4.5rem, 1fr) auto minmax(0, auto);
  grid-template-areas:
    'media'
    'head'
    'facts';
  border: 1px solid var(--hairline);
  border-radius: clamp(20px, 2vw, 28px);
  background: var(--surface);
  overflow: hidden;
}

/* Поля задаются содержимому, а не карточке: иначе скриншот не дойдёт до края */
.project__head {
  grid-area: head;
  padding: clamp(1.5rem, 2.5vw, 2.25rem) clamp(2rem, 3.5vw, 3.25rem) 0;
}

/* Подпись слева, текст справа. В три колонки строка выходила в тридцать
   знаков вместо комфортных пятидесяти, а блок был выше этого */
.project__facts {
  grid-area: facts;
  display: grid;
  align-content: start;
  gap: clamp(0.9rem, 1.6vw, 1.25rem);
  padding: clamp(1rem, 1.6vw, 1.4rem) clamp(2rem, 3.5vw, 3.25rem) clamp(1.5rem, 2.5vw, 2.25rem);
  /* Последняя линия обороны. Обычно фактам хватает места и прокрутки тут нет,
     но в совсем тесном окне — телефон в ландшафте — им некуда деваться:
     строка minmax(0, auto) даёт им сжаться, а прокрутка не даёт обрезаться.
     Панели при этом оставлен нижний предел, чтобы она не схлопнулась в ноль */
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
}

/* Класс ставит скрипт, когда ниже действительно есть текст */
.project__facts.is-clipped {
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2.25rem), transparent);
}

.fact {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: clamp(1rem, 2vw, 1.5rem);
  align-items: baseline;
}

/* Акцентная линия отмечает шов между снимком и текстом */
/* Янтарной черты под снимком больше нет: она делила карточку надвое
   и тянула взгляд на себя. Панель и без неё отделена — она темнее карточки */
.project__media {
  grid-area: media;
  min-height: 0;
  background: var(--bg-deep);
}

/* Высоту панели задаёт сам снимок. Фиксированная пропорция означала бы
   выбор между обрезкой и тёмными полями по краям; здесь нет ни того, ни другого,
   и каждый проект показывает свой кадр целиком, какой бы формы он ни был.
   min-height нужен только слайдам, где снимка ещё нет */
.project__frame {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 0.5rem;
  /* Панель занимает ровно тот остаток высоты, что оставили ей заголовок
     и факты. Своей пропорции у неё больше нет: она была бы пятым размером,
     который надо согласовывать с экраном, а так согласовывать нечего */
  height: 100%;
  min-height: 0;
  overflow: hidden;
}

.project__frame img {
  display: block;
  /* Браузер иначе принимает протаскивание за попытку утащить картинку
     и показывает её призрак вместо листания ленты */
  -webkit-user-drag: none;
  user-select: none;
}

/* --- Слайд-шоу внутри карточки ------------------------------ */

/* Панель одна и та же у всех проектов: пропорция заглавного кадра видеосвязи.
   Кадры внутри разной формы — от широких снимков рабочего стола до
   вертикальных телефонных, — и ложатся целиком через object-fit: contain.
   Обрезать ради одинаковой формы нельзя, это уже отвергалось */
.project__frame--slides {
  min-height: 0;
}

.project__frame--slides .shot {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 800ms var(--ease);
}

.project__frame--slides .shot.is-on {
  opacity: 1;
}


/* Сам кадр — целиком, по центру панели */
.shot__on {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Подложка — тот же кадр, растянутый на всю панель и размытый.
   Она нужна из-за телефонных снимков: вертикальный кадр занимает треть
   ширины панели, и без подложки по бокам оставались бы две чёрные полосы.
   Так панель залита цветом самого снимка, и кадр стоит на своём же фоне.
   Размытие тут безопасно — в отличие от подложки страницы, где оно
   сбивало мелкое зерно в пятна: у фотографии сбивать нечего.
   scale прячет прозрачные края, которые blur оставляет по периметру */
.shot__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.3);
  filter: blur(52px) saturate(1.5) brightness(0.45);
}

/* Поля у подписей, а не у рамки: иначе снимок не дошёл бы до краёв панели */
.project__slug,
.project__pending {
  padding-inline: clamp(1.25rem, 2.5vw, 2rem);
}

.project__pending {
  padding-bottom: clamp(1.25rem, 2.5vw, 2rem);
}

/* --- Снимок крупно ------------------------------------------ */

/* Панель в карточке маленькая: на ней видно, что за экран, но не видно
   подписей. Нажатие открывает снимок во весь экран — курсор об этом говорит */
[data-zoom-open] {
  cursor: zoom-in;
}

/* На телефоне курсора нет, и увеличение было не обнаружить: знак лупы
   в углу панели говорит о нём на любом устройстве */
/* Четыре уголка вместо лупы. Лупа с плюсом внутри — это три фигуры в знаке
   размером с ноготь: в него всматривались, вместо того чтобы понимать сразу.
   Уголки читаются как «развернуть» и состоят из одних линий */
[data-zoom-open]::after {
  content: '';
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 3;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23F5F4F1' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5'/%3E%3C/svg%3E")
    center / 16px 16px no-repeat,
    rgba(7, 7, 7, 0.5);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
}

/* Закрыв зум, браузер возвращает фокус на панель и обводит её янтарной рамкой
   во всю ширину карточки — то же, что было со строками списка. Вместо рамки
   по периметру — акцентная черта по нижней кромке панели */
[data-zoom-open]:focus-visible {
  outline: none;
}

html.kbd [data-zoom-open]:focus-visible::before{
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  z-index: 3;
  background: var(--accent);
}

[data-zoom-open]:hover .shot,
html.kbd [data-zoom-open]:focus-visible .shot{
  filter: brightness(1.08);
}

/* Лист поверх карточки проекта. Диалог внутри диалога — оба лежат в верхнем
   слое, и порядок там по времени открытия, так что зум всегда сверху */
.zoom {
  inset: 0;
  width: 100%;
  height: 100svh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: clamp(2.5rem, 6vh, 4rem) clamp(3rem, 7vw, 5rem);
  border: 0;
  background: #070707;
  color: var(--text);
  overflow: hidden;

  opacity: 0;
  transform: scale(0.965);
  transition: opacity 280ms var(--ease), transform 280ms var(--ease),
    overlay 280ms allow-discrete, display 280ms allow-discrete;
}

.zoom[open] {
  opacity: 1;
  transform: none;
}

@starting-style {
  .zoom[open] {
    opacity: 0;
    transform: scale(0.965);
  }
}

/* Подложка гасла рывком: без перехода она появляется и исчезает мгновенно,
   и всё окно читалось как склейка, даже когда само оно плыло */
.zoom::backdrop {
  background: #070707;
  opacity: 0;
  transition: opacity 280ms var(--ease), overlay 280ms allow-discrete,
    display 280ms allow-discrete;
}

.zoom[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .zoom[open]::backdrop {
    opacity: 0;
  }
}

/* Лента снимков: прилипание и свайп делает браузер, здесь только рамки */
.zoom__track {
  width: 100%;
  height: 100%;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  cursor: grab;
}

.zoom__track::-webkit-scrollbar {
  display: none;
}

/* Янтарная рамка по периметру была бы во весь экран: лента — контейнер
   прокрутки, а не орган управления. Но совсем без указателя нельзя — она
   в цепочке табуляции, поэтому рамка врезана внутрь кромки */
html.kbd .zoom__track:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.zoom__track.is-dragging {
  cursor: grabbing;
  user-select: none;
}

/* Флексбокс, а не грид: у грид-элемента процентная высота меряется от строки,
   а строка растёт по содержимому — снимок выходил в натуральную величину
   и торчал за экран. У флекс-элемента containing block — сам слайд */
.zoom__slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  overflow: hidden;
  margin: 0;
}

/* Пропорция кадра важнее размера: object-fit: contain вписывает снимок целиком
   и никогда его не тянет. Через max-width/max-height не вышло: слайд —
   грид-контейнер, и процент упирался в высоту строки по содержимому,
   а не в высоту слайда, поэтому снимок выходил в натуральную величину */
.zoom__slide img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-user-drag: none;
  user-select: none;
}

.zoom__close {
  position: absolute;
  top: clamp(0.5rem, 2vh, 1rem);
  right: clamp(0.5rem, 2vw, 1.25rem);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 240ms var(--ease), transform 300ms var(--ease-press);
}

.zoom__close:hover,
html.kbd .zoom__close:focus-visible{
  color: var(--accent);
}

.zoom__close:hover {
  transform: rotate(90deg);
}

/* Тот же случай, что у стрелок: смена цвета давала 1,28:1 между покоем
   и фокусом. Кольцо по кромке кнопки, поворот оставлен наведению */
html.kbd .zoom__close:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 999px;
}

.zoom__close svg {
  width: 24px;
  height: 24px;
}

/* Стрелки прижаты к кромкам экрана: в поле, которое оставляют поля диалога,
   они не наезжают на снимок */
.zoom__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 240ms var(--ease), opacity 240ms var(--ease);
}

.zoom__arrow--prev {
  left: clamp(0.25rem, 1.5vw, 1.25rem);
}

.zoom__arrow--next {
  right: clamp(0.25rem, 1.5vw, 1.25rem);
}

.zoom__arrow:hover:not(:disabled) {
  color: var(--accent);
}

/* Одной сменой цвета фокус тут не показать: покой и акцент различаются
   на 1,62:1 при норме 3:1, да и от наведения такой фокус не отличить.
   Поэтому кольцо — стрелка размером с палец, рамка ей к лицу */
html.kbd .zoom__arrow:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 999px;
  color: var(--accent);
}

.zoom__arrow:disabled {
  opacity: 0.25;
  cursor: default;
}

.zoom__arrow svg {
  width: 22px;
  height: 22px;
}

/* Указатель листания стоит у нижней кромки окна, над подсказкой. Раньше он
   гонялся за нижней кромкой самого кадра — на десктопе снимок занимает почти
   весь экран и приём работал, а на телефоне широкий снимок вписывается тонкой
   полосой посреди экрана, и точки уезжали в середину. Место должно быть
   предсказуемым при любой форме снимка */
.zoom__dots {
  position: absolute;
  left: 50%;
  bottom: calc(clamp(0.5rem, 1.5vh, 1rem) + 1.9rem);
  transform: translateX(-50%);
  z-index: 2;
  display: flex;
  gap: 0.1rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: rgba(7, 7, 7, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.zoom__dot {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

/* Отметка внутри точки всего 6 px, и у текущего кадра она уже акцентная —
   сменой её цвета фокус не показать. Кольцо берёт всю кнопку в 26 px */
html.kbd .zoom__dot:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: 999px;
}

html.kbd .zoom__dot:focus-visible::before{
  background: var(--text);
}

.zoom__dot::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--hairline-strong);
  transition: background-color 240ms var(--ease), transform 240ms var(--ease);
}

.zoom__dot:hover::before {
  background: var(--text-muted);
}

.zoom__dot[aria-current='true']::before {
  background: var(--accent);
  transform: scale(1.25);
}

/* Подсказка нужна: слайд-шоу здесь остановлено, и листать человек должен сам.
   Способ зависит от устройства, поэтому строк две и показывается одна */
.zoom__hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(0.5rem, 1.5vh, 1rem);
  margin: 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.75rem;
}

.zoom__hint--touch {
  display: none;
}

@media (hover: none) {
  .zoom__hint--pointer {
    display: none;
  }

  .zoom__hint--touch {
    display: inline;
  }

  [data-zoom-open] {
    cursor: default;
  }
}

@media (prefers-reduced-motion: reduce) {
  .zoom,
  .zoom::backdrop {
    transition: none;
  }
}

/* --- Список проектов на главной ----------------------------- */

/* Строка несёт не только название, но и повод: без него список
   прятал бы за клик самый сильный аргумент — отвергнутые ценники */
.plist {
  display: grid;
  border-top: 1px solid var(--hairline);
}

.plist li {
  border-bottom: 1px solid var(--hairline);
}

.plist__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 1.75rem);
  width: 100%;
  padding: clamp(1.1rem, 2.2vw, 1.6rem) clamp(0.5rem, 1.5vw, 1rem);
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color 300ms var(--ease);
}

.plist__row:hover,
html.kbd .plist__row:focus-visible{
  background: rgba(255, 255, 255, 0.03);
}

/* После Esc браузер возвращает фокус на строку, с которой открыли карточку,
   и обводит её рамкой во всю ширину — выглядит как случайное выделение.
   Рамка заменена акцентной чертой по левому краю: она заметна (черта на фоне
   даёт 8,55:1 при норме 3:1), но блок не обводит */
.plist__row {
  position: relative;
}

.plist__row:focus-visible {
  outline: none;
}

html.kbd .plist__row:focus-visible::before{
  content: '';
  position: absolute;
  left: 0;
  top: 0.5rem;
  bottom: 0.5rem;
  width: 3px;
  border-radius: 999px;
  background: var(--accent);
}

html.kbd .plist__row:focus-visible .plist__num,
html.kbd .plist__row:focus-visible .plist__go {
  color: var(--accent);
}

/* Вместо значка — номер. Значок был лишней картинкой в строке, где и так
   есть название, пояснение и стрелка: он забирал место и добавлял шума.
   Номер набран тем же приёмом, что счётчик в карточке проекта, — моноширинный
   акцентный, — и держит порядок, ничего не изображая */
.plist__num {
  flex: none;
  align-self: start;
  padding-top: 0.2em;
  font-family: ui-monospace, 'Cascadia Mono', 'SF Mono', Consolas, monospace;
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  transition: color 300ms var(--ease);
}

.plist__row:hover .plist__num {
  color: var(--accent);
}

.plist__name {
  display: block;
  font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.375rem);
  font-weight: 500;
  letter-spacing: -0.02em;
}

.plist__note {
  display: block;
  max-width: 62ch;
  margin-top: 0.35rem;
  color: var(--text-muted);
  font-size: 0.9375rem;
  line-height: 1.45;
}

.plist__go {
  flex: none;
  color: var(--text-muted);
  transition: color 300ms var(--ease), transform 300ms var(--ease);
}

.plist__row:hover .plist__go {
  color: var(--accent);
  transform: translateX(3px);
}

.plist__go svg {
  display: block;
  width: 18px;
  height: 18px;
}

.plist__pages {
  display: flex;
  gap: 0.5rem;
  margin-top: clamp(1.25rem, 2.5vw, 1.75rem);
}

/* Точки вместо цифр: номер страницы ничего не сообщает, а кружок
   показывает и сколько их всего, и где ты сейчас */
.plist__page {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.plist__page::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.2);
  transition: background-color 300ms var(--ease), transform 300ms var(--ease);
}

.plist__page:hover::before {
  background: rgba(255, 255, 255, 0.4);
}

.plist__page[aria-current='true']::before {
  background: var(--accent);
  transform: scale(1.35);
}

/* --- Карточка проекта поверх страницы ----------------------- */

/* Нативный dialog: Esc, ловушка фокуса и затемнение фона — от браузера.
   Прокрутка внутри окна намеренная: снимок показывается во всю ширину
   карточки, без потолка по высоте, поэтому на низком экране лучше
   прокрутить окно, чем показать картинку с полями по бокам */
.pmodal {
  /* Ширину по высоте экрана не считаем: пробовали, и выигрыш съедался —
     чем уже карточка, тем сильнее переносится текст под снимком, а сам
     снимок падал с 858 до 666 px. Полсотни пикселей прокрутки внутри
     окна дешевле, чем мелкая картинка */
  width: min(94vw, 64rem);
  max-width: none;
  /* Высота задана, а не набрана содержимым: иначе окно сжимается до размера
     текста и снимку достаются жалкие остатки. Так панель забирает всё,
     что не занял текст, — на большом экране снимок крупный, на низком
     ужимается, но прокручивать не приходится ни там, ни там */
  height: 94dvh;
  max-height: 94dvh;
  margin: auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  /* Колонка на всю доступную высоту: карточка внутри подстраивается под экран,
     вместо того чтобы вылезать за него и требовать прокрутки.
     display включается вместе с [open] и гасится обратно правилом браузера:
     пока он стоял здесь, закрытое окно оставалось размеченным, и браузер
     грузил все снимки внутри него сразу при заходе на страницу —
     полмегабайта до первого клика. Разрывом появление не станет:
     display в списке переходов с allow-discrete */
  flex-direction: column;
  overflow: hidden;
  scrollbar-width: none;

  opacity: 0;
  transform: translateY(14px) scale(0.985);
  transition: opacity 300ms var(--ease), transform 300ms var(--ease),
    overlay 300ms allow-discrete, display 300ms allow-discrete;
}

.pmodal::-webkit-scrollbar {
  display: none;
}

.pmodal[open] {
  display: flex;
  opacity: 1;
  transform: none;
}

/* Стартовое состояние для появления: без него окно возникает рывком */
@starting-style {
  .pmodal[open] {
    opacity: 0;
    transform: translateY(14px) scale(0.985);
  }
}

/* Без backdrop-filter. Размытие превращало зерно подложки в разводы:
   шум мелкий, а блюр усредняет его пятнами. Плотного затемнения достаточно,
   и оно вдобавок не заставляет браузер перерисовывать всю страницу */
.pmodal::backdrop {
  background: rgba(7, 7, 7, 0.9);
  opacity: 0;
  transition: opacity 300ms var(--ease), overlay 300ms allow-discrete,
    display 300ms allow-discrete;
}

.pmodal[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .pmodal[open]::backdrop {
    opacity: 0;
  }
}

.pmodal .carousel {
  max-width: none;
}

/* Крестик уведён в боковое поле, за правый край карточки: там он
   ничего не загораживает. Стрелка в этом поле стоит по середине высоты,
   так что верх свободен */
.pmodal__close {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 4;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 300ms var(--ease), transform 300ms var(--ease-press);
}

.pmodal__close:hover {
  color: var(--accent);
  transform: rotate(90deg);
}

.pmodal__close svg {
  width: 22px;
  height: 22px;
}

/* --- Управление каруселью ---------------------------------- */

/* Стрелки вынесены за карточку, в боковые поля: они больше ничего
   не загораживают, и на краях их можно приглушить, а не прятать —
   макет не дёргается */
/* Ширина ограничена не ради красоты: карточку по высоте задаёт снимок,
   а он тем выше, чем шире карточка. На всю колонку в 1130 px слайд
   вырастал под 950 px и не помещался на экран ноутбука */
/* Высота передаётся вниз по цепочке: окно → карусель → лента → карточка.
   min-height: 0 на каждом звене обязателен — без него flex-элемент не даёт
   себя сжать меньше содержимого, и вся затея с подгонкой под экран рушится */
.carousel {
  width: 100%;
  max-width: 64rem;
  margin-inline: auto;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
}

/* Поле не может быть уже кнопки: иначе стрелка наползает на карточку */
.carousel__frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  padding-inline: clamp(4rem, 5vw, 4.75rem);
}

/* Ни кружка, ни обводки — только сам знак. Стрелки стоят на затемнении,
   контраста хватает, а рамка вокруг них выглядела тяжело */
.carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 300ms var(--ease), opacity 300ms var(--ease),
    transform 300ms var(--ease);
}

.carousel__arrow--prev {
  left: 0;
}

.carousel__arrow--next {
  right: 0;
}

.carousel__arrow--prev:hover:not(:disabled) {
  color: var(--accent);
  transform: translate(-3px, -50%);
}

.carousel__arrow--next:hover:not(:disabled) {
  color: var(--accent);
  transform: translate(3px, -50%);
}

.carousel__arrow:disabled {
  opacity: 0.2;
  cursor: default;
}

.carousel__arrow svg {
  width: 24px;
  height: 24px;
}

/* Отступ сверху был до 40 px и вместе с самими точками съедал у карточки
   заметную полосу внизу. Номер проекта над заголовком убран совсем: положение
   в списке показывают эти точки, дважды об этом говорить незачем */
.carousel__dots {
  flex: none;
  display: flex;
  justify-content: center;
  gap: 0.35rem;
  margin-top: clamp(0.75rem, 1.5vw, 1.25rem);
}

/* Кнопка 24 px ради пальца, точка внутри 8 px ради вида */
.carousel__dot {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.carousel__dot::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.2);
  transition: background-color 300ms var(--ease), transform 300ms var(--ease);
}

.carousel__dot:hover::before {
  background: rgba(255, 255, 255, 0.4);
}

.carousel__dot[aria-current='true']::before {
  background: var(--accent);
  transform: scale(1.35);
}

.project__slug {
  font-family: ui-monospace, 'Cascadia Mono', 'SF Mono', Consolas, monospace;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.project__pending {
  font-size: 0.8125rem;
  color: var(--text-muted);
  opacity: 0.7;
}

.project__title {
  max-width: 18ch;
  margin-top: 0.8rem;
  font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.5rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.03em;
}

.fact dt {
  display: block;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  color: var(--accent);
}

.fact dt {
  padding-top: 0.15em;
}

.fact dd {
  max-width: 62ch;
  color: var(--text-muted);
  font-size: 0.9375rem;
  line-height: 1.55;
}

.fact--result dd {
  color: var(--text);
}

.tag {
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.03);
  font-size: 0.8125rem;
  color: var(--text-muted);
  white-space: nowrap;
  transition: color 280ms var(--ease), border-color 280ms var(--ease),
    background-color 280ms var(--ease);
}

.stack__list .tag:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

/* --- Контакты --------------------------------------------- */

/* Та же ступень, что у прочих заголовков секций: роль одна, и разными
   кеглями она делилась без причины. Переносу это не вредит — вопрос и ответ
   внутри заголовка стоят отдельными блоками и ломаются каждый по себе */
.contact__title {
  font-size: clamp(2rem, 1.3rem + 3vw, 3.25rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.035em;
}

/* Вопрос набран тем же кеглем, но light: ответ под ним должен звучать твёрже */
.contact__ask {
  display: block;
  margin-bottom: 0.15em;
  font-weight: 400;
  color: var(--text-muted);
}

.contact__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  margin-top: clamp(2.5rem, 5vw, 4rem);
}

.card-copy {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  background: var(--surface);
  transition: background-color 420ms var(--ease), border-color 420ms var(--ease);
}

.card-copy:hover {
  background: var(--surface-raised);
  border-color: var(--hairline-strong);
}

/* Два действия в одной карточке: текст копирует, иконка открывает */
.card-copy__main {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform 420ms var(--ease-press);
}

.card-copy__main:active {
  transform: scale(0.99);
}

.card-copy__label {
  display: block;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  color: var(--text-muted);
}

.card-copy__value {
  display: block;
  margin-top: 0.6rem;
  font-size: clamp(1.0625rem, 1rem + 0.5vw, 1.375rem);
  font-weight: 500;
  letter-spacing: -0.015em;
}

.card-copy__hint {
  display: block;
  margin-top: 0.4rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
  transition: color 300ms var(--ease);
}

.card-copy__main.is-copied .card-copy__hint {
  color: var(--accent);
}

/* Ссылка, а не украшение: ведёт прямо в Telegram или в почтовый клиент */
.card-copy__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-muted);
  transition: background-color 420ms var(--ease), border-color 420ms var(--ease),
    color 420ms var(--ease), transform 420ms var(--ease-press);
}

.card-copy__icon:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
  transform: translate(2px, -2px);
}

.card-copy__icon svg {
  width: 17px;
  height: 17px;
}

/* --- Подвал ----------------------------------------------- */

.footer {
  position: relative;
  z-index: 2;
  padding-block: clamp(2.5rem, 5vw, 3.5rem);
  border-top: 1px solid var(--hairline);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
}

/* Зверёк ходит по верхней кромке знака — полосы во всю ширину подвала у него
   больше нет. Холст ровно по ширине знака и стоит на нём донцем: так он
   выглядит сидящим на логотипе, а не бегущим где-то рядом */
.footer__mark {
  position: relative;
  flex: none;
  display: block;
}

.footer__mark canvas {
  position: absolute;
  left: 0;
  bottom: 100%;
  width: 100%;
  /* Высота — это запас сверху, а не размер зверька: его клетку ограничивает
     параметр unit при создании сцены */
  height: 44px;
  /* Зверёк ни на что не реагирует: это фон, а не кнопка. Нажатия он
     не забирает — под ним живой знак со ссылкой */
  pointer-events: none;
}

.footer img {
  flex: none;
  height: 27px;
  width: auto;
  opacity: 0.6;
}

.footer__note {
  color: var(--text-muted);
  font-size: 0.8125rem;
}

/* --- Проявление при скролле -------------------------------- */

/* Проявление блоков при прокрутке убрано. На телефоне двадцать элементов
   с переходами по opacity и transform считались прямо во время скролла,
   и он заметно дёргался. Блоки просто на месте */
.reveal {
  opacity: 1;
  transform: none;
}

/* --- Адаптив ---------------------------------------------- */

/* Очень широкие экраны: контент не должен тонуть в полях */
@media (min-width: 1800px) {
  :root {
    --shell: 1440px;
  }
}

/* Низкий кадр — ноутбук 768p, окно в половину экрана, ландшафт телефона.
   Первый экран перестаёт требовать полную высоту, иначе заголовок
   упирается в кнопки, а прокрутка начинается с пустоты */
@media (max-height: 760px) and (min-width: 700px) {
  /* Высоту первый экран сохраняет: с min-height: auto он схлопывался по тексту
     и на ноутбуке 1512 x 734 занимал 521 px вместо 734. Под ним оставалась
     тёмная полоса в две сотни пикселей, а сцене доставался сплюснутый кадр.
     Ужимаются только поля и заголовок — этого хватает, чтобы кнопки
     не упирались в нижнюю кромку */
  .hero {
    padding-block: clamp(6rem, 14vh, 8rem) clamp(2.5rem, 6vh, 4rem);
  }

  .hero__title {
    font-size: clamp(2.25rem, 1rem + 3vw, 3.5rem);
  }

  .hero__lead {
    margin-top: clamp(1.25rem, 2vw, 1.75rem);
  }

  .hero__actions {
    margin-top: clamp(1.5rem, 2.5vw, 2rem);
  }

  .hero__sky {
    min-height: 30rem;
  }
}


@media (max-width: 1024px) {
  .about__grid {
    grid-template-columns: 1fr;
    gap: 2rem;
  }
}

/* Планшет: две колонки в проекте становятся теснее, чем читаемо */
@media (max-width: 900px) {
  /* На узком экране подпись переезжает над текстом: две колонки
     не оставляют тексту читаемой ширины */
  .fact {
    grid-template-columns: 1fr;
    gap: 0.35rem;
  }

  .fact dt {
    padding-top: 0;
  }

  /* Стрелки уступают место свайпу и точкам */
  .carousel__frame {
    padding-inline: 0;
  }

  .carousel__arrow {
    display: none;
  }

  /* Бокового поля больше нет, поэтому крестик переезжает в полосу над карточкой */
  .pmodal {
    padding-top: 3.5rem;
  }

  .steps {
    gap: clamp(1.5rem, 3vw, 2.25rem);
  }
}

@media (max-width: 860px) {
  .nav__links {
    display: none;
  }

  .nav__burger {
    display: grid;
  }

  /* Раскладка шапки здесь другая, а не сжатая настольная: знак меню слева,
     знак бренда по центру, языки справа. Логотип центрируется абсолютом —
     иначе его сдвигали бы вбок разные ширины соседей: кнопка 38 px слева
     и переключатель языков под 90 px справа */
  .nav {
    padding: 0.55rem 0.6rem;
  }

  /* Знак по центру шапки. Доля 1fr не мешает колонке вырасти под содержимое:
     пилюля языков справа шире кнопки меню слева, поэтому правая колонка
     забирала лишнее и середина съезжала. Нижняя граница в ноль заставляет
     боковые колонки делить место поровну — тогда средняя стоит ровно
     посередине, и знак вместе с ней.

     Абсолютное позиционирование тут не годится: у такого элемента сетки
     точкой отсчёта становится его ячейка, а не шапка, и центр всё равно
     оказывался смещённым */
  .nav {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  }

  .nav__logo {
    grid-column: 2;
    justify-self: center;
    margin-left: 0;
  }
}

@media (max-width: 860px) {
  .nav__logo img {
    height: 25px;
  }

  /* Палец толще курсора: у языков и знака меню высота под нажатие */
  .lang a {
    padding: 0.6rem 0.9rem;
  }
}

/* Ниже 400 px знак по центру начинал налезать на пилюлю языков: шапка узкая,
   а знак, кнопка меню и языки в сумме шире неё. Поджимаются оба края —
   знак немного по высоте и языки по горизонтали. Центр при этом остаётся
   настоящим центром, а не сдвигается «лишь бы влезло» */
@media (max-width: 400px) {
  .nav__logo img {
    height: 21px;
  }

  .lang a {
    padding: 0.55rem 0.6rem;
  }

  .lang {
    gap: 0;
    padding: 2px;
  }
}

@media (max-width: 760px) {
  .contact__grid {
    grid-template-columns: 1fr;
  }

  /* --- Первый экран на телефоне ------------------------------ */

  /* Собран иначе, чем на мониторе, а не сжат из него. На мониторе сцена
     лежит справа от колонки текста и боковой скрим гасит её только слева.
     Колонки здесь нет: текст занимает всю ширину и по вертикали от 29 до 81
     процента экрана. Ровная вуаль поверх всего кадра давала 74–88 процентов
     затемнения — сцена рисовалась, но планеты гасли до неразличимости,
     и от неё оставались одни звёзды.
     Поэтому сцена уходит в верхнюю часть экрана, а текст прижимается к низу.
     Сцена сама пересобирается под новый размер — за этим следит её
     ResizeObserver, ничего подсказывать ей не нужно */
  /* Шапка закреплена: её низ приходится на 92 px (отступ 24 плюс высота 68).
     Верхнее поле было 5rem, то есть 80 px, — заголовок начинался выше шапки
     и уезжал под неё. Пока содержимое было ниже, это не проявлялось; как
     только кнопки встали друг под друга, оно доросло до верха и наложилось */
  /* Шапка закреплена, её низ приходится на 92 px. Прижимали содержимое
     к низу через align-items: flex-end — но у флексбокса это выталкивает
     содержимое ВВЕРХ, когда места не хватает, и на коротком экране заголовок
     уезжал под шапку. Автоматическое поле делает то же прижатие, но при
     нехватке места обнуляется, и текст просто начинается от верхнего поля */
  .hero {
    align-items: flex-start;
    padding-block: 7.5rem clamp(3rem, 8vh, 5rem);
  }

  .hero__grid {
    margin-top: auto;
  }

  .hero__sky canvas {
    height: 58%;
  }

  /* Короткий телефон: там же, где заголовок начинается выше, сцена должна
     кончаться раньше — иначе звезда встаёт ровно на строку «От идеи» */
  @media (max-height: 740px) {
    .hero__sky canvas {
      height: 40%;
    }

    /* Верхнее поле тут раньше сокращалось до 3,25rem, чтобы первый экран
       укладывался в короткое окно. Но шапка закреплена и её низ приходится
       на 92 px: при таком поле заголовок начинался под ней. Лишний ход
       прокрутки — меньшая беда, чем текст под шапкой, поэтому поле держит
       ровно столько, сколько нужно, чтобы уйти из-под неё */
    .hero {
      padding-block: 7rem clamp(2.5rem, 6vh, 4rem);
    }
  }

  /* Вуаль теперь работает по вертикали: прозрачная там, где система,
     плотная там, где начинается заголовок */
  .hero__sky::after {
    background: linear-gradient(180deg,
      rgba(18, 18, 18, 0.45) 0%,
      rgba(18, 18, 18, 0.12) 20%,
      rgba(18, 18, 18, 0.34) 44%,
      rgba(18, 18, 18, 0.88) 64%,
      var(--bg) 100%);
  }

  /* --- Карточка проекта на телефоне: лист во весь экран ------ */

  /* На настольном экране карточка — окно поверх страницы, и поля вокруг неё
     показывают, что страница никуда не делась. На телефоне эти поля стоят
     дорого: 94dvh минус полоса с крестиком оставляли снимку 80 px, и от
     проекта было видно полоску. Здесь карточка занимает экран целиком —
     это и привычнее на телефоне, и отдаёт снимку всё, что не занял текст */
  .pmodal {
    /* inset и margin обнуляются вместе с размерами: у dialog от браузера
       есть собственные поля, и без них лист не доходит до кромок экрана */
    inset: 0;
    margin: 0;
    width: 100%;
    height: 100svh;
    max-height: none;
    padding-top: 3rem;
    /* Полоса с крестиком и полоса с точками — часть листа, а не просвет
       на страницу: сквозь прозрачный фон в них проглядывала шапка */
    background: var(--bg);
  }

  .carousel__frame {
    padding-inline: 0;
  }

  /* Прежде высота карточки была жёстко поделена: снимку — доля экрана,
     тексту — сжатый остаток с собственной прокруткой. На телефоне это читалось
     плохо: текст листался в тесном окошке внутри неподвижной карточки.

     Сетка тут не годится совсем. Все снимки внутри панели лежат абсолютно,
     поэтому её минимальная высота равна нулю: на коротком экране строка
     со снимком сжималась, сам снимок держал свой предел и наползал
     на заголовок — в Safari места хватало и это не проявлялось, а в Brave
     с его нижней панелью проявилось сразу.

     Колонка с запретом сжатия такого сделать не может: каждая часть занимает
     ровно свою высоту, карточка листается целиком, как обычная страница */
  .project {
    border: 0;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    -webkit-overflow-scrolling: touch;
  }

  .project__media,
  .project__head,
  .project__facts {
    flex: none;
  }

  /* Снимок идёт первым, хотя в разметке заголовок стоит раньше */
  .project__media {
    order: -1;
  }

  /* Пропорция 5:4 не годится: снимки телефона вертикальные, 1:2, и в широкой
     рамке они ужимались до полутора сотен пикселей при её ширине под четыреста.
     Высота задаётся долей окна — и в vh, а не в svh или dvh: эти две единицы
     разные браузеры на айфоне считают по-своему, а vh везде считает от полной
     высоты и при прокрутке не меняется */
  .project__frame {
    height: 54vh;
    min-height: 240px;
    max-height: 480px;
  }

  /* Внутренней прокрутки у текста больше нет — он весь на виду */
  .project__facts {
    overflow: visible;
    min-height: auto;
    padding-bottom: 1.5rem;
  }

  .project__facts.is-clipped {
    mask-image: none;
  }

  /* Поля внутри карточки тоже уже: на 390 px полтора сантиметра слева
     и справа — это треть строки */
  .project__head {
    padding-inline: 1.25rem;
  }

  .project__facts {
    padding-inline: 1.25rem;
  }

  .carousel__dots {
    margin-top: 0.4rem;
    padding-bottom: 0.25rem;
  }

  /* Крестик прижат к краю экрана, а не к краю карточки */
  .pmodal__close {
    top: 0.25rem;
    right: 0.5rem;
  }

  /* --- Прочее под палец ------------------------------------- */

  /* Были 40 px и вместе с полями съедали внизу карточки шесть десятков
     пикселей. 28 px пальцу хватает с запасом — норма 24, — а карточке
     возвращается двадцать пикселей высоты под текст */
  .carousel__dot,
  .plist__page {
    width: 28px;
    height: 28px;
  }

  .carousel__dots {
    gap: 0;
  }

  .plist__pages {
    gap: 0;
    margin-top: 1rem;
  }

  /* Тридцать два тега на узком экране — это девять сотен пикселей подряд.
     Пилюли мельче на пол-шага: список садится примерно на четверть,
     а читается по-прежнему легко */
  .tag {
    padding: 0.4rem 0.8rem;
    font-size: 0.875rem;
  }

  .stack__list {
    gap: 0.5rem;
  }
}

@media (max-width: 520px) {
  /* Кнопки во всю ширину здесь были: подпись прижималась влево, стрелка
     улетала к правому краю, и между ними зияла пустая полоса — кнопка
     выглядела растянутой, а не нажимаемой. Ширину задаёт содержимое,
     переносить их по строкам умеет сам .hero__actions */

  /* Номер узкий и типографский, в отличие от прежнего значка: прятать его
     на телефоне не нужно, достаточно поджать промежуток между колонками */
  .plist__row {
    gap: 0.9rem;
  }

  .steps {
    grid-template-columns: 1fr;
  }

  /* Держался на 36 px и ломался на четыре строки: средний член clamp
     (1.3rem + 4vw) на узком экране всё ещё выше нижней границы, поэтому
     уменьшать саму границу бесполезно — нужен свой шаг для узких экранов */
}

/* Низкое окно. Вертикальные поля карточки заданы в vw, и на широком, но
   низком экране — ноутбук 720p, окно не на весь экран — они раздуваются,
   отбирая у снимка около сотни пикселей. Здесь их считает высота, а не ширина:
   именно её и не хватает. Текст при этом не мельчает, сжимается только воздух */
@media (max-height: 820px) {
  .project__head {
    padding-top: 1.1rem;
  }

  .project__facts {
    gap: 0.6rem;
    padding-top: 0.75rem;
    padding-bottom: 1.15rem;
  }

  .carousel__dots {
    margin-top: 1rem;
  }
}

/* --- Уважение к настройкам движения ------------------------ */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .pmodal,
  .pmodal::backdrop,
  .drawer,
  .drawer::backdrop,
  .nav,
  .lang,
  .lang a {
    transition: none;
  }

  .pmodal {
    transform: none;
  }

  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
