/* ============================================================================
   Резюме — Алексей Коняев

   Оформление одно, тёмное: страница выглядит как рабочая среда, а не как
   документ. Вместо однотонного фона — поле моноширинных символов на canvas
   (`#field`, рисуется в app.js), курсор освещает его вокруг себя. Индиговый
   акцент на всём интерактивном, мятный — только на числах результата, янтарный
   — ровно один раз. Кислотного неона нет намеренно.

   Шрифты  Unbounded (заголовки и числа), Golos Text (текст),
           JetBrains Mono (данные, метки, поле символов)
   Сетка   колонка 68rem, у опыта левое поле под мета-данные
   Печать  светлые правила оставлены на случайный Ctrl+P; кнопки «Сохранить в
           PDF» на странице нет
   ========================================================================= */

/* --- Токены ------------------------------------------------------------- */

:root {
  --void: #080a10;
  --panel: #0f131c;
  --panel-2: #141926;
  --line: #1e2534;
  --line-soft: #161c28;

  --text: #e7eaf2;
  --text-dim: #b4bbcd;
  --muted: #838da4;

  --accent: #00c5cd;
  --accent-dim: #0b7a82;
  --accent-wash: rgba(0, 197, 205, 0.10);
  --mint: #3ddc97;
  --amber: #ffb454;
  --sky: #79c0ff;
  --magenta: #ff86c0;

  --font-display: 'Unbounded', 'Golos Text', 'Segoe UI', system-ui, sans-serif;
  --font-body: 'Golos Text', -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace;

  --step--2: 0.72rem;
  --step--1: 0.83rem;
  --step-0: 1rem;
  --step-1: 1.12rem;
  --step-2: 1.4rem;
  --step-3: clamp(1.6rem, 1.1rem + 1.7vw, 2.2rem);
  --step-4: clamp(1.9rem, 1.1rem + 3.2vw, 3.4rem);

  /* Скругление плашек одним числом: кнопки, чипы, карточки, показатели. Правится
     здесь, а не по двенадцати местам. --radius-sm — для вложенного ползунка
     языка: рамка `.lang` держит отступ 3px, и без вычета он смотрится не
     концентрично. Шкала опыта сюда не входит намеренно: её отрезки 10px в
     высоту, на большом радиусе они станут пилюлями. */
  --radius: 8px;
  --radius-sm: 5px;

  --measure: 66ch;
  --page: 68rem;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --margin-col: 11rem;
}

/* --- Основа ------------------------------------------------------------- */

* { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5.5rem;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background: var(--void);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  /* Подстраховка: декоративный слой не должен заводить горизонтальную полосу
     прокрутки. clip, а не hidden — иначе сломается липкая шапка. */
  overflow-x: clip;
}

/* Содержимое лежит НИЖЕ существ и обстановки (20 и 19) — так было с самого
   начала, и так и должно быть: предметы берут мышью, а слой выше содержимого
   у них для того и есть. Поднимать содержимое пробовали 2026-09-04, чтобы оно
   не закрывалось, — от этого предметы перестали браться вовсе, и владелец
   правку отменил. Текст же от мебели спасает не слой, а расстановка по краям
   и полоса у подвала. */
#app {
  position: relative;
  z-index: 1;
}

/* Свечение внизу страницы. Раньше оно жило внутри блока контактов и обрывалось
   по его нижнему краю — под ним оставался чёрный прямоугольник подвала. Теперь
   идёт от самого низа вверх и растворяется, границы не видно. Отступы только
   нулевые: отрицательные выводили слой за края и заводили полосу прокрутки. */
#app::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: min(62vh, 760px);
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(70% 100% at 28% 100%, rgba(0, 197, 205, 0.14), transparent 72%);
  /* Нижние 120 px слой гасит сам себя. За краем документа рисовать нечем: фон
     висит на body, у html его нет, а слой лежит внутри #app и кончается вместе
     с документом. Пока свечение было ярче всего в последней строке, на телефоне
     при оттяжке до упора выходила ровная горизонталь: rgb(7,29,35) над краем и
     rgb(8,10,16) под ним. Выключить сам откат не вышло — `overscroll-behavior`
     на странице WebKit не слушает. Теперь в последней строке слой прозрачен и
     сходится с полотном без шва, чем бы браузер за краем ни рисовал. Яркость
     поднята с 0.10 до 0.14, чтобы после гашения пик остался прежним. */
  -webkit-mask-image: linear-gradient(to top, transparent, #000 120px);
  mask-image: linear-gradient(to top, transparent, #000 120px);
}

/* Поле символов вместо однотонного фона. Лежит под всем содержимым (#app выше
   по z-index), поэтому панели и текст его перекрывают. Рисуется оно выше окна —
   на запас, который съедает глубина при прокрутке, — поэтому низ не прибиваем:
   высоту задаёт скрипт. */
#field {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 0;
  pointer-events: none;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.14;
  text-wrap: balance;
  margin: 0;
  letter-spacing: -0.02em;
}

p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color 0.15s ease;
}

/* Кнопки-ссылки (`.btn`) под это правило не попадают: у них свой отклик на
   наведение, а у главной кнопки его нет вовсе. Без исключения `a:hover`
   (0,1,1) бил бы цвет текста `.btn--primary` (0,1,0) — замерено 2026-09-03. */
a:not(.btn):hover { color: var(--text); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

::selection {
  background: var(--accent-dim);
  color: #fff;
}

.wrap {
  width: min(100% - var(--gutter) * 2, var(--page));
  margin-inline: auto;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

.skip { position: absolute; left: -9999px; }

/* Текст на акцентной заливке — по правилу у `.btn--primary`. */
.skip:focus {
  left: 1rem;
  top: 1rem;
  z-index: 100;
  padding: 0.5rem 0.9rem;
  background: var(--accent);
  color: var(--void);
}

/* --- Существо ----------------------------------------------------------- */

/* Ходит по нижнему краю окна поверх содержимого, но ниже шапки. Холст ровно по
   размеру рисунка, поэтому щелчку он мешает только там, где существо и стоит. */
/* Существа и обстановка живут по одним правилам: холст прибит к низу окна,
   двигает его только `transform`, щелчки холст пропускает сквозь себя. */
.pet,
.thing {
  position: fixed;
  inset-block-end: 0;
  inset-inline-start: 0;
  z-index: 20;
  image-rendering: pixelated;
  cursor: grab;
  user-select: none;
  /* По умолчанию холст прозрачен для щелчков — скрипт включает их, когда
     курсор оказывается на самом существе. */
  pointer-events: none;
}

/* Мебель стоит слоем ниже существ: они проходят перед ней, а не за.
   Висящее на стене — ещё ниже: перед ним проходит и мебель. */
.thing { z-index: 19; }
.thing--wall { z-index: 18; }

/* Доска висит в поле слева от текстовой колонки. Поле — это (ширина окна минус
   68rem колонки) пополам; если оно уже доски, она наезжает на строки, а
   закрывать текст нельзя. Доске (48 px) с отступами нужно поле в 68 px, то
   есть окно от 76.5em.

   Порог именно в `em`, а не в пикселях: колонка задана в `rem`, и у человека с
   крупным системным шрифтом она шире, а поле по краям — уже. В пикселях порог
   у него не сработал бы, и доска легла бы на текст. */
@media (max-width: 76.5em) {
  .thing--wall { display: none; }
}

/* Пока держат — курсор сжатый, а холст ловит движение мыши по всей странице. */
.pet.is-held,
.thing.is-held {
  cursor: grabbing;
  pointer-events: auto;
}

/* На телефоне существо только мешало: `position: fixed` считается от
   раскладочного окна, а у мобильных браузеров оно живёт своей жизнью вместе с
   выезжающей адресной строкой — существо оказывалось висящим в воздухе. */
@media (max-width: 720px) {
  .pet,
  .thing { display: none; }
}

@media print {
  .pet,
  .thing { display: none; }
}

/* --- Шапка -------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  background: color-mix(in srgb, var(--void) 82%, transparent);
  backdrop-filter: blur(12px);
}

.topbar__inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 3.5rem;
}

.topbar__name {
  font-family: var(--font-display);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.topbar__nav {
  display: flex;
  gap: 1.35rem;
  margin-inline-start: auto;
  font-size: var(--step--1);
}

.topbar__nav a {
  color: var(--muted);
  text-decoration: none;
  padding-block: 0.35rem;
  border-bottom: 1px solid transparent;
}

.topbar__nav a:hover,
.topbar__nav a.is-active {
  color: var(--text);
  border-bottom-color: var(--accent);
}

.topbar__tools {
  display: flex;
  gap: 0.4rem;
  margin-inline-start: auto;
}

.topbar__nav + .topbar__tools { margin-inline-start: 1.35rem; }

/* Шесть разделов в одну строку с именем и языком не помещаются. Раньше меню
   в этом месте просто пряталось, и на телефоне страница оставалась вообще без
   навигации — только прокрутка сверху донизу. Теперь оно переезжает во вторую
   строку шапки и прокручивается вбок. */
@media (max-width: 880px) {
  .topbar__inner {
    flex-wrap: wrap;
    gap: 0.1rem 1rem;
    min-height: 0;
    /* Шапка липкая и стала в две строки — лишнюю высоту тут держать нечем. */
    padding-block: 0.35rem;
  }

  .topbar__nav {
    order: 3;
    width: 100%;
    margin-inline-start: 0;
    gap: 1.15rem;
    overflow-x: auto;
    /* Прокрутка ленты не должна утягивать за собой страницу. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  .topbar__nav::-webkit-scrollbar { display: none; }

  .topbar__nav a {
    white-space: nowrap;
    font-size: var(--step--2);
    padding-block: 0.4rem;
  }

  .topbar__nav + .topbar__tools { margin-inline-start: auto; }
}

/* --- Переключатель языка ------------------------------------------------ */

.lang {
  position: relative;
  display: flex;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

/* Подложка едет под выбранную половину — отсюда ощущение ползунка. */
.lang__pill {
  position: absolute;
  inset-block: 3px;
  inset-inline-start: 3px;
  width: calc(50% - 3px);
  background: var(--accent);
  border-radius: var(--radius-sm);
  transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.lang.is-en .lang__pill { transform: translateX(100%); }

.lang__btn {
  position: relative;
  z-index: 1;
  min-width: 2.6rem;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 0.38rem 0.3rem;
  cursor: pointer;
  transition: color 0.2s ease;
}

/* Текст на акцентной заливке — по правилу у `.btn--primary`. */
.lang__btn[aria-pressed='true'] { color: var(--void); }
.lang__btn:not([aria-pressed='true']):hover { color: var(--text); }

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

.btn {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.45rem 0.75rem;
  cursor: pointer;
  /* transform — для магнита: кнопка тянется к курсору, сдвиг задаёт app.js.
     Небольшая задержка тут и нужна, иначе движение выходит дёрганым. */
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease,
    transform 0.18s cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* Главная кнопка («Написать в Telegram») исключена явно: по специфичности это
   правило (0,2,0) бьёт `.btn--primary` (0,1,0) и без исключения снимало бы под
   курсором её заливку. Ровно так и было в 4.27 — свечение добавили, а размывку
   вместо заливки не заметили. */
.btn:not(.btn--primary):hover {
  color: var(--text);
  border-color: var(--accent);
  background: var(--accent-wash);
}

.btn--primary,
.btn--ghost {
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  padding: 0.72rem 1.2rem;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* Текст поверх акцентной заливки — цветом фона страницы, а не белым: белое на
   бирюзе даёт 2.13:1 при норме 4.5:1, `--void` — 9.3:1.
   В печати оба токена переворачиваются (`--accent` чернеет, `--void` белеет),
   и правило продолжает работать само. Тем же покрашены выбранная половина
   переключателя языка и ссылка «К содержанию». */
.btn--primary {
  background: var(--accent);
  color: var(--void);
  border-color: var(--accent);
}

/* Наведение на главную кнопку не меняет её вовсе — решение владельца
   2026-09-03. Затемнение с белым текстом (4.26) и свечение вокруг (4.27) ему
   не понравились оба. Правила `:hover` у неё нет намеренно: пустое правило
   лишь маскировало бы общее `.btn:hover`, а оно исключает её само (см. выше).
   Магнит и рамка фокуса с клавиатуры остаются — они не наведение. */

/* --- Первый экран ------------------------------------------------------- */

/* Нижний отступ повторяет `padding-block-start` у `.facts`: анкета — последний
   ребёнок первого экрана, и этот отступ отделяет её от разделителя следующего
   раздела. Разными выражениями получалось 28 px сверху и 56 снизу — на сетке
   перекос прятала высота блока, а одна строка ленты его обнажила. Меняешь одно
   выражение — меняй и второе. */
.hero {
  position: relative;
  padding-block: clamp(2.5rem, 7vw, 5rem) clamp(1.25rem, 3vw, 1.75rem);
}

/* Мягкое свечение вместо картинки: единственный «эффектный» элемент фона.
   Отступы строго нулевые — с отрицательными свечение вылезало за края экрана,
   заводило горизонтальную полосу прокрутки и обрезалось на границе страницы.
   Градиенты гаснут внутри блока сами, поэтому жёсткого края не видно. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(58% 78% at 16% 0%, rgba(0, 197, 205, 0.11), transparent 70%),
    radial-gradient(48% 68% at 84% 2%, rgba(61, 220, 151, 0.08), transparent 70%);
}

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 22rem;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
}

/* Портрет ростом во весь текстовый блок: низ ложится на одну линию с нижним
   краем кнопок, верх — с началом надписи. Высота берётся у строки сетки, а не
   задаётся числом, поэтому линия не разъедется при правке текста. Ширина
   следует за высотой по пропорции и заходит влево в зазор между колонками. */
.hero__grid .portrait {
  align-self: stretch;
  justify-self: end;
  height: 100%;
  width: auto;
}

@media (max-width: 920px) {
  .hero__grid { grid-template-columns: minmax(0, 1fr); }

  /* Портрет с узкого экрана убран целиком — решение владельца 2026-09-02: на
     телефоне первым идёт сразу заголовок. Размеры ниже оставлены не напрасно:
     лист A4 — это около 794 px и попадает под тот же порог, а в печать
     фотография уходить должна. Печатные стили возвращают портрету display,
     размеры берутся отсюда. Скрипт лишней работы не делает сам: и отрисовка
     набора, и мерцание выходят раньше при нулевом размере. */
  .hero__grid .portrait {
    display: none;
    order: -1;
    align-self: start;
    justify-self: start;
    height: auto;
    width: min(15rem, 58%);
  }
}

.hero__name {
  font-family: var(--font-mono);
  font-size: var(--step-0);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-dim);
  margin-block: 0.5rem 0;
}

.hero__role {
  font-family: var(--font-display);
  font-size: var(--step-4);
  font-weight: 700;
  line-height: 1.06;
  letter-spacing: -0.035em;
  margin-block: 0.7rem 0;
  text-wrap: balance;
}

.hero__role em {
  font-style: normal;
  color: var(--accent);
}

/* «AI-разработке» не ломается по дефису: иначе акцентный термин рвётся пополам
   между строками. Только для русской версии: в английской акцент из двух слов,
   и запрет переноса вытолкнул бы его за край на узком экране. */
.hero__role:lang(ru) em { white-space: nowrap; }

.hero__lede {
  font-size: var(--step-1);
  color: var(--text-dim);
  max-width: var(--measure);
  margin-block-start: 1.2rem;
}

.hero__specs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-block-start: 1.35rem;
}

.spec {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.28rem 0.55rem;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-block-start: 1.9rem;
}

/* --- Портрет из символов ------------------------------------------------ */

/* Лицо набрано теми же знаками, что и фоновое поле страницы, — рисует его
   app.js на canvas. Фотография лежит сверху и проявляется только в круге под
   курсором: маска вырезает из неё пятно, за его пределами остаётся набор. */

.portrait {
  --mx: 50%;
  --my: 50%;
  position: relative;
  margin: 0;
  aspect-ratio: 92 / 100;
}

.portrait__glyphs,
.portrait__photo {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.portrait__photo {
  opacity: 0;
  transition: opacity 0.35s ease;
  -webkit-mask-image: radial-gradient(circle 125px at var(--mx) var(--my),
    #000 20%, transparent 70%);
  mask-image: radial-gradient(circle 125px at var(--mx) var(--my),
    #000 20%, transparent 70%);
}

.portrait.is-live .portrait__photo { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .portrait__photo { transition: none; }
}

/* Анкета бегущей лентой под первым экраном */

/* Пять пунктов — город, формат, переезд, опыт, почта — идут одной строкой по
   кругу. Сетка из пяти колонок отменена: на узком экране она разваливалась на
   три ряда, а значения ломались переносом посреди фразы.
   Лента склеена из двух одинаковых групп; сдвиг дорожки на половину её ширины
   ставит вторую группу ровно на место первой, и шов не виден. Промежуток между
   группами держит концевой отступ самой группы, а не `gap` дорожки: `gap`
   прибавился бы к ширине дорожки, половина перестала бы равняться группе — и
   шов бы поехал. */
.facts {
  --facts-gap: 2.5rem;
  --facts-fade: 3rem;
  /* Круг группы шириной около 950 px за 38 с — это примерно 25 px/с. Быстрее
     читать на ходу уже неудобно. */
  --facts-cycle: 38s;

  overflow: hidden;
  margin-block-start: clamp(2rem, 4vw, 3rem);
  padding-block-start: clamp(1.25rem, 3vw, 1.75rem);
  border-block-start: 1px solid var(--line);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--facts-fade),
    #000 calc(100% - var(--facts-fade)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--facts-fade),
    #000 calc(100% - var(--facts-fade)), transparent);
}

.facts__track {
  display: flex;
  width: max-content;
  animation: facts-run var(--facts-cycle) linear infinite;
}

/* Наведение и фокус с клавиатуры останавливают ленту. Без этого почта —
   движущаяся мишень, в которую надо попасть курсором. */
.facts:hover .facts__track,
.facts:focus-within .facts__track { animation-play-state: paused; }

/* Кольцо фокуса и растушёвка краёв не уживаются: маска гасит всё, что рисует
   элемент, а `outline-offset` выносит кольцо за её границу, где прозрачно
   уже полностью. Замер 2026-09-07: полоса матчит `:focus-visible`, а кольца
   на снимке нет вовсе — то есть один `tabindex` закрыл бы 2.1.1 и уронил
   2.4.7. Пока полоса в фокусе, растушёвку снимаем: она подсказывает про
   прокрутку тому, кто ещё не встал на полосу, а вставшему нужен видимый
   фокус. */
.facts:focus-visible {
  -webkit-mask-image: none;
  mask-image: none;
}

.facts__group {
  display: flex;
  flex: none;
  gap: var(--facts-gap);
  padding-inline-end: var(--facts-gap);
}

@keyframes facts-run {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* Лента стоит и листается пальцем или колесом — в двух случаях.
   Первый: движение отключено в системе. Второй: узкий экран, и тут дело в
   замере. Набор из пяти пунктов занимает 1045 px, а в окно телефона (375 px)
   ленты остаётся 335 — видно два пункта, и до почты, которая стоит на 880-м
   пикселе набора, на ходу пришлось бы ждать до 38 секунд. Порог 740 px взят
   не на глаз: ниже него в окно не помещаются и три пункта из пяти (им нужно
   около 690 px плюс поля), а сам порог в проекте уже принят — на нём опыт
   разворачивается в одну колонку.
   Дубль в обоих случаях убран: без движения он читается повтором. */
@media (prefers-reduced-motion: reduce), (max-width: 740px) {
  .facts { overflow-x: auto; overflow-y: hidden; }
  .facts__track { animation: none; }
  .facts__group + .facts__group { display: none; }
}

/* Значение в одну строку: перенос посреди фразы на едущей ленте нечитаем. */
.fact { display: grid; gap: 0.15rem; white-space: nowrap; }

.fact__key {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.fact__val {
  font-size: var(--step--1);
  color: var(--text);
}

.fact__val a { text-decoration: none; }
.fact__val a:hover { text-decoration: underline; }

/* --- Секции ------------------------------------------------------------- */

/* Полос во всю ширину окна на странице нет: владелец снял их 2026-09-05, оставив
   те, что живут внутри колонки с текстом (хвост у заголовка, разделители работ
   и записей). Разделы разводит воздух: 40–72 px сверху и снизу, то есть между
   соседними 80–144 px. В печати линейка сохранена — на бумаге она помогает. */
.section {
  padding-block: clamp(2.5rem, 6vw, 4.5rem);
}

.section__head {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-block-end: clamp(1.5rem, 3vw, 2.5rem);
}

.section__head::before {
  content: '';
  width: 7px;
  height: 7px;
  flex: none;
  background: var(--accent);
}

.section__head::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--line);
}


.section__title {
  font-size: var(--step-2);
  letter-spacing: -0.02em;
}

.section__note {
  font-size: var(--step--1);
  color: var(--muted);
  max-width: var(--measure);
  margin-block-end: 1.5rem;
}

/* --- Результаты --------------------------------------------------------- */

/* Четыре плашки в ряд, на узком — по две и по одной. */
.metrics {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
}

@media (max-width: 900px) {
  .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 480px) {
  .metrics { grid-template-columns: minmax(0, 1fr); }
}

.metric {
  --tone: var(--mint);
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.6rem 1.35rem 1.4rem;
  display: grid;
  grid-template-rows: auto 1fr;
  gap: 0.75rem;
  overflow: hidden;
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

/* Черта над числом — в цвет самого числа; загорается по всей ширине при
   наведении. Рисуется верхней кромкой рамки, а не прямой полоской: у карточки
   `overflow: hidden` и скруглённый угол, и прямая полоска обрезалась дугой —
   выглядело так, будто она не доходит до угла. Кромка идёт по самой дуге и в
   угол заходит. Радиус на пиксель меньше карточкиного: absolute отсчитывается
   от внутреннего края рамки, а она в 1 px. Растёт черта срезом справа, а не
   шириной: ширина утащила бы за собой и правый угол. */
.metric::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-top: 2px solid var(--tone);
  border-radius: calc(var(--radius) - 1px);
  clip-path: inset(0 calc(100% - 2.5rem) 0 0);
  transition: clip-path 0.35s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.metric:hover {
  background: var(--panel-2);
  border-color: color-mix(in srgb, var(--tone) 40%, var(--line));
  transform: translateY(-2px);
}

.metric:hover::before { clip-path: inset(0 0 0 0); }

/* Блик под курсором. Точку задаёт app.js через --mx / --my; пока мышь не
   заходила на карточку, пятно лежит по центру и всё равно не видно — слой
   проявляется только на :hover. */
.metric::after,
.project::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.metric::after {
  background: radial-gradient(220px circle at var(--mx, 50%) var(--my, 50%),
    color-mix(in srgb, var(--tone) 11%, transparent), transparent 62%);
}

.project::after {
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 50%),
    rgba(0, 197, 205, 0.10), transparent 62%);
}

.metric:hover::after,
.project:hover::after { opacity: 1; }

/* Содержимое карточек должно лежать поверх блика. */
.metric > *,
.project > * {
  position: relative;
  z-index: 1;
}

.metric__value {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.2rem + 1.9vw, 2.5rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.05em;
  color: var(--tone);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Каждому показателю свой цвет — как подсветка значений в консоли Python.
   Тон задан позицией, а не данными: цвет это оформление, в data/resume.js
   ему не место. Пятая карточка, если появится, возьмёт мятный по умолчанию.
   За тоном идут и черта над числом, и рамка при наведении, и блик под
   курсором — иначе пурпурное число стояло бы под мятной чертой. */
.metric:nth-child(1) { --tone: var(--amber); }
.metric:nth-child(2) { --tone: var(--mint); }
.metric:nth-child(3) { --tone: var(--sky); }
.metric:nth-child(4) { --tone: var(--magenta); }

.metric__caption {
  font-size: var(--step--1);
  color: var(--text-dim);
  max-width: 24ch;
}

/* --- Подход ------------------------------------------------------------- */

.approach {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: start;
}

@media (max-width: 820px) {
  .approach { grid-template-columns: minmax(0, 1fr); }
}

.approach__text {
  color: var(--text-dim);
  max-width: var(--measure);
}

.pullquote {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  line-height: 1.32;
  letter-spacing: -0.02em;
  color: var(--text);
  border-inline-start: 2px solid var(--accent);
  padding: 0.2rem 0 0.2rem 1.25rem;
  text-wrap: balance;
  margin: 0;
}

/* --- Шкала опыта -------------------------------------------------------- */

.timeline { margin-block-end: clamp(2rem, 4vw, 3rem); }

.timeline__scale {
  position: relative;
  height: 1.3rem;
}

/* Левая граница метки работает засечкой года — отдельных палочек не нужно. */
.timeline__year {
  position: absolute;
  inset-block-start: 0;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  line-height: 1.3rem;
  color: var(--muted);
  padding-inline-start: 0.4rem;
  border-inline-start: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}

/* Высота 12, а не 10: на десяти пикселях максимальное скругление даёт радиус 5 и
   почти не читается — полоса выглядит прямой. Скругление у дорожки и отрезков
   стоит `999px`, то есть предельное, и единственный способ сделать его заметным
   — поднять высоту. Пробовали 16 — владелец назвал толстым; 12 это точка, где
   пилюля читается, а веса ещё нет. */
.timeline__track {
  position: relative;
  height: 12px;
  background: var(--line-soft);
  border-radius: 999px;
}

/* Отрезки считаются по датам, и подряд идущие работы стыкуются вплотную: правый
   край «Дом-Профи» и левый край «ГалВента» — одна и та же точка. На пилюлях там
   встречались закругления и получалась «талия». Прозрачная левая кромка
   отодвигает заливку на 2 px, и на стыке выходит ровный зазор. Кромка именно
   левая: справа у текущей работы стоит засечка «сейчас», и от правой заливка
   отошла бы от неё. Ширина приходит инлайном от скрипта в процентах, а
   `box-sizing: border-box` держит её неизменной — геометрия по датам не
   съезжает, двигается только заливка.
   Цвета ниже заданы через `background-color`, а не сокращённой записью:
   сокращённая сбрасывала бы `background-clip` обратно к рамке, и обрезка
   слетала бы у текущей работы и на наведении. */
.timeline__seg {
  position: absolute;
  inset-block: 0;
  border-radius: 999px;
  border-left: 2px solid transparent;
  background-color: var(--accent-dim);
  background-clip: padding-box;
  transition: background-color 0.18s ease, transform 0.18s ease;
}

.timeline__seg:hover {
  background-color: var(--accent);
  transform: scaleY(1.35);
}

/* Полоса высотой 12 px — пальцем в неё не попасть. Прозрачная накладка
   расширяет область нажатия по вертикали, не трогая вид. Только для
   неточного указателя: под мышью подсветка отрезка «за 14 px до него»
   выглядела бы поломкой. */
@media (pointer: coarse) {
  .timeline__seg::after {
    content: '';
    position: absolute;
    inset-block: -14px;
    inset-inline: 0;
  }
}

.timeline__seg.is-current { background-color: var(--mint); }
.timeline__seg.is-current:hover { background-color: color-mix(in srgb, var(--mint) 80%, #fff); }

/* Метка «сейчас». Текущая работа не кончилась, но её отрезок обрывался справа
   так же, как завершённые, и читался как «закончилось». Метка стоит ровно на
   правом краю оси: ось и считается до текущего месяца, поэтому другого места у
   неё быть не может. Взята засечкой почти белого — мятная на мятном отрезке
   слилась бы, — и медленно дышит мятным кольцом.
   Пузырь висит на `::before`: `::after` у отрезка уже занят областью нажатия
   для пальца. */
.timeline__seg.is-current::before {
  content: '';
  position: absolute;
  inset-block: -4px;
  inset-inline-end: -1px;
  width: 3px;
  border-radius: 999px;
  background: var(--text);
  animation: now-breath 2.6s ease-out infinite;
}

@keyframes now-breath {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mint) 60%, transparent); }
  70% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--mint) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mint) 0%, transparent); }
}

@media (prefers-reduced-motion: reduce) {
  .timeline__seg.is-current::before { animation: none; }
}

.timeline__labels {
  position: relative;
  height: 2.7rem;
  margin-block-start: 0.6rem;
}

.timeline__label {
  position: absolute;
  inset-block-start: 0;
  display: grid;
  gap: 0.1rem;
  text-decoration: none;
}

.timeline__company {
  font-size: var(--step--1);
  color: var(--text-dim);
  white-space: nowrap;
  transition: color 0.15s ease;
}

.timeline__span {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--muted);
  white-space: nowrap;
}

.timeline__label:hover .timeline__company { color: var(--accent); }

/* На узком экране подписи наезжают друг на друга: полоса остаётся, названия
   всё равно видны ниже, в самих карточках опыта. */
@media (max-width: 720px) {
  .timeline__labels { display: none; }
  .timeline { margin-block-end: 1.75rem; }
}

/* --- Опыт --------------------------------------------------------------- */

.jobs { display: grid; }

.job {
  display: grid;
  grid-template-columns: var(--margin-col) minmax(0, 1fr);
  gap: 0 clamp(1.25rem, 3vw, 2.5rem);
  padding-block: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--line-soft);
}

.job:first-child { border-top: 0; padding-block-start: 0; }

@media (max-width: 740px) {
  .job { grid-template-columns: minmax(0, 1fr); gap: 0.75rem; }
}

.job__meta {
  display: grid;
  gap: 0.3rem;
  align-content: start;
  position: sticky;
  top: 5rem;
}

@media (max-width: 740px) {
  .job__meta { position: static; }
}

.job__period {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--text-dim);
  line-height: 1.45;
}

.job__duration {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--muted);
}

.job__now {
  justify-self: start;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mint);
  border: 1px solid color-mix(in srgb, var(--mint) 45%, transparent);
  border-radius: var(--radius);
  padding: 0.1rem 0.4rem;
  margin-block-start: 0.25rem;
}

.job__company {
  font-size: var(--step-2);
  margin-block-end: 0.15rem;
}

.job__role {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--accent);
  margin-block-end: 1.1rem;
}

.job__bullets {
  display: grid;
  gap: 0.9rem;
  max-width: var(--measure);
}

.bullet {
  display: grid;
  grid-template-columns: 0.9rem minmax(0, 1fr);
  gap: 0.6rem;
  color: var(--text-dim);
}

.bullet::before {
  content: '';
  width: 6px;
  height: 6px;
  margin-block-start: 0.6em;
  background: var(--line);
}

/* --- Проекты ------------------------------------------------------------ */

.projects {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: 1.25rem;
}

.project {
  position: relative;
  display: grid;
  align-content: start;
  gap: 0.6rem;
  padding: 1.5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}

.project:hover {
  border-color: var(--accent);
  background: var(--panel-2);
  transform: translateY(-2px);
}

.project__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.project__name { font-size: var(--step-2); }

.project__stars {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--amber);
  white-space: nowrap;
}

.project__tagline {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.04em;
  color: var(--accent);
}

.project__release {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--muted);
  margin-block-start: 0.35rem;
}

.project__text {
  font-size: var(--step--1);
  color: var(--text-dim);
}

.project__stack {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-block-start: 0.3rem;
}

.project__stack span {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.15rem 0.4rem;
}

.project__link {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.04em;
  text-decoration: none;
  margin-block-start: 0.6rem;
}

.project__link:hover { text-decoration: underline; }

/* --- Навыки ------------------------------------------------------------- */

/* Групп четыре, поэтому две колонки: при автоподборе четвёртая уезжала
   в отдельный ряд, а длинные названия навыков ломались пополам. */
.skill-groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 2.75rem);
}

@media (max-width: 760px) {
  .skill-groups { grid-template-columns: minmax(0, 1fr); }
}

.skill-group__title {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  padding-block-end: 0.5rem;
  border-bottom: 1px solid var(--line);
  margin-block-end: 0.9rem;
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.chip {
  font-family: var(--font-body);
  font-size: var(--step--1);
  color: var(--text-dim);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.34rem 0.62rem;
}

/* --- Образование -------------------------------------------------------- */

.two-col {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: clamp(1.5rem, 4vw, 3rem);
}

.record {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: 0 1rem;
  padding-block: 0.9rem;
  border-top: 1px solid var(--line-soft);
}

.record:first-of-type { border-top: 0; }

.record__year {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.record__title {
  font-size: var(--step-0);
  font-weight: 500;
  color: var(--text);
}

.record__place {
  font-size: var(--step--1);
  color: var(--muted);
}

/* --- Контакты ----------------------------------------------------------- */

.contact {
  position: relative;
  padding-block: clamp(3rem, 7vw, 5rem);
}

.contact__title {
  font-size: var(--step-3);
  max-width: 20ch;
}

.contact__text {
  font-size: var(--step-1);
  color: var(--text-dim);
  max-width: var(--measure);
  margin-block-start: 1rem;
}

.contact__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-block-start: 1.9rem;
}

/* Снизу оставлена полоса под обстановку: холсты мебели и существ прибиты к
   низу окна, и на прокрученной до конца странице подписи подвала оказывались
   ровно под ними — владелец увидел это на телевизоре. Полоса выше всего, что
   там живёт: самый высокий предмет на полу — полка (54 px), а выше всех висят
   часы (до 99 px). Отсюда 6.5rem, но не меньше 112 px: предметы нарисованы в
   пикселях и с размером шрифта не меняются, а `rem` у человека с мелким
   системным шрифтом сжался бы и полоса ушла под часы. */
.footer {
  padding-block: 1.5rem max(6.5rem, 112px);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--muted);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  justify-content: space-between;
}

/* Подчёркивание снято по просьбе владельца 2026-09-02. На наведении оно
   возвращается — та же идиома, что у ссылок в анкете и на карточках проектов:
   ссылка остаётся отличима не одним лишь цветом. Ссылка стоит отдельным
   элементом, не внутри фразы, поэтому в покое подчёркивание не обязательно. */
.footer a { color: var(--muted); text-decoration: none; }
.footer a:hover { color: var(--text); text-decoration: underline; }

/* --- Появление при прокрутке -------------------------------------------- */

.enter {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.enter.is-in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .enter { opacity: 1; transform: none; }
}

/* Чипы навыков проявляются волной, а не разом: каждому своя задержка по
   сквозному номеру `--i`, который ставит app.js, шаг 20 мс — двадцать шесть
   чипов встают за полсекунды. Анимация с `both`: она держит конечное
   состояние, поэтому базовое `opacity: 0` после волны не мешает.
   Специфичность у трёх правил ниже одинаковая, решает порядок: тихий режим и
   печать стоят позже и выигрывают. */
.skill-groups.enter .chip { opacity: 0; }

.skill-groups.is-in .chip {
  animation: chip-in 0.35s ease both;
  animation-delay: calc(var(--i, 0) * 20ms);
}

@keyframes chip-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .skill-groups.enter .chip { opacity: 1; animation: none; }
}

/* --- Печать ------------------------------------------------------------- */

@media print {
  :root {
    --void: #fff;
    --panel: #fff;
    --panel-2: #fff;
    --line: #bbb;
    --line-soft: #ddd;
    --text: #000;
    --text-dim: #1a1a1a;
    --muted: #555;
    --accent: #000;
    --accent-wash: transparent;
    --mint: #000;
    --amber: #000;
    --sky: #000;
    --magenta: #000;
  }

  @page { margin: 14mm; }

  body { font-size: 10.5pt; background: #fff; }
  .hero::before,
  .contact::before,
  .metric::after,
  .project::after,
  .portrait__glyphs { display: none !important; }

  /* Портрет спрятан правилом узкого экрана, под которое лист A4 попадает.
     В печать он идти должен — возвращаем; размеры берутся из того же правила. */
  .hero__grid .portrait { display: block; }

  /* В печать уходит обычная фотография, а не набор из знаков. */
  .portrait__photo {
    opacity: 1 !important;
    mask-image: none !important;
    -webkit-mask-image: none !important;
  }

  /* На листе ленты нет: дубль убран, движение и размытые края сняты, пункты
     снова стоят сеткой в три колонки — ровно как стояли до ленты. Флексом их
     тут не оставить: группа не сжимается (`flex: none`) и уезжает за поле
     страницы, унося с собой правые пункты. */
  .facts {
    overflow: visible;
    mask-image: none !important;
    -webkit-mask-image: none !important;
  }

  .facts__track { display: block; width: auto; animation: none; }

  .facts__group {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.35rem 2rem;
    padding-inline-end: 0;
  }

  .facts__group + .facts__group { display: none; }

  /* Значение переносится по словам: в одну строку оно уедет за поле листа. */
  .fact { white-space: normal; }

  .topbar,
  .hero__actions,
  .contact__actions,
  .section__note,
  #field,
  .skip { display: none !important; }

  .section, .contact { border-top: 1px solid #bbb; padding-block: 12pt; }
  .hero { padding-block: 0 12pt; }
  .job, .project, .metric, .record { break-inside: avoid; }
  .job__meta { position: static; }
  .enter { opacity: 1; transform: none; }
  .skill-groups.enter .chip { opacity: 1; animation: none; }
  .projects { grid-template-columns: 1fr 1fr; }
  a { text-decoration: none; color: #000; }

  .print-only { display: block !important; }
}

.print-only { display: none; }
