Про воздух: сколько его между секциями, между блоками внутри секции
и по краям страницы. Соседний документ TYPOGRAPHY.md
отвечает за текст, этот — за расстояния.
Читать перед добавлением нового блока и перед любой правкой py-*,
mt-*, mb-*, gap-* в разметке.
Отступ — это роль, а не число. Ролей пять по вертикали и две по горизонтали.
Каждая роль — один токен; своего clamp() в разметке быть не должно.
До введения шкалы вертикальный ритм секций был набран четырьмя разными
формулами (clamp(3rem,6vw,8rem), clamp(3.5rem,7vw,7rem),
clamp(4rem,7vw,9rem), фиксированные mb-10). Кривые пересекались: на
ширине ~1000px «маленькая» секция имела отступ больше «большой», а на 1600px —
наоборот. Порядок значений менялся вместе с шириной экрана — это и есть
отсутствие системы, а не набор случайных чисел.
Токены живут в блоке @theme в ../src/styles/global.css.
Неймспейс --spacing-* принадлежит Tailwind, поэтому именованный ключ даёт
сразу и var(--spacing-block) в CSS, и утилиты mt-block / mb-block /
gap-block / p-block в разметке.
| Токен | Формула | 390px | 1440px | 1920px | Роль |
|---|---|---|---|---|---|
--spacing-section |
clamp(3rem, 6vw, 8rem) |
48 | 86 | 115 | Внешний ритм секции на светлом фоне |
--spacing-section-lg |
clamp(4rem, 7vw, 9rem) |
64 | 101 | 134 | То же для секции поверх фотопанорамы |
--spacing-block |
clamp(2.5rem, 5vw, 4.5rem) |
40 | 72 | 72 | Шапка секции → контент; крупные блоки внутри секции |
--spacing-group |
clamp(2rem, 3.5vw, 3rem) |
32 | 48 | 48 | Подраздел внутри блока |
--spacing-gutter |
clamp(2.5rem, 4vw, 5rem) |
40 | 58 | 77 | Зазор между колонками grid'а |
--spacing-panel |
clamp(1.75rem, 3vw, 3rem) |
28 | 43 | 48 | Внутренние поля крупной панели (врезка с цитатой, карточки модалок) |
Ниже ступени group работает обычная шкала Tailwind (mt-2 … mb-8):
она уже кратна 4px и в токенах не нуждается. Токен нужен там, где расстояние
должно расти вместе с экраном.
--spacing-panel стоит особняком: это не ступень ритма страницы,
а «толщина стенки» контейнера.
section-lg получают секции, лежащие поверх фотографии (#video,
#offer-stats, #request). Причина не в важности блока, а в фоне: на снимке
контент без запаса поля читается как наклейка поверх картинки. Правило
механическое — фотофон есть, значит ступень lg; фона нет, значит базовая.
<section class="relative z-10 bg-bg section-py">…</section> <!-- светлая -->
<section class="relative z-10 … section-py-lg">…</section> <!-- на фото -->.section-py и .section-py-lg — composition-классы в @layer components,
внутри это padding-block: var(--spacing-section[-lg]). Нужна асимметрия
(pt без pb) — берутся утилиты: pt-section, pb-section-lg.
<div class="section-px">…</div> <!-- px-6 sm:px-10 md:px-12 lg:px-20 -->
<div class="section-px-wide">…</div> <!-- px-6 sm:px-10 md:px-12 -->Ступени всего две. section-px-wide — для контента, который живёт «ковром»
во всю ширину и на lg+ отдаёт поля ради размера: диаграмма ПРП, лента
логотипов. Всё остальное — section-px.
Отдельный edge-случай — masonry-лента галереи: её боковые поля равны зазору
между кадрами (--gallery-gap, 4–8px), чтобы решётка читалась единым полем.
Это задокументированное исключение, третьей ступенью полей оно не является.
<div data-reveal>
<p class="t-eyebrow t-on-muted mb-6">Надзаголовок</p> <!-- eyebrow → H2: всегда mb-6 -->
<h2 class="t-h-section t-on-brand">Заголовок</h2>
</div>
<div class="mt-block">…контент секции…</div> <!-- шапка → контент -->
<div class="mt-group">…следующий подраздел…</div> <!-- внутри блока -->
<div class="grid lg:grid-cols-12 gap-gutter">…</div> <!-- колонки -->Внутренние поля контента модалки — всегда ступень panel, независимо от того,
что внутри: лонгрид, список, цитата или расшифровка видео.
| Слой | Отступы |
|---|---|
<dialog> → карточка |
--dialog-py / --dialog-px (поля модального слоя) |
| Карточка → контент | p-panel (или px-panel + своя вертикаль) |
| Полоса-шапка карточки | px-panel + вертикаль по Tailwind-шкале (py-3, py-5) |
Полоса-шапка — край панели, а не панель: её вертикаль намеренно меньше поля. Горизонталь при этом общая, иначе крестик и заголовок стоят по разным вертикалям.
Лайтбоксы #modal-certificate и #modal-gallery в это правило не входят:
внутри них изображение во весь экран, полей у контента нет вовсе.
Устойчивые микрорасстояния, которые токенами не покрываются и держатся Tailwind-шкалой:
| Пара | Отступ |
|---|---|
| eyebrow → H2 (и eyebrow → список) | mb-6 |
| H2 → лид | mt-8 |
| лид → уточняющий абзац | mt-4 |
| текст → подпись/figcaption | mt-6 |
| Секция | Вертикаль | Горизонталь |
|---|---|---|
Hero |
свой контракт первого экрана (pt-10 md:pt-14, pb-[55vh] md:pb-14) |
section-px |
#about-training |
section-py |
section-px |
#video |
section-py-lg |
section-px |
#examples |
pt-section + pb-0 |
section-px |
#offer-stats |
section-py-lg |
section-px |
#prp-steps |
section-py, xl:pb — половина ступени |
section-px-wide |
#about-trainer |
section-py на контентной колонке |
section-px |
#certificate |
section-py |
section-px |
#clients |
section-py |
section-px + section-px-wide у ковра логотипов |
#reviews |
section-py |
section-px |
#training-photos |
section-py |
section-px + edge-поля у masonry |
#request |
section-py-lg |
section-px |
#modal-about, #modal-examples, #modal-review, #modal-video-review |
вертикаль контента — panel, у #modal-about низ — block |
panel |
#modal-certificate, #modal-gallery |
полей нет: изображение во весь экран | — |
Их четыре, каждое с комментарием прямо в коде:
#examples,pb-0. Фото Дениса стоит на нижней границе секции (lg:self-end); нижний паддинг оторвал бы фигуру от края. Текстовая колонка компенсирует это своимlg:pb-block— иначе подсказка под списком категорий прилипает вплотную к следующей секции.#prp-steps,xl:pb-[calc(var(--spacing-section)/2)]. У stage-«креста» (aspect 13/10) под октагоном 5 есть встроенная пустота ~4% ширины stage. Полная ступень поверх неё делала низ заметно толще верха. Считается от токена, а не своимclamp(): сдвинется шкала — сдвинется и он.#about-training,gap-xколонки с иероглифом (clamp(2.5rem,6vw,8rem)) шире ступениgutterнамеренно: эта колонка работает как поле-разделитель, а не как соседняя колонка контента.Hero. Первый экран живёт по своему контракту (min-h-screen, резерв ~55vh под фото на мобильном) и в ритм страницы не входит.
Конца страницы «воздушным» <div> нет: полоса белого под тёмной формой
читалась как обрыв вёрстки, а собственный pb у #request финалу воздуха
уже даёт. Появится футер — встанет сразу после <LeadForm />.
- Свой
clamp()для отступа в разметке. Любое расстояние, которое растёт с экраном, — это одна из шести ролей. Не подошла ни одна — сначала спроси, правда ли нужна седьмая. - Фиксированный
py-16/mb-10на границе секции или блока. Секция обязана дышать вместе с вьюпортом, фикс из Tailwind-шкалы этого не делает. - Новая ступень «чуть больше существующей» (
clamp(2.5rem,5vw,5rem)рядом сclamp(2.5rem,5vw,4.5rem)). Разница в 8px не читается, а словарь удваивается. - Повторённая формула вместо переменной. Поля модального слоя
(
--dialog-py/--dialog-px) участвуют ещё и в расчётахmax-heightкарточек: разъехавшись с padding'ом, значение молча обрежет содержимое. - Асимметричный
pyбез причины.pt≠pb— только там, где к границе секции что-то примыкает (фото в край, встроенная пустота диаграммы), и обязательно с комментарием. - Элемент, торчащий за поля секции. На мобильном он растягивает
горизонтальную прокрутку всей страницы. Проверять надо computed-геометрией,
а не глазами:
visibility: hiddenиз потока не убирает (на этом уже попадалась подсказка.termtip__bubble).
Свои clamp() в отступах разметки — должно остаться ровно одно попадание,
задокументированное исключение с gap-x в AboutTraining.astro:
grep -rnE "\b(gap(-[xy])?|[mp][trblxy]?)-\[clamp" src/Вертикаль секции задаётся только классом или токеном — своих формул в компонентах быть не должно (grep возвращает пусто):
grep -rnE "p[tby]?-\[clamp" src/components/ src/pages/Фактический ритм на собранной странице (padding секций должен принимать
ровно два значения — базовое и lg):
npm run build
npm run preview
# затем в DevTools:
# $$('section').map(s => [s.id, getComputedStyle(s).paddingTop])