/* ══════════════════════════════════════════════════════════════════════════
   ДИЗАЙН-СИСТЕМА ЛИЧНОГО САЙТА СЛАВЫ УФИМЦЕВА
   Полное описание и вывод величин — в DESIGN-SYSTEM.md

   Заданных величин пять. Всё остальное — кегли, отступы, сетка, ширины —
   из них вычисляется. Ни одного подобранного числа.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── 1. ПЯТЬ ЗАДАННЫХ ВЕЛИЧИН ─────────────────────────────────────────── */

  --chars:      32;     /* мера плакатной строки, знаков. У jenniferheintz.com 31 */
  --adv:        0.540;  /* средняя ширина знака Literata на русском тексте.
                           Измерена в браузере, не взята из спецификации шрифта.
                           Зависит от оси opsz: 0.547 на 20px, 0.540 от 72px */
  --lh:         1.05;   /* интерлиньяж плаката */
  --lines-min:  9;      /* минимум строк в кадре — потолок для ультрашироких */
  --fine-ratio: 3;      /* мелкий кегль = плакатный, делённый на три */

  /* Трекинг плаката. Ноль осознанно: ось оптического размера Literata сама
     уплотняет крупный набор. Если задать — обязан войти в --adv-eff,
     иначе мера тихо съедет. */
  --track:   0;   /* ТОЛЬКО безразмерное число (0.01, а не 0.01em):
                     оно складывается с --adv, и единица сделает
                     --adv-eff и все зависимые размеры невалидными */
  --adv-eff: calc(var(--adv) + var(--track));

  /* ── 2. КЕГЛИ ─────────────────────────────────────────────────────────── */

  /* Требование: поле страницы равно ровно одной строке. Отсюда
     кегль × (знаки × ширина) + 2 × (кегль × интерлиньяж) = 100vw */
  --denom:   calc(var(--chars) * var(--adv-eff) + 2 * var(--lh));
  --fs:      min(calc(100vw / var(--denom)),
                 calc(100svh / (var(--lines-min) * var(--lh))));

  /* Пол в 17px: ниже него мелкий кегль перестаёт быть читаемым.
     Единственное место в системе, где стоит абсолютная величина. */
  --fs-fine: max(1.0625rem, calc(var(--fs) / var(--fine-ratio)));

  /* Интерлиньяж мелкого выведен так, чтобы ДВЕ его строки были равны
     ОДНОЙ плакатной. Даёт 1.575 — внутри читательской нормы 1.5–1.6. */
  --lh-fine: calc(var(--lh) * var(--fine-ratio) / 2);

  /* ── 3. ВЕРТИКАЛЬНЫЙ МОДУЛЬ ───────────────────────────────────────────── */

  /* Четверть плакатной строки = половина строки мелкого кегля.
     Все вертикальные отступы системы — целые кратные модуля. */
  --module: calc(var(--fs) * var(--lh) / 4);

  --space-1:  var(--module);
  --space-2:  calc(var(--module) * 2);   /* строка мелкого кегля */
  --space-3:  calc(var(--module) * 3);
  --space-4:  calc(var(--module) * 4);   /* плакатная строка, поле страницы */
  --space-6:  calc(var(--module) * 6);
  --space-8:  calc(var(--module) * 8);
  --space-12: calc(var(--module) * 12);

  /* ── 4. ГОРИЗОНТАЛЬНАЯ СЕТКА ──────────────────────────────────────────── */

  /* Десять колонок — не выбор, а единственное решение. Требование:
     колонка чтения (66 знаков мелким кеглем) должна лечь на границу
     колонок. Перебор N от 3 до 16 при межколоннике в модуль даёт
     единственный ответ с ошибкой меньше процента: N = 10, чтение = 7. */
  --cols:      10;
  --cols-read: 7;
  --gutter:    var(--module);

  --page-pad:  var(--space-4);
  --grid-w:    calc(var(--chars) * var(--adv-eff) * var(--fs));
  --col:       calc((var(--grid-w) - (var(--cols) - 1) * var(--gutter)) / var(--cols));
  --w-read:    calc(var(--cols-read) * var(--col)
                    + (var(--cols-read) - 1) * var(--gutter));
  --w-margin:  calc(var(--grid-w) - var(--w-read) - var(--gutter));

  /* ── 5. ГАРНИТУРЫ ─────────────────────────────────────────────────────── */

  /* Две на весь сайт. Literata несёт всю прозу — и плакатную, и документную.
     Golos Text — имена, разделы, метаданные, всё структурное. */
  --font-text: 'Literata', Georgia, 'Times New Roman', serif;
  --font-ui:   'Golos Text', -apple-system, 'Segoe UI', sans-serif;

  --weight-text:   400;
  --weight-strong: 600;
  --weight-ui:     400;
  --weight-ui-md:  500;

  /* ── 6. ЦВЕТ ──────────────────────────────────────────────────────────── */

  /* В покое страница чёрно-белая. Акцент показывается только
     во взаимодействии — приём с jenniferheintz.com. */
  --paper:     #FCFCFA;
  --ink:       #14110F;
  --ink-quiet: #6E6660;
  --rule:      #DCD6CB;

  /* Синяя рампа — единственный цвет сайта. Четыре оттенка получены шагом
     по кругу тонов OKLCH в 22° при ПОСТОЯННЫХ светлоте и насыщенности
     (L 0.45, C 0.15). Постоянная светлота — не украшение: она держит
     контраст к бумаге в узком коридоре 6.41…7.70 : 1, поэтому любой оттенок,
     оказавшийся под буквой, остаётся читаемым. */
  --blue-1: #006390;   /* oklch(.45 .15 222) — 6.41:1 */
  --blue-2: #0059A1;   /* oklch(.45 .15 244) — 6.95:1 */
  --blue-3: #2D4DA7;   /* oklch(.45 .15 266) — 7.49:1 */
  --blue-4: #5441A2;   /* oklch(.45 .15 288) — 7.70:1 */

  /* Акцент один на весь сайт: ссылка, кольцо фокуса, выделение, раскрывашка
     под курсором. Две разные акцентные краски на такой аскетичной странице
     читались бы как случайность. До появления ссылок здесь стоял
     терракотовый #9A2A1E — вернуть можно этой одной строкой. */
  --accent: var(--blue-3);

  /* Цвет ореола, которым ссылка прорезает свою волну на выносных элементах.
     Это цвет ПОДЛОЖКИ, а не краска: на цветной панели его придётся
     переопределить, иначе разрывы проступят светлыми засечками. */
  --halo: var(--paper);

  /* ── 7. ДВИЖЕНИЕ ──────────────────────────────────────────────────────── */

  --dur:  260ms;
  --ease: cubic-bezier(.22, .61, .36, 1);
  --flow: 2.2s;   /* полный проход цветовой волны по ссылке */
}

@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:     #131211;
    --ink:       #EDE9E1;
    --ink-quiet: #9A928A;
    --rule:      #3A3632;
    /* Та же дуга по тону, поднятая по светлоте: L 0.78, C 0.12.
       Контраст к тёмной бумаге 9.10…9.63 : 1. */
    --blue-1: #4CC8EE;
    --blue-2: #70BFFF;
    --blue-3: #93B5FF;
    --blue-4: #B4ABFF;
  }
  /* Сглаживание только на тёмном: на светлом фоне оно анемизирует кириллицу */
  body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
}

/* ══════════════════════════════════════════════════════════════════════════
   БАЗА
   ══════════════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; }

body {
  background: var(--paper);
  color: var(--ink);
  /* Базовый регистр — документный. Без этого любой новый тип контента
     (заголовок, список, цитата, подпись) получил бы стили браузера,
     потому что * { margin: 0 } уже снял их отступы. */
  font-family: var(--font-text);
  font-weight: var(--weight-text);
  font-size: var(--fs-fine);
  line-height: var(--lh-fine);
  padding-inline: var(--page-pad);
  padding-block: var(--page-pad) calc(var(--page-pad) * 1.5);
  font-optical-sizing: auto;
  font-synthesis: none;
  hanging-punctuation: first;
  overflow-wrap: break-word;
}

::selection { background: color-mix(in oklab, var(--accent) 18%, transparent); color: var(--ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 0.1em; }

/* Заготовки под будущие страницы: фотография и форма консультаций.
   Без них картинка со своей шириной даст горизонтальный вылет,
   а поля формы не унаследуют гарнитуру. */
img, picture, svg, video { max-inline-size: 100%; block-size: auto; }
figure { margin: 0; }
figcaption, .caption { font-family: var(--font-ui); font-size: max(0.875rem, calc(var(--fs-fine) * 0.82));
             line-height: var(--lh-fine); color: var(--ink-quiet); margin-top: var(--space-1); }
button, input, select, textarea { font: inherit; color: inherit; }

/* ══════════════════════════════════════════════════════════════════════════
   СЕТКА
   ══════════════════════════════════════════════════════════════════════════ */
.grid {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  column-gap: var(--gutter);
  max-width: var(--grid-w);
}

.span-full   { grid-column: 1 / -1; }
.span-read   { grid-column: 1 / calc(var(--cols-read) + 1); }
.span-margin { grid-column: calc(var(--cols-read) + 1) / -1; }

/* ══════════════════════════════════════════════════════════════════════════
   ДВА ТИПОГРАФИЧЕСКИХ РЕГИСТРА

   Плакат — то, что видно при загрузке. Один кегль, иерархия строится
   сменой гарнитуры. Документ — то, что читают: мелкий кегль, мера 66 знаков.
   Третьего регистра в системе нет.
   ══════════════════════════════════════════════════════════════════════════ */

.poster {
  font-family: var(--font-text);
  font-weight: var(--weight-text);
  font-size: var(--fs);
  line-height: var(--lh);
  letter-spacing: calc(var(--track) * 1em);
  max-width: var(--grid-w);
  font-variant-numeric: oldstyle-nums proportional-nums;
}
.poster > * + *  { margin-top: var(--space-1); }
.poster p + p    { margin-top: var(--space-3); }
.poster p        { text-wrap: pretty; }

.doc {
  font-family: var(--font-text);
  font-weight: var(--weight-text);
  font-size: var(--fs-fine);
  line-height: var(--lh-fine);
  letter-spacing: 0;
  max-width: var(--w-read);
  font-variant-numeric: oldstyle-nums proportional-nums;
}
.doc p + p { margin-top: var(--space-2); }
.doc p     { text-wrap: pretty; }

/* Структурный голос: имена, разделы, метаданные, даты */
.ui {
  font-family: var(--font-ui);
  font-weight: var(--weight-ui);
  letter-spacing: -0.021em;
}

.meta {
  font-family: var(--font-ui);
  font-weight: var(--weight-ui-md);
  /* Метки и даты — не проза: их сканируют, а не читают, поэтому кегль
     мельче документного. Пол 14px, ниже метка перестаёт опознаваться. */
  font-size: max(0.875rem, calc(var(--fs-fine) * 0.82));
  line-height: var(--lh-fine);
  letter-spacing: 0.004em;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-quiet);
}

em, i           { font-style: italic; }
strong, b       { font-weight: var(--weight-strong); }

/* ══════════════════════════════════════════════════════════════════════════
   ССЫЛКИ

   В покое — волнистое подчёркивание цвета текста. Под курсором и под фокусом
   текст берёт акцент, а по волне едут оттенки синего. Приём с jenniferheintz.com,
   но там он собран на <canvas> и JavaScript, а здесь целиком на CSS.

   ГЕОМЕТРИЯ ВОЛНЫ ВЫВЕДЕНА ИЗ САМОЙ ГАРНИТУРЫ:
     обводка = волосяная линия Literata             → 0.019em
               замерена попиксельно, как и --adv. Волна должна читаться
               как штрих того же шрифта, а не как графический элемент
               поверх набора: первая редакция взяла 0.045em, в два с половиной
               раза жирнее самого тонкого штриха, и полоса перевешивала текст
     период  = средняя ширина знака --adv           → 0.540em
               одно колебание на знак: у подчёркивания тот же ритм, что у текста
     полоса  = --descent минус --underline-offset   → 0.135em
               зона выносных элементов: от высоты прямого подчёркивания
               до низа метрик шрифта
     глубина = верхняя половина полосы              → 0.068em
               НИЖНЯЯ ПОЛОВИНА ОСТАЁТСЯ ВОЗДУХОМ. При плакатном интерлиньяже
               1.05 тушь следующей строки начинается с 0.35em под базовой;
               волна на всю полосу оставляла просвет 0.055em и упиралась
               в неё, на половине полосы просвет 0.122em

   ПОЛОЖЕНИЕ ПО ВЕРТИКАЛИ — НЕ ЧИСЛО, А КРАЙ БОКСА. background-position
   ... bottom сажает полосу на нижнюю границу метрик шрифта. Глубина самой
   волны внутри полосы — свойство спрайта, а не CSS, поэтому мельче волна
   становится за счёт воздуха снизу, а её верх остаётся там, где раньше
   проходило прямое подчёркивание. Сменится гарнитура — волна переедет сама.

   ФОНОВЫЙ СЛОЙ ОДИН, И ЭТО ВАЖНО. Была редакция с двумя слоями и послойным
   background-clip: рампа обрезалась по глифам, волна красилась по боксу, —
   от неё пришлось отказаться ради прорезания (см. ниже). Побочная выгода:
   исчез целый класс риска, при котором текст ссылки пропадал бы на движке
   без обрезки по глифам. Разметка — голый <a>, поэтому ссылки из markdown
   в будущем блоге не потребуют постобработки.

   ВСЯ ГЕОМЕТРИЯ СЧИТАНА С LITERATA. Если Google Fonts не ответит и подставится
   Georgia, у неё другие метрики: период разойдётся с шириной знака, полоса —
   с нижней границей выносных элементов, а ореол может иначе резать «р» и «у».
   Вёрстка не рассыплется, но приём перестанет быть выведенным. Снимается
   самохостингом шрифта.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Волосяная линия Literata — 0.0185em на плакатном кегле, 0.0202em на
     документном (замерено попиксельно по канве, как и --adv). Округлено
     до 0.019em: одно значение на оба регистра. */
  --hairline: 0.019em;

  /* Нижняя граница метрик Literata под базовой линией (замерено) и высота,
     на которой раньше проходило прямое подчёркивание. */
  --descent:  0.295em;
  --underline-offset: 0.16em;

  /* Полоса выносных элементов: от подчёркивания до низа метрик. Волна живёт
     в ВЕРХНЕЙ её половине, нижняя остаётся воздухом — иначе при плакатном
     интерлиньяже 1.05 волна упирается в тушь следующей строки. Полоса задаёт
     высоту фона; глубина самой волны — свойство спрайта, а не CSS. */
  --wave-band: calc(var(--descent) - var(--underline-offset));

  /* Радиус ореола, которым буква прорезает волну. Единственное число здесь,
     подобранное глазом: 0.045em — это 3.3px на плакатном кегле и 1.1px
     на документном. Диагональные тени берут 0.707 радиуса, иначе ореол
     вышел бы квадратным, а не круглым. */
  --halo-r: 0.045em;
  --halo-d: calc(var(--halo-r) * 0.707);

  /* Одно колебание на средний знак: у подчёркивания тот же ритм, что у текста. */
  --wave-period: calc(var(--adv-eff) * 1em);
  --wave-len:    calc(var(--wave-period) * 8);   /* длина спрайта = длина цветовой волны */

  --wave-ink: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 6' preserveAspectRatio='none'><path d='M0 1.5q3 -2.156 6 0t6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0' fill='none' stroke='%2314110F' stroke-width='0.844'/></svg>");
  --wave-blue: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 6' preserveAspectRatio='none'><defs><linearGradient id='g' x1='0' y1='0' x2='96' y2='0' gradientUnits='userSpaceOnUse'><stop offset='0.0000' stop-color='%23006390'/><stop offset='0.1667' stop-color='%230059A1'/><stop offset='0.3333' stop-color='%232D4DA7'/><stop offset='0.5000' stop-color='%235441A2'/><stop offset='0.6667' stop-color='%232D4DA7'/><stop offset='0.8333' stop-color='%230059A1'/><stop offset='1.0000' stop-color='%23006390'/></linearGradient></defs><path d='M0 1.5q3 -2.156 6 0t6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0' fill='none' stroke='url(%23g)' stroke-width='0.844'/></svg>");

  /* Рампа-палиндром: крайние стопы одинаковы, поэтому плитка стыкуется
     без шва и цвет может ехать бесконечно, не дёргаясь на стыке. */
  --ramp: linear-gradient(90deg,
    var(--blue-1) 0%, var(--blue-2) 16.67%, var(--blue-3) 33.33%, var(--blue-4) 50%,
    var(--blue-3) 66.67%, var(--blue-2) 83.33%, var(--blue-1) 100%);
}

@media (prefers-color-scheme: dark) {
  :root {
  --wave-ink: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 6' preserveAspectRatio='none'><path d='M0 1.5q3 -2.156 6 0t6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0' fill='none' stroke='%23EDE9E1' stroke-width='0.844'/></svg>");
  --wave-blue: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 6' preserveAspectRatio='none'><defs><linearGradient id='g' x1='0' y1='0' x2='96' y2='0' gradientUnits='userSpaceOnUse'><stop offset='0.0000' stop-color='%234CC8EE'/><stop offset='0.1667' stop-color='%2370BFFF'/><stop offset='0.3333' stop-color='%2393B5FF'/><stop offset='0.5000' stop-color='%23B4ABFF'/><stop offset='0.6667' stop-color='%2393B5FF'/><stop offset='0.8333' stop-color='%2370BFFF'/><stop offset='1.0000' stop-color='%234CC8EE'/></linearGradient></defs><path d='M0 1.5q3 -2.156 6 0t6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0 6 0' fill='none' stroke='url(%23g)' stroke-width='0.844'/></svg>");
  }
}

@keyframes wave-flow {
  to { background-position: calc(-1 * var(--wave-len)) bottom; }
}

a {
  color: inherit;
  text-decoration: none;
  background-image: var(--wave-ink);
  background-repeat: repeat-x;
  background-position: left bottom;
  background-size: var(--wave-len) var(--wave-band);
  /* Иначе фон на перенесённой строке считается от «несуществующего»
     общего бокса и волна на втором отрезке начинается с середины периода. */
  -webkit-box-decoration-break: clone;
          box-decoration-break: clone;

  /* ПРОРЕЗАНИЕ ВОЛНЫ НА ВЫНОСНЫХ ЭЛЕМЕНТАХ — замена нативному
     text-decoration-skip-ink, которого у фоновой картинки нет.

     Восемь копий глифа цветом подложки, разложенных по кругу радиусом
     --halo-r, дают ореол: тени рисуются ПОВЕРХ фона, но ПОД заливкой буквы,
     поэтому пробивают в волне разрыв по форме глифа, не трогая саму букву.
     Ныряют под волну только «р» и «у» (0.216em под базовой); «д», «ц», «щ»
     заканчиваются на 0.148em, выше волны, и разрыва не вызывают.

     Именно text-shadow, а не -webkit-text-stroke с paint-order: тот работает
     не хуже, но остаётся нестандартным свойством в паре со свойством родом
     из SVG. text-shadow — обычный CSS без префиксов, и глиф он не трогает
     вообще, тогда как обводка залезает под заливку буквы. */
  text-shadow:
    var(--halo-r) 0 var(--halo),  calc(-1 * var(--halo-r)) 0 var(--halo),
    0 var(--halo-r) var(--halo),  0 calc(-1 * var(--halo-r)) var(--halo),
    var(--halo-d) var(--halo-d) var(--halo),
    calc(-1 * var(--halo-d)) var(--halo-d) var(--halo),
    var(--halo-d) calc(-1 * var(--halo-d)) var(--halo),
    calc(-1 * var(--halo-d)) calc(-1 * var(--halo-d)) var(--halo);
}

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

   ЦВЕТ ЕДЕТ ПО ВОЛНЕ, А НЕ ПО БУКВАМ, И ЭТО ВЫНУЖДЕННО. Бегущий по буквам
   цвет требует background-clip: text, то есть заливка буквы приходит
   из ФОНОВОГО слоя. Ореол прорезания рисуется поверх фона и стирает такие
   буквы начисто — проверено, от слова остаются одни кавычки. Порядок
   отрисовки не обойти, поэтому приходится выбирать: либо разрывы волны
   на выносных элементах, либо перемещение цвета по буквам. Постоянный
   дефект перевесил мимолётный эффект, тем более что на тачскрине
   наведения нет вовсе. Вариант с обратным выбором — на proto-skip.html.

   Перехода по color здесь СОЗНАТЕЛЬНО нет: смена мгновенная в обе стороны,
   иначе на уходе курсора буква и волна разъезжаются по времени. */
a:hover, a:focus-visible, a:active {
  background-image: var(--wave-blue);
  color: var(--accent);
  animation: wave-flow var(--flow) linear infinite;
}

/* ══════════════════════════════════════════════════════════════════════════
   ПОРТРЕТ В СТРОКЕ ЗАГОЛОВКА

   Фотография стоит в конце имени как ещё одна буква. Никакой отдельной
   вертикальной подгонки для этого не нужно: у строчного блока с overflow
   не visible базовой линией считается его НИЖНИЙ край. Значит, при
   vertical-align: baseline портрет садится на базовую линию сам.

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

   ГАБАРИТ ВЫВЕДЕН ИЗ ПРОПИСНОЙ. По высоте прописной равна не вся фигура,
   а СРЕДНЯЯ окружность контура: доли симметрично выходят за неё наружу
   и внутрь — ровно так же овал круглой буквы перекрывает базовую линию
   и линию прописных. Отсюда --mug-d = --cap-ui × (1 + a) = 0,81 em.

   Сверху величину ограничивает требование «заголовок помещается в одну
   строку», и это неравенство, а не пожелание. Мера заголовка — 8,64 em
   его кегля (32 знака плакатной меры пополам, потому что заголовок вдвое
   крупнее). Имя с пробелом и портретом занимает 8,39 em на Golos Text
   и 8,45 на самой широкой подмене — запас 2,9% и 2,2%. Тот же расчёт
   для портрета в кегельную площадку даёт 8,58 и 8,64: на подмене ровно
   впритык, то есть не помещается. Замер по стеку подмен — в девятой группе
   checks/verify.mjs; полным он быть не может ни на какой одной машине:
   отсутствующую гарнитуру браузер молча заменяет системной, а родовой
   sans-serif — это тоже лишь системный шрифт текущей ОС, а не худший случай
   вообще. Сравнение величин — на proto-photo2.html.

   ФОРМА — ТА ЖЕ ВОЛНА, ЧТО У ПОДЧЁРКИВАНИЯ. Край портрета идёт долями,
   выведенными из тех же величин, что и волна ссылки: отношение амплитуды
   к периоду сохранено, а число долей не выбрано — длина средней окружности
   делится на период волны и даёт 4,13, то есть четыре. Отсюда
   относительная амплитуда a = 2π × (амплитуда/период) / 4 = 0,1411.

   Простой круг остался на proto-photo.html: он аккуратнее, но безлик,
   а волнистый край связывает портрет с единственным другим рисованным
   элементом системы.
   ══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Высота прописной Golos Text. Замерена по канве, как --adv и --hairline:
     из спецификации шрифта не берётся. */
  --cap-ui: 0.71em;

  /* Относительная амплитуда долей — выведена, см. шапку раздела. */
  --mug-amp: 0.1411;

  /* Габарит портрета. По прописной равна СРЕДНЯЯ окружность контура,
     а доли симметрично выходят за неё и внутрь: ровно так же ведут себя
     круглые буквы, у которых овал перекрывает базовую линию и линию
     прописных. Поэтому габаритом служит средняя окружность плюс амплитуда. */
  --mug-d: calc(var(--cap-ui) * (1 + var(--mug-amp)));

  --mug-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'><path d='M100.00 50.00C100.00 53.13 98.85 56.61 97.26 59.40C95.68 62.20 92.89 64.68 90.48 66.77C88.07 68.85 85.11 70.27 82.80 71.91C80.49 73.55 78.43 74.80 76.61 76.61C74.80 78.43 73.55 80.49 71.91 82.80C70.27 85.11 68.85 88.07 66.77 90.48C64.68 92.89 62.20 95.68 59.40 97.26C56.61 98.85 53.13 100.00 50.00 100.00C46.87 100.00 43.39 98.85 40.60 97.26C37.80 95.68 35.32 92.89 33.23 90.48C31.15 88.07 29.73 85.11 28.09 82.80C26.45 80.49 25.20 78.43 23.39 76.61C21.57 74.80 19.51 73.55 17.20 71.91C14.89 70.27 11.93 68.85 9.52 66.77C7.11 64.68 4.32 62.20 2.74 59.40C1.15 56.61 0.00 53.13 0.00 50.00C0.00 46.87 1.15 43.39 2.74 40.60C4.32 37.80 7.11 35.32 9.52 33.23C11.93 31.15 14.89 29.73 17.20 28.09C19.51 26.45 21.57 25.20 23.39 23.39C25.20 21.57 26.45 19.51 28.09 17.20C29.73 14.89 31.15 11.93 33.23 9.52C35.32 7.11 37.80 4.32 40.60 2.74C43.39 1.15 46.87 0.00 50.00 0.00C53.13 0.00 56.61 1.15 59.40 2.74C62.20 4.32 64.68 7.11 66.77 9.52C68.85 11.93 70.27 14.89 71.91 17.20C73.55 19.51 74.80 21.57 76.61 23.39C78.43 25.20 80.49 26.45 82.80 28.09C85.11 29.73 88.07 31.15 90.48 33.23C92.89 35.32 95.68 37.80 97.26 40.60C98.85 43.39 100.00 46.87 100.00 50.00Z' fill='%23fff'/></svg>");

  /* Зерно для замаранного текста. feTurbulence даёт шум, feColorMatrix
     переводит его в прозрачность: маска получается дырявой, и размытый
     текст под ней рассыпается крупой. Плитка 128px повторяется. */
  --noise: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1.4 0 0 0 -0.15'/></filter><rect width='128' height='128' filter='url(%23n)'/></svg>");
}

/* Заголовок с портретом не переносится. Правило вешается на наличие
   портрета, а не на класс в разметке, — так его нельзя забыть.
   Там, где :has() не поддержан, правило просто отпадает и заголовок
   переносится: вёрстка не ломается, приём теряет строгость. */
h1:has(.mugshot) { white-space: nowrap; }

.mugshot {
  display: inline-block;
  vertical-align: baseline;
  position: relative;
  /* Не декорация, а то, что задаёт боксу базовую линию по нижнему краю. */
  overflow: hidden;
  inline-size: var(--mug-d);
  block-size: var(--mug-d);
  -webkit-mask-image: var(--mug-mask);
          mask-image: var(--mug-mask);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  /* Если картинка не пришла, на её месте останется пятно, а не дыра
     посреди имени. */
  background: var(--rule);
}

/* Там, где маски нет, портрет остался бы квадратом. Круг ближе к замыслу,
   чем квадрат, поэтому он и подставляется. Вместе с маской его задавать
   нельзя: скругление обрезало бы доли, и от волнистого края ничего
   не осталось бы. */
@supports not ((mask-image: url('')) or (-webkit-mask-image: url(''))) {
  .mugshot { border-radius: 50%; }
}

.mugshot img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

/* Дурашливая версия лежит вторым слоем и всегда загружена: подмена src
   по наведению дала бы паузу на загрузку ровно в тот момент, когда шутка
   должна сработать. При prefers-reduced-motion общее правило обнуляет
   длительность, и версии просто меняются мгновенно. */
.mugshot__doodle {
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.mugshot:hover .mugshot__doodle,
.mugshot:active .mugshot__doodle { opacity: 1; }

/* ══════════════════════════════════════════════════════════════════════════
   РАСКРЫВАШКИ

   Нативный <details>: работают без JavaScript, состояние озвучивается
   скринридером без ARIA-костылей. Атрибут name НЕ выставлять — с ним
   соседний раздел автозакрывается.
   ══════════════════════════════════════════════════════════════════════════ */
.fold { margin-top: var(--space-3); }

.fold__head {
  font-family: var(--font-ui);
  font-weight: var(--weight-ui);
  letter-spacing: -0.021em;
  cursor: pointer;
  list-style: none;
  display: block;
  /* Тач-цель. На десктопе строка плаката и так под 80px, отступ выходит
     нулевым; на телефоне добирает до 44px. Без брейкпоинта. */
  padding-block: max(0px, calc((2.75rem - 1em * var(--lh)) / 2));
  transition: color var(--dur) var(--ease);
}
.fold__head::-webkit-details-marker { display: none; }
.fold__head::marker { content: ''; }
.fold__head:hover { color: var(--accent); }
/* Заголовок внутри summary нужен для навигации скринридера по разделам,
   но не должен становиться блоком и уводить индикатор на свою строку. */
.fold__head > h2 { display: inline; font: inherit; letter-spacing: inherit; }

/* Индикатор — типографский знак в кегле строки, а не иконка интерфейса */
.mark {
  font-family: var(--font-ui);
  font-size: 1em;
  color: var(--ink-quiet);
  margin-inline-start: 0.34em;
  font-variant-numeric: lining-nums;
  transition: color var(--dur) var(--ease);
}
.mark::after { content: '+'; }
.fold[open] .mark::after { content: '\2212'; }
.fold__head:hover .mark { color: var(--accent); }

.fold__panel { padding-block: var(--space-2) var(--space-3); }

.fold::details-content {
  block-size: 0;
  overflow: hidden;
  opacity: 0;
  transition:
    block-size var(--dur) var(--ease),
    content-visibility var(--dur) var(--ease) allow-discrete,
    opacity 200ms var(--ease);
}
.fold[open]::details-content { block-size: auto; opacity: 1; }


/* ══════════════════════════════════════════════════════════════════════════
   ПРИМЕЧАНИЕ В СТРОКЕ

   Наследник журнальных «звёздочек» [[[текст]]]. Закрытое — знак сноски
   в строке; по клику знак уступает место примечанию, абзац переверстывается.
   Клик по раскрытому тексту закрывает обратно.

   Два правила, каждое проверено, а не взято на глаз.

   1. ЗНАК — обычная звёздочка U+002A. Красивые ✳ U+2733 и ★ U+2605
      отсутствуют и в Literata, и в Golos Text: браузер подставляет
      системный шрифт, и знак выглядит по-разному на разных платформах.
      U+002A есть в обеих гарнитурах.

   2. ПЕРЕД ЗНАКОМ В РАЗМЕТКЕ ОБЯЗАН СТОЯТЬ НЕРАЗРЫВНЫЙ ПРОБЕЛ.
      С обычным звёздочка отрывается от своего слова и повисает в начале
      следующей строки: перебор 201 ширины колонки дал 33 таких ширины.
      У нас кегль привязан к ширине экрана, поэтому достижима любая.
      Пробел стоит в абзаце, а не внутри компонента, — поэтому достаётся
      и знаку, и раскрытому примечанию, которому он тоже нужен.

   3. ЗНАК — <span role="button">, а НЕ <button>. Это не небрежность:
      у <button> вычисленный display остаётся inline-block, даже если
      явно задать inline, а перед атомарным строчным боксом браузер ставит
      перенос, игнорируя неразрывный пробел. Проверено перебором 261 ширины:
      <button> отрывается 6 раз, <span> и <label> — ни разу.
      Клавиатуру (Tab, Enter, Space) добирает asides.js.
   ══════════════════════════════════════════════════════════════════════════ */
.aside__mark {
  display: inline;
  font-family: var(--font-ui);
  color: var(--accent);
  cursor: pointer;
  user-select: none;
  transition: color var(--dur) var(--ease);
}
.aside__mark:hover { color: var(--ink); }

.aside__text {
  display: none;
  font-style: italic;
  color: var(--ink-quiet);
  cursor: pointer;
}

.aside.is-open > .aside__mark { display: none; }
.aside.is-open > .aside__text { display: inline; }

/* Отказ скрипта: знак прячется, примечания показаны раскрытыми.
   Свернуть нельзя, но ни одно слово не потеряно. */
html.no-js .aside__mark { display: none; }
html.no-js .aside__text { display: inline; }

/* ══════════════════════════════════════════════════════════════════════════
   ХРОНОЛОГИЯ

   Колонка дат — max-content, то есть ровно ширина самой длинной даты.
   Ни линий таймлайна, ни кружков, ни бейджей.
   ══════════════════════════════════════════════════════════════════════════ */
/* Той же сеткой набраны итоги кейса (.facts) и списки работ: там слева тоже
   короткая метка, справа текст. Форма одна, потому что задача одна — колонка
   меток ровно своей ширины и колонка прозы. Отдельного компонента для этого
   заводить не за что. */
.timeline, .facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: var(--space-4);
  row-gap: var(--space-3);
  align-items: baseline;
  max-width: none;
}
.timeline dt, .facts dt { white-space: nowrap; }
.timeline dd, .facts dd { max-width: var(--w-read); text-wrap: pretty; }

/* ══════════════════════════════════════════════════════════════════════════
   СТАТЬЯ

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

   На плакате иерархия строится одной сменой гарнитуры, и там этого хватает:
   имя набрано вдвое крупнее всего остального просто потому, что это плакат.
   В статье не хватило — гротеск в кегле прозы сливался с ней, и подзаголовок
   переставал опознаваться заголовком. Поэтому у раздела свой кегль,
   и он выведен: ПОЛОВИНА плакатного, то есть документный, умноженный
   на --fine-ratio / 2. Тот же множитель 3/2 уже стоит в интерлиньяже
   документа, своего числа заголовок не заводит.

   Отбивки взяты из готовой шкалы. Между абзацами — один модуль, то есть
   ПОЛОВИНА строки: полная строка читается пустой строкой между абзацами,
   а нужен отступ. Дальше кратности того же модуля: вдвое у отдельной строки
   со ссылкой, втрое вокруг картинки, вшестеро перед разделом. Разрыв перед
   разделом обязан быть заметно больше разрыва вокруг картинки, иначе
   картинка в конце раздела читается началом следующего.
   ══════════════════════════════════════════════════════════════════════════ */
/* Отбивка от заголовочного блока — плакатная строка: смена регистра должна
   читаться крупнее, чем разрыв между абзацами внутри одного регистра. */
.article { margin-top: var(--space-4); }
.article > * + * { margin-top: var(--space-1); }

.article h2 {
  font-family: var(--font-ui);
  font-weight: var(--weight-ui);
  letter-spacing: -0.021em;
  font-size: calc(var(--fs-fine) * var(--fine-ratio) / 2);
  line-height: var(--lh);
  margin-top: var(--space-6);
}
.article h2 + * { margin-top: var(--space-2); }

.article figure { margin-top: var(--space-3); }
.article figure + * { margin-top: var(--space-3); }
.article img { display: block; inline-size: 100%; background: var(--rule); }

/* Список шагов. Колонка номеров — max-content, ровно ширина самого длинного
   номера, как колонка дат в хронологии. Номера выравниваются между пунктами
   через subgrid: без него каждый пункт считал бы свою ширину колонки сам
   и номера разъехались бы на двузначных. */
.steps, .bullets {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: var(--space-4);
  row-gap: var(--space-2);
  /* Пункты списка сканируют, а не читают, — как метки и даты. Поэтому
     и кегль тот же, что у метаданных: документный, умноженный на 0.82. */
  font-size: max(0.875rem, calc(var(--fs-fine) * 0.82));
  counter-reset: step;
  list-style: none;
  /* Браузерный отступ списка снят: сброс в начале файла обнуляет поля,
     но не внутренние отступы, и без этого колонка номеров вставала
     на сорок пикселей правее текста вокруг. */
  padding-inline-start: 0;
  max-width: none;
}
.steps li, .bullets li {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: baseline;
}
.steps li::before {
  counter-increment: step;
  content: counter(step);
  font-family: var(--font-ui);
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink-quiet);
}
/* Маркер — тире, а не «указующий перст» из оригинала и не буллет.
   Причина та же, по которой знак примечания — обычная звёздочка:
   ☞ и • отсутствуют в Literata, браузер подставил бы системный шрифт,
   и в строке появился бы чужой знак. Тире есть, и оно нормально
   для русского списка. */
.bullets li::before { content: '\2014'; color: var(--ink-quiet); }
.steps p + p, .bullets p + p { margin-top: var(--space-2); }

/* ══════════════════════════════════════════════════════════════════════════
   КОД

   Третьей гарнитуры система не заводит: моноширинная берётся системная
   через ui-monospace. Это не выбранный шрифт, а «чем машина показывает код».
   Кегль — тот же, что у метаданных и списков: код сканируют.

   Полоса прокрутки внутри блока обязательна. Строка кода не переносится,
   и без overflow-x длинный листинг растянул бы страницу горизонтально —
   ровно то, что проверка вылета ловит на семи страницах.
   ══════════════════════════════════════════════════════════════════════════ */
.code {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: max(0.875rem, calc(var(--fs-fine) * 0.82));
  line-height: var(--lh-fine);
  background: color-mix(in oklab, var(--rule) 38%, var(--paper));
  padding: var(--space-2);
  overflow-x: auto;
  white-space: pre;
  tab-size: 2;
  max-width: var(--w-read);
}
code { font-family: inherit; }
/* Инлайновый код без боковых отступов: с ними подложка отодвигала
   следующий за кодом знак препинания, и в строке появлялась дыра. */
p code, li code, dd code {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: color-mix(in oklab, var(--rule) 38%, var(--paper));
  padding-block: 0.1em;
}

/* ══════════════════════════════════════════════════════════════════════════
   РАЗГОВОР

   Реплика с портретом собеседника. Портрет ровно в две строки документа
   высотой — не подобранный размер, а два готовых интерлиньяжа. Имя набрано
   как метаданные: это метка, а не проза.
   ══════════════════════════════════════════════════════════════════════════ */
.talk {
  display: grid;
  grid-template-columns: calc(var(--lh-fine) * 2em) minmax(0, 1fr);
  column-gap: var(--space-2);
  align-items: start;
  max-width: var(--w-read);
}
.talk + .talk { margin-top: var(--space-2); }
.talk img {
  inline-size: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  background: var(--rule);
}
.talk__name { display: block; }

/* ══════════════════════════════════════════════════════════════════════════
   ЦИТАТА

   Втянута на плакатную строку и подписана метаданными. Ни кавычек-лапок
   поверх, ни линейки слева: втяжки и подписи достаточно, чтобы отделить
   чужую речь от своей.
   ══════════════════════════════════════════════════════════════════════════ */
.quote {
  padding-inline-start: var(--space-4);
  max-width: var(--w-read);
}
.quote > * + * { margin-top: var(--space-1); }

/* Демонстрационная кнопка из статьи про Тильду: она сама и есть пример,
   поэтому воспроизведена своим кодом, а не показана картинкой. */
.demo-btn {
  font-family: var(--font-ui);
  font-size: max(0.875rem, calc(var(--fs-fine) * 0.82));
  color: var(--paper);
  background: var(--ink);
  border: 0;
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: transform 400ms var(--ease);
}
.demo-btn:hover { transform: scale(1.1); }
@media (prefers-reduced-motion: reduce) {
  .demo-btn { transition: none; }
}

/* Блоки, которые не проза, отбиваются от прозы заметнее абзаца:
   список, листинг, цитата и реплика — это врезки в поток. */
.article > .steps, .article > .bullets, .article > .code,
.article > .quote, .article > .talk { margin-top: var(--space-3); }
.article > .steps + *, .article > .bullets + *, .article > .code + *,
.article > .quote + *, .article > .talk + * { margin-top: var(--space-3); }
/* Реплики одного разговора идут плотнее — это одна сцена, а не пять врезок. */
.article > .talk + .talk { margin-top: var(--space-2); }

/* Отдельная строка со ссылкой — то же, что подпись под картинкой,
   только без картинки. Поэтому и набрана как подпись. */
.article .caption { margin-top: var(--space-2); }
.article .caption + * { margin-top: var(--space-2); }

/* ══════════════════════════════════════════════════════════════════════════
   ЗАМАРАННОЕ

   Цифры себестоимости на публичной странице — конфликтная материя, но
   вымарывать их молча скучно. Текст размывается и засыпается шумом,
   по наведению поверх проступает объяснение.

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

   Радиус размытия и зерно шума подобраны глазом и записаны в исключения:
   это оформление приёма, а не геометрия системы.
   ══════════════════════════════════════════════════════════════════════════ */
.redacted {
  position: relative;
  /* Блок, а не строчный блок: у строчного базовая линия приходится
     на ПОСЛЕДНЮЮ строку, и метка слева в сетке итогов вставала вровень
     с третьей строкой замаранного текста вместо первой. */
  display: block;
  cursor: help;
}
.redacted__ink {
  filter: blur(0.19em);
  /* Шум поверх размытого текста: собственная зернистость, чтобы размытие
     не читалось «страница не догрузилась». */
  -webkit-mask-image: var(--noise);
          mask-image: var(--noise);
  mask-size: 128px 128px;
  -webkit-mask-size: 128px 128px;
  user-select: none;
  transition: opacity var(--dur) var(--ease);
}
.redacted__note {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  text-align: center;
  font-family: var(--font-ui);
  font-size: max(0.875rem, calc(var(--fs-fine) * 0.82));
  color: var(--ink-quiet);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.redacted:hover .redacted__ink,
.redacted:focus-visible .redacted__ink { opacity: 0.12; }
.redacted:hover .redacted__note,
.redacted:focus-visible .redacted__note { opacity: 1; }

/* ══════════════════════════════════════════════════════════════════════════
   ПОДВАЛ

   Подвалов два по содержанию и один по набору. Контакт (.contact) зовёт
   написать, колофон (.colophon) подписывает гарнитуры; на главной стоит
   первый, на специмене и прототипах второй. Оформление у них общее, потому
   что подвал — это регистр, а не тема: гротеск, приглушённая краска и кегль
   --fs-fine, то есть ровно треть плакатного. Отношение уже выведено, своего
   числа подвал не заводит.

   Отбивка --space-6 вдвое больше разрыва между разделами, то есть подвал
   отделяется от страницы заметнее, чем разделы друг от друга.
   ══════════════════════════════════════════════════════════════════════════ */
.contact,
.colophon {
  margin-top: var(--space-6);
  font-family: var(--font-ui);
  font-size: var(--fs-fine);
  line-height: var(--lh-fine);
  color: var(--ink-quiet);
  max-width: var(--w-read);
}

/* ══════════════════════════════════════════════════════════════════════════
   АДАПТИВ

   Единственный брейкпоинт на всю систему. Плакатная мера означает, что
   на телефоне кегль падает примерно до 20px — плаката как объекта больше
   нет, есть просто текст. Поэтому хронология раскладывается в одну колонку.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 34rem) {
  .timeline, .facts { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
  .timeline dt, .facts dt { margin-top: var(--space-3); }
  .timeline dd, .facts dd { margin-top: var(--space-1); }
  .timeline dt:first-of-type, .facts dt:first-of-type { margin-top: 0; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 1ms !important; }
  .fold::details-content { transition: none; }
  /* Цвет остаётся, движение убирается: ссылка под курсором по-прежнему
     синяя и по-прежнему опознаётся, просто оттенки больше не едут. */
  a { animation: none !important; }
}

/* Высококонтрастный режим. Там принудительно назначаются системные цвета,
   text-shadow снимается, а наш SVG остаётся своей фиксированной краски —
   волна может слиться с подложкой, и ссылка потеряет единственный признак,
   отличающий её от текста. Возвращаем нативное подчёркивание.
   Состояния перечислены поимённо: без этого a:hover победил бы по
   специфичности и вернул фон с анимацией. */
@media (forced-colors: active) {
  a, a:hover, a:focus-visible, a:active {
    background-image: none;
    text-shadow: none;
    text-decoration: underline;
    text-underline-offset: var(--underline-offset);
    color: LinkText;
    animation: none;
  }
}

@media print {
  .fold::details-content { block-size: auto; opacity: 1; content-visibility: visible; }
  .mark { display: none; }
  body { background: #fff; color: #000; }
  /* Фоны при печати обычно выключены, и волна бы просто пропала —
     на бумаге ссылка возвращается к обычному подчёркиванию. Ореол тоже
     снимается: прорезать нечего, а белая обводка на белом съела бы засечки.
     Состояния перечислены поимённо: печать со ссылкой под фокусом или
     курсором иначе унесла бы на бумагу акцентный цвет и фон. */
  a, a:hover, a:focus-visible, a:active {
    background-image: none; color: #000; text-shadow: none; animation: none;
    text-decoration: underline; text-underline-offset: var(--underline-offset);
  }
  /* На бумаге фокуса нет, а кольцо ушло бы в печать акцентной краской,
     если распечатать страницу со ссылкой под фокусом. */
  :focus-visible { outline: none; }
}
