/* Раскладка визитки. Единственный источник цветов и отступов — tokens.css;
   здесь только форма. Правило: ни одного цвета и ни одного rem мимо
   токенов, иначе страница перестанет переключаться темой целиком.

   Редизайн 2026-10-09 «Консоль-герой». Одно отступов — clamp() из спеки,
   один размер — fluid-шкала clamp(): это единственные литералы в файле,
   и они записаны тут же с пояснением. */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  /* Спека §4: 1.65 — главный рычаг воздуха после отступов секций. */
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;

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

     Два размытых пятна (голубое и фиолетовое) убраны 2026-10-28. Они
     давали мягкий свет в верхних углах, но это ровно та пара цветов,
     по которой страницу узнают как собранную нейросетью: синий и
     фиолетовый радиальные градиенты. Плоский фон с сеткой читается
     спокойнее, и глубина осталась у фактуры, а не у свечения.

     Габариты сетки заданы в px: это абсолютные размеры, а не отступы,
     и rem здесь означал бы, что сетка дышит вместе с шириной окна. */
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 32px 32px, 32px 32px;
  background-repeat: repeat, repeat;
  /* fixed — сетка не едет при скролле. Минус известен: на мобильных
     браузерах фон с fixed перерисовывается на всё прокручивание, и
     дешёвый первый экран становится дорогим. На 32px-текстуре разница
     не видна глазом, поэтому оставляем — а на медленных устройствах
     первым делом отключать надо именно эту строку. */
  background-attachment: fixed;
}

/* --- Переход к содержимому ---------------------------------------------- */

/* Ссылка нужна только клавиатуре и скринридеру: мышью её не достать.
   clip-path вместо отрицательного left — элемент остаётся в дереве
   доступности и в порядке обхода, но не занимает места на экране.
   На фокусе показывается поверх шапки, в левом верхнем углу. */
.skip {
  position: fixed;
  top: var(--space-4);
  left: var(--space-4);
  z-index: 1;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-strong);
  text-decoration: none;
  clip-path: inset(50%);
}

.skip:focus { clip-path: none; }

/* Ширина страницы, спека §2: min(1040px, 100% - 2rem), где 1040px —
   это --wrapper-max из tokens.css. Отступ по бокам входит в
   100% - 2rem, поэтому padding-inline не нужен — иначе поля
   получились бы 2rem плюс ещё 1rem. Враппер — единственное место,
   где ширина ограничивается; margin-inline центрирует. */
.wrapper {
  width: min(var(--wrapper-max), 100% - 2rem);
  margin-inline: auto;
  padding-block: var(--space-7);
  display: flex;
  flex-direction: column;
  gap: var(--space-section);
}

/* --- Hero --------------------------------------------------------------- */

/* Hero по высоте контента. Распорка min-height: 70vh была спецификацией
   старой версии, но при коротком hero она оставляла под ним дыру в
   пол-экрана, а сверху ещё --space-section — двойной провал перед about.
   Контент прижат к началу по умолчанию, и первый экран не требует
   скролла именно потому, что контент идёт сразу за шапкой.

   Сетка, а не флекс: с 900px шапка повторяет колонки страницы, и одна
   раскладка должна уметь обе. До 900px это одна колонка — порядок в DOM
   совпадает с порядком на экране, поэтому order нигде не нужен. */
.hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

/* $ whoami стоит в одном ряду с заголовками блоков: та же fluid-шкала
   24-32px, тот же вес 500 и --text-strong, тот же акцентный маркер.
   Раньше это была подпись 14px --muted — страница начиналась мелким серым
   текстом и дальше уходила в заголовки крупнее шапки. Теперь промпт
   читается как первая команда сессии, а имя — как её вывод.

   Кегль не 48px, как у имени: $ whoami это метка раздела, а не заголовок
   страницы, и второй крупный текст рядом с именем спорил бы с ним. */
.hero__prompt {
  margin: 0;
  font-family: var(--mono);
  font-size: clamp(1.5rem, 4vw, 2rem);
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-strong);
}

/* Аватар слева от имени; ниже 640px он встаёт над именем. Плитка 104px:
   исходник 96px и 4px подложки с каждой стороны.

   Имя и подпись — один блок, а не два элемента сетки: подпись должна
   стоять вплотную под именем при любом размере аватара. Отдельным рядом
   она привязывалась бы к высоте плитки, и с плиткой 104px между именем
   и подписью открылась бы дыра в сорок пикселей. */
.hero__id {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

/* В разметке имя идёт первым, аватар — вторым: имя задаёт начало блока
   для чтения и скринридера, а картинка декоративная (alt=""), так что
   после заголовка ей нечего терять. На экране аватар всё равно слева:
   order ставит его перед именем, пока .hero__id остаётся флексом, а с
   900px он и так первая колонка грида. */
.hero__names {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

/* Плитка шире картинки на подложку с обеих сторон: 96px исходника плюс
   4px подложки. object-fit: cover держит картинку квадратной, если
   исходник когда-нибудь переедет на другое соотношение. */
.hero__avatar {
  flex: none;
  order: -1;
  width: calc(var(--avatar-size) + 2 * var(--space-1));
  height: calc(var(--avatar-size) + 2 * var(--space-1));
  padding: var(--space-1);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  object-fit: cover;
}

/* Имя 32-48px — решение владельца 2026-10-10, против прежних 40-80px.
   Оно перестало быть главным пятном страницы: двухколоночная раскладка
   отдала первый экран блокам, и имя в 80px смотрелось вывеской поверх
   содержимого. Ubuntu, а не моноширинный — терминальность ушла в промпт
   и решётку заголовков, а имя читается как заголовок. */
.hero__name {
  margin: 0;
  font-family: var(--font);
  font-size: clamp(2rem, 5vw, 3rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text-strong);
  text-wrap: balance;
}

.hero__role {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.875rem;
  color: var(--muted);
}

/* Подпись живёт под именем, а не после роли. Щит и «Cybersecurity
   enthusiast» — характеристика человека, а Red team · Pentest · DFIR —
   строка специализации в правой колонке; в одной строке они спорили за
   внимание и рвали подпись переносом на узких экранах.

   Подпись стоит вплотную под именем внутри .hero__names: отступ задаёт
   gap этого блока, а не поле слева, поэтому подпись остаётся под именем
   и на узком экране, где аватар встаёт над ним. */
.hero__tag {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.875rem;
  color: var(--muted);
  white-space: nowrap;
}

/* Щит неразрывен со словом «enthusiast»: перенос разорвал бы связку
   пополам. Вертикальное выравнивание по нижнему краю — щит не должен
   прыгать в строке с кириллицей и латиницей разного роста. */
.hero__shield {
  display: inline-flex;
  align-items: baseline;
  vertical-align: -0.08em;
  margin-right: 0.3em;
  color: var(--signal);
}

.hero__shield svg { width: 0.95em; height: 0.95em; }

/* С 900px шапка повторяет сетку страницы: имя в левой колонке, роль — в
   правой. Колонки те же, что у main — 1fr / 1.25fr с тем же column-gap,
   поэтому правый край шапки совпадает с правым краем панели projects и
   глаз ведёт по одной вертикали сверху вниз.

   Роль выровнена по центру ряда, а не по базовой линии. Baseline здесь
   обманывает: .hero__id — грид с центрированными элементами, и наружу он
   не отдаёт линию имени, браузер синтезирует её по нижнему краю блока.
   Роль садилась на низ аватара — на 50px ниже имени. Центр ряда держит
   её на оптической линии имени: при 48px имени и 104px плитке базовая
   линия роли оказывается в трёх пикселях от линии имени, то есть на глаз
   они стоят на одной строке.

   900px — не произвол: роль в моно 14px занимает около 460px, и ниже
   этого порога правая колонка перестаёт вмещать её в одну строку.
   Медиа-условие отдельное от 640px: основной грид складывается раньше,
   потому что ему хватает и 700px, а шапке нужен запас на длинную роль. */
@media (min-width: 900px) {
  .hero {
    grid-template-columns: 1fr 1.25fr;
    grid-template-areas:
      'prompt prompt'
      'id     role';
    column-gap: var(--space-6);
    align-items: center;
  }

  .hero__prompt { grid-area: prompt; }

  /* Имя и подпись раскладываются колонками внутри .hero__id, а не в
     общем гриде: так подпись всегда прижата к имени, а аватар стоит
     слева независимо от их высоты. */
  .hero__id {
    grid-area: id;
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
  }

  .hero__avatar { order: 0; grid-column: 1; grid-row: 1; }
  .hero__names  { grid-column: 2; grid-row: 1; }

  .hero__role { grid-area: role; }
}

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

/* Спека §2: широкий экран — две колонки, projects шире и тянется на обе
   строки. Названия областей вместо номеров строк: перестановка блоков
   в разметке не должна ломать раскладку, а области это выражают.
   Порядок в DOM (hero, about, projects, best, elsewhere) совпадает с
   порядком на узком экране, поэтому order не нужен нигде. */
main {
  display: grid;
  grid-template-areas:
    'about projects'
    'elsewhere projects'
    'best best';
  grid-template-columns: 1fr 1.25fr;
  column-gap: var(--space-6);
  /* Ряды без общего отступа: у каждого блока своя вертикальная ритмика,
     а один row-gap на все три ряда растянул бы промежуток между фактами
     в about до размера секции. */
  row-gap: 0;
  align-items: start;
}

#about     { grid-area: about; }
#projects  { grid-area: projects; }
#best      { grid-area: best; }
#elsewhere { grid-area: elsewhere; }

/* Вертикальные отступы внутри грида задаются полями: в гриде и во flex
   поля не схлопываются, поэтому ритмика предсказуема — в отличие от
   margin-top у соседних блочных элементов.

   Отступ между about и elsewhere несёт нижний блок, а не верхний.
   Причина в распределении лишней высоты: projects тянется на два ряда,
   и если он выше левой колонки, браузер делит разницу между рядами —
   отступ получался 100px вместо заявленных 24, и это читалось дырой.
   Токен на месте стыка делает шов таким же, как перед best of, и
   оставляет раскладке нечего распределять: колонка заполняется сама. */
#about     { margin-block-end: 0; }
#elsewhere { margin-block-start: var(--space-section); }

/* Отступ сверху у best — тот же --space-section, что и швы .wrapper.
     Раньше он стоял поверх block-gap, и над «#best of» получалась дыра
     в 198px — владелец отметил её отдельно, и именно она была самым
     заметным местом страницы. Теперь это ровно один шов, а не полтора:
     retune --space-section 2026-10-09 снял 40px отсюда и 39px у соседних
     секций, и третий ряд отделился от второго так же, как второй от
     первого. Отдельного числа здесь намеренно нет: если бы #best отодвигался
     не тем же токеном, правка ритма разъехалась бы в двух местах сразу. */
#best      { margin-block-start: var(--space-section); }

.block {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Грид-элемент по умолчанию не даёт сжиматься уже своего содержимого
     (min-width: auto). Длинная строка в карточке projects растянула бы
     трек 1.25fr шириной текста, и колонки перестали бы быть колонками —
     страница при этом выглядит рабочей, править придётся по факту. */
  min-width: 0;
}

/* Спека §4: mono 500, решётка вместо прежнего висячего `_`. Подчёркивание
   читалось как незакрытый промпт — незаконченная мысль, а решётка в
   начале читается как рут и рифмуется с `$ whoami` в шапке. */
.block__title {
  margin: 0;
  font-family: var(--mono);
  font-size: clamp(1.5rem, 4vw, 2rem);
  font-weight: 500;
  /* 1.2, а не унаследованные от body 1.65. Заголовок 32px занимал коробку
     52.8px: двадцать пикселей пустоты сверху и снизу, которых не видно в
     токенах, но которые складывались с gap блока. Отступ «по шкале» на
     деле был больше шкалы, и это читалось как забытый воздух. */
  line-height: 1.2;
  color: var(--text-strong);
  text-wrap: balance;
}

/* Отступ от заголовка до содержимого разный: у блоков с панелью он
   больше, панель тяжелее строки текста. Промежуточный отступ — тот же,
   что и под `$ whoami`, где промпт тоже отделён от содержимого. */
.block--panel > .block__title { margin-block-end: var(--space-5); }

/* Акцентный маркер общий у заголовков блоков и у `$ whoami` в шапке:
   решётка и доллар красятся одним и тем же --accent, поэтому промпт
   читается как первая команда той же сессии, что и `# about`.

   Пробел после маркера — 0.6em, ширина пробела в этой гарнитуре
   (замерено), и em, а не rem: заголовок течёт через clamp(), и
   фиксированный отступ разошёлся бы с ним на узких экранах. У промпта
   кегль тот же clamp, так что один margin обслуживает оба. */
.block__hash,
.hero__prompt-mark {
  color: var(--accent);
  margin-right: 0.6em;
}

/* about — список фактов, а не абзац: каждый пункт отвечает на свой
   вопрос, и ссылка внутри факта остаётся на своём месте при правке
   соседней строки. */
/* Факты читаются списком, а не тремя абзацами: 2026-10-09 владелец
   назвал их «растянутыми» и отметил все три. Поэтому gap ужат до
   --space-1, а межстрочный интервал самого факта — до 1.45 (было 1.65 от
   body, то есть 26.4px на 17px текста). Итоговый шаг строки списка вместо
   34.4px — около 29px: факты остаются отдельными строками, но стоят
   плотно, как список, а не как три абзаца с пустыми промежутками. */
.facts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.fact {
  /* 18px — ступень шкалы. Было 17px: размер, которого нет ни в одном
     шаге, и он ставил факты между body и названиями строк без причины. */
  font-size: 1.125rem;
  line-height: 1.45;
  color: var(--text);
  text-wrap: pretty;
}

.fact__link {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Ховер уводит ссылку в цвет основного текста, а не в зелёный: зелёный
   был вторым акцентом рядом с голубым, и ссылка при наведении меняла
   палитру страницы. Здесь меняется только вес — подчёркивание остаётся
   на месте, поэтому ссылка не прыгает. */
.fact__link:hover { color: var(--text-strong); }

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

/* --- Чипы: лента ссылок ------------------------------------------------- */

/* Ширина по содержимому, а не сетка. Владелец добавляет ссылки, и
   фиксированная сетка ломалась бы на каждой новой: чип на flex-wrap
   занимает ровно свою ширину, новая ссылка встаёт в конец строки или
   переносится. На узком экране чипы перестают помещаться и сами уходят
   в столбик — отдельное медиа-правило не нужно. */
.chips {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* li остаются обычными флекс-элементами, без display: contents. Бокс li
   и так равен ширине чипа: чип объявлен white-space: nowrap, поэтому
   min-content li равен его содержимому и перенос считается по ссылке.
   contents убрал бы пункты списка из дерева доступности у части
   браузеров и скринридеров — ради выигрыша, которого здесь нет. */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.875rem;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1),
              border-color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Ховер — в акцент, а не в зелёный: --signal остался микро-сигналом щита
   в шапке, и вторым акцентом в интерактиве он больше не работает. */
.chip:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.chip:active { background: var(--surface-2); }

.chip__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  color: var(--brand-ink);
}

.chip__icon img,
.chip__icon svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.chip__icon svg { fill: currentColor; }

/* --- Projects: одна широкая панель ------------------------------------- */

.panel {
  background: var(--surface);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.panel > .row + .row { border-top: 1px solid var(--surface-2); }

.row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 56px;
  padding: var(--space-3) var(--space-4);
  text-decoration: none;
  color: inherit;
  transition: background 0.3s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.row:hover { background: var(--surface-2); }

/* Нажатие отмечается полосой у левого края: фон уже занят ховером, и
   второй оттенок поверхности рядом с первым неразличим. Тень рисуется
   внутрь строки — панель обрезает всё, что выходит наружу. */
.row:active {
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* Знак без плашки. Плашка 36px с заливкой --surface-2 была фоном для
   глифа, но четыре из пяти знаков — это favicon чужого проекта со своей
   квадратной подложкой внутри файла. Плашка под плашкой давала рамку в
   рамке: у Scene оранжевый квадрат в сером, у stats и ctf тёмный в
   тёмном. Знак — это сам файл, второй фон ему не нужен.

   Размер 24px, а не 20px: без плашки 20px читались как значки в списке
   настроек, а не как проекты. 24px держит вес строки, не споря с
   названием, и попадает в тот же диапазон, что знак спотлайта (28px).

   object-fit: contain оставлен: знаки разной природы (svg и растр)
   приводятся к одному боксу, а не растягиваются под него. */
.row__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  color: var(--accent);
}

.row__icon img,
.row__icon svg {
  display: block;
  width: 24px;
  height: 24px;
  object-fit: contain;
}

.row__icon svg { fill: currentColor; }

/* Знак канала в фирменном цвете: он и без подписи узнаётся, а общий
   акцент на первой строке сделал бы её просто ещё одной. */
.row--accent .row__icon {
  color: var(--tg);
}

/* Название и подпись прижаты друг к другу: интерлиньяж обоих (1.65) уже
   даёт воздух между строками, а прежний gap в 4px добавлял к нему ещё
   один зазор, и пара читалась как два отдельных пункта списка. */
.row__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.row__name {
  color: var(--text-strong);
  font-size: 1.125rem;
  font-weight: 500;
}

.row__note {
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.875rem;
}

/* Кольцо фокуса на строке панели рисуется внутрь строки: снаружи его
   срезал бы overflow панели, и первая строка потеряла бы индикатор
   фокуса целиком.

   Селектор с `.panel` — не украшение, а единственная причина, по которой
   правило вообще работает. Общее правило фокуса ниже перечисляет
   `.row:focus-visible` рядом с чипами и кнопками и объявляет
   `outline-offset: 2px`. У простого `.row:focus-visible` специфичность
   была бы такая же, (0,2,0), и побеждал бы более поздний по тексту —
   то есть общий отступ 2px, а кольцо срезал бы overflow панели. Лишний
   класс поднимает правило до (0,3,0) и делает отступ -2px независимым от
   порядка правил в файле: переставить блоки местами или перенести это
   правило выше общего больше ничего не сломает. В общий список фокуса
   `outline-offset` для `.row` возвращать нельзя. */
.panel .row:focus-visible { outline-offset: -2px; }

/* --- best of ------------------------------------------------------------ */

/* Рамка по кругу, а не полоса слева. Полоса рисовала у карточки одну
   сторону — приём, который читается как закладка или обрезанный край,
   а не как рамка. Тонкий контур по всему периметру держит карточку
   целиком и не спорит с панелью projects, у которой рамки нет вовсе. */
.spotlight {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

.spotlight__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.spotlight__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--accent);
}

.spotlight__icon img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.spotlight__name {
  font-family: var(--mono);
  font-size: 1rem;
  font-weight: 600;
  color: var(--accent);
}

.spotlight__note {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

.places {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.place {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  background: var(--surface-2);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.875rem;
  color: var(--text);
}

/* Год и место — атомарные подписи, и переносить их нельзя.

  На 320px панель отдаёт ленте мест 238px (288 полосы минус 2px рамки и
  48px полей), а плитка с местом занимает 208px. Флекс сжимает её, и
  сжимается не панель, а подписи внутри: перенос названия площадки ещё
  терпим, а «Top / 3» — это уже не перенос, а опечатка, и плитка
  становится вдвое выше соседних. С неразрывными подписями сжимается
  только название площадки, и то по границе слова.

  Перенос названия площадки терпим, но не решает задачи: полное «Standoff
  Red Team Scenarios» просит с плиткой 267px против 238px полосы, флекс
  сжимает плитку, и перенос падает на название — плитка 2024 становится
  54px против 31px у соседей (замерено в браузере на 320px). Поэтому в
  плитке стоит короткое «Standoff BRICS» (233px, запас 5px), а полное
  название унесено в title того же span, где оно не борется за ширину.
  Вернуть полное название в плитку нельзя: white-space: nowrap на
  .place__event убирает перенос, но не ширину — плитка станет 267px и
  перестанет влезать в ленту. Это ловит check_wrapping_tiles в
  tools/narrow_test.py. Подпись впритык: если место снова поедет —
  сокращать BRICS, не год и не место. */
.place__year { color: var(--muted); white-space: nowrap; }
.place__rank { color: var(--accent); font-weight: 600; white-space: nowrap; }

/* Фоновые места: приглушённая строка под панелью. Разница масштаба
   с панелью и есть весь смысл — Standoff читается первым без
   цветовых пятен.

   `margin: 0` — не украшение, а необходимое объявление. Отступ сверху
   должен нести контейнер: #best это .block, то есть флекс-колонка с
   gap: var(--space-4), и своего margin у .also быть не должно, иначе он
   сложится с gap. Но и убрать объявление целиком нельзя: .also это <p>,
   а в файле нет сброса для p (только body { margin: 0 }), поэтому без
   margin: 0 сверху вступает пользовательский агент — 1em, и отступ под
   панелью становится gap плюс эти 13px вместо одного gap. `margin: 0`
   обнуляет обе границы, и единственный отступ, который остаётся, —
   gap контейнера; внизу он тоже не нужен, там отступ до следующего
   блока задаёт уже сам .block. */
.also {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.75;
  color: var(--muted);
}

/* --- Footer ------------------------------------------------------------- */

/* Спека §3: копирайт и переключатель темы в подвале. Переключатель
   уехал из header, где он конкурировал с именем за внимание. */
.footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}

.footer__copy {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.875rem;
  color: var(--muted);
}

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

.theme-toggle {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  /* flex: none обязателен: без него кнопка — гибкий элемент, и при
     переносе текста рядом она сжимается; border-radius: 50% на
     вытянутом боксе рисует эллипс вместо круга — луна и солнце
     выглядели плющеными. */
  flex: none;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1),
              border-color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.theme-toggle__icon { display: none; }

/* Иконка не должна ни сжиматься, ни переполнять кнопку. Ширина и
   высота заданы явно, а не наследуются из HTML-атрибутов: без
   flex-shrink SVG внутри grid-сетки занимает всю доступную ячейку. */
.theme-toggle__icon {
  width: 18px;
  height: 18px;
  flex: none;
}

/* Какой знак видно — решает CSS по data-атрибуту, который скрипт
   ставит на кнопку: до первого клика атрибута data-theme на <html>
   нет, и media-выражением не отличить «тёмная потому что так в
   системе» от «тёмная потому что выбрали». */
.theme-toggle[data-theme='light'] .theme-toggle__icon--sun,
.theme-toggle[data-theme='dark'] .theme-toggle__icon--moon {
  display: block;
}

.theme-toggle:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.theme-toggle:active { background: var(--surface-2); }

/* --- Фокус -------------------------------------------------------------- */

/* Спека §7: 2px цветом акцента на всех интерактивах. Отступ 2px — для
   всех, кроме строк панели projects: их кольцо уходит внутрь строки, и
   это переопределяет правило `.panel .row:focus-visible` повышенной
   специфичности. */
.chip:focus-visible,
.row:focus-visible,
.fact__link:focus-visible,
.theme-toggle:focus-visible,
.skip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- Мобильный < 640px -------------------------------------------------- */

/* Спека §2.2 и §6: ниже 640px — одна колонка в порядке DOM. Области
   перечислены явно, а не сброшены в auto: порядок чтения обязан быть
   виден в коде, иначе возвращение порядка в DOM не заметит ни один
   прогон. Отступы секций сползают к нижней границе clamp() сами.

   Нижняя граница --space-section после правки 2026-10-09 — 3rem, то есть
   48px на телефоне против прежних 64px: seam стал плотнее, но не схлопнулся
   в ноль (замерено в браузере на 320px). Поэтому поля обёртки здесь
   оставлены на --space-5 (24px), а не опущены до --space-3: 24 и 48 —
   два порядка, и низ страницы не прилипает к краю окна. */
@media (max-width: 640px) {
  main {
    grid-template-areas:
      'about'
      'projects'
      'best'
      'elsewhere';
    grid-template-columns: 1fr;
  }

  #about    { margin-block-end: var(--space-section); }
  #projects { margin-block-end: var(--space-section); }

  /* В одной колонке шов над elsewhere уже даёт about, и второй такой же
     сверху сложился бы с ним в двойной промежуток. */
  #elsewhere { margin-block-start: 0; }

  /* У best на широком экране отступ сверху: он третий ряд и отделён от
     верхних блоков. В одной колонке над ним стоит projects, который уже
     отодвинут своим margin-block-end, а поля в гриде не схлопываются —
     без сброса промежуток projects → best вышел бы вдвое больше всех
     остальных. Сброс полный (0 сверху), а не уменьшение до --space-5:
     снизу best и так отодвинут своим --space-section, и на 320px это
     48px — ещё 48 сверху дали бы дыру в 96px подряд. */
  #best     { margin-block: 0 var(--space-section); }

  .hero__id {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }

  .wrapper { padding-block: var(--space-5); }
}

/* --- Reduced motion ----------------------------------------------------- */

/* Спека §7: движение при вестибулярных нарушениях — не украшение, а
   помеха, поэтому выключаются переходы. Подъёма на ховере на странице
   больше нет: он ушёл вместе с круглыми карточками и тенью, а отклик
   дают рамка и цвет. Гасить нечего, поэтому transform здесь не
   упоминается — иначе правило охраняло бы пустоту. Переходы при этом
   остались: строка панели projects меняет фон под курсором, как чип —
   цвет и рамку. */
@media (prefers-reduced-motion: reduce) {
  .chip, .row, .theme-toggle, .fact__link { transition: none; }
}

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

/* Визитку реально печатают — из неё делают визитки. Тёмный фон на
   печати съедает тонер, а текст, нарисованный --muted, при выключенных
   фонах печатающего диалога может оказаться белым на белом. Поэтому
   цвета подставляются явно, а не «надеемся, что браузер догадается». */
@media print {
  :root {
    --bg: #FFFFFF;
    --surface: #FFFFFF;
    --surface-2: #FFFFFF;
    --text: #000000;
    --text-strong: #000000;
    --muted: #333333;
    --border: #BBBBBB;
    --accent: #0369A1;
    --signal: #166534;

    /* Фирменные знаки печатались белилами по белому и просто исчезали:
       --brand-ink в тёмной теме это #FFFFFF, и на бумаге он неотличим
       от фона. Telegram-синий #229ED9 даёт на белом 2.68:1, то есть ниже
       порога даже для графики — на печати он тоже темнеет. Тонированная
       плитка аватара на бумаге не нужна вовсе. */
    --brand-ink: #000000;
    --tg: #157BAE;
    --accent-soft: #FFFFFF;

    /* Подложка на бумаге не нужна: сетка — фактура экрана, а на листе
       она либо не печатается вовсе, либо съедает тонер серой кашей.
       Гасится токенами, а не background-image: none, потому что те же
       токены могут пригодиться вложенным элементам. */
    --grid-line: transparent;
  }

  /* Гасим и сам background-image: фон по умолчанию печатать не
     отказывает, и явное правило надёжнее, чем надежда на диалог. */
  body {
    background: #FFFFFF;
    background-image: none;
  }

  /* Спека §7: на листе одна колонка в порядке чтения. Области названы
     явно, а не сброшены в auto — иначе возвращение двух колонок на
     печати не заметил бы ни один прогон. */
  main {
    grid-template-areas:
      'about'
      'projects'
      'best'
      'elsewhere';
    grid-template-columns: 1fr;
  }

  #about, #projects, #best { margin-block-end: var(--space-5); }

  /* У best сверху есть отступ, и он не от кого-то: широкая раскладка
     отодвигает третий ряд от двух верхних, потому что projects тянется
     на оба ряда правой колонки. На листе в одну колонку этого
     родства нет, а поле остаётся: медиа-условия не складываются,
     блок 640px на A4 (794px) и Letter (816px) не действует вовсе.
     Поля в гриде не схлопываются, поэтому промежуток projects → best
     сложился бы из двух — своего margin-block-end и чужого margin-block-start
     у best — и стал бы вдвое шире соседних.

     Числа в пояснении пришлось переписать вместе со шкалой 2026-10-09:
     --space-section теперь clamp(3rem, 6vw, 4.5rem), то есть 48px на A4
     (было 79px), и дыра над панелью Standoff вышла 72px против 24px у
     остальных промежутков на бумаге — тройка, а не четверка. Правило
     осталось: печатный блок под @media 640px не попадает, и снимать
     margin-block-start у best обязан он сам. */
  #best { margin-block-start: 0; }

  /* Шов перед elsewhere в печати даёт margin-block-end у best, как и на
     остальных стыках: колонка одна, и верхнее поле блока здесь было бы
     вторым отступом поверх первого. */
  #elsewhere { margin-block-start: 0; }

  /* Скругление панели держится на обрезании: на бумаге углы должны быть
     прямыми, и панель обязана отдать этот угол содержимому строк. */
  .panel {
    border-radius: 0;
    overflow: visible;
  }

  .spotlight { border-radius: 0; }

  /* Блок не должен рваться пополам между страницами. Тени на странице нет
     (--shadow убран редизайном), поэтому правило охраняет только разрыв. */
  .hero,
  .chip,
  .row,
  .spotlight,
  .panel { break-inside: avoid; }

  .theme-toggle { display: none; }

  a[href]::after {
    content: " (" attr(href) ")";
    font-size: 0.75em;
    color: #555555;
  }

  /* Адрес в конце ссылки должен помещаться на лист любого размера, а
     правило выше печатает его у КАЖДОЙ ссылки, включая чипы. На экране
     чип помещается в полосу, на бумаге — нет: .chip объявлен
     white-space: nowrap, а самый широкий адрес (max.ru/join/…) даёт
     вместе с подписью около 665px против 527px полосы A5 и 289px
     полосы визитки 85 мм. A4 проходит случайно, и до первого
     распечатанного черновика это не видно: на превью страница
     выглядит рабочей, а на бумаге вылезает за поля.

     Перенос — правильное решение, а не скрытие адреса: визитку
     печатают именно потому, что её потом отдают в руки, и адрес
     max.ru/join/… на карточке нужен больше, чем его целостность в одну
     строку. overflow-wrap: anywhere нужен отдельно от
     white-space: normal — в адресе нет пробелов, и по ним одним он не
     переносится ни при каком normal. */
  .chip {
    white-space: normal;
    overflow-wrap: anywhere;
  }
}