Система, по которой собран этот сайт
Токены, правила и компоненты verno/dev — открыто и целиком. Не витрина: те же значения и те же импорты, из которых собирается продакшен-бандл.
Значения сняты с src/styles/_variables.sass. Кнопки, карточки и айброу ниже — не скриншоты и не копии разметки, а те же компоненты, что стоят на остальных страницах.
Принципы
Пять решений, из которых следует всё остальное. Каждое стоило либо измерения, либо переделки.
- 01
Один тон, до конца
Всё акцентное — вечнозелёное, тон ≈162°. Перекрашены даже ::selection и полоса прокрутки. Исключения ровно два: сигнальные цвета и намеренные «светофоры» macOS в терминале.
- 02
Стекло с физикой
Свет падает сверху слева: блик в углу, светлые верхняя и левая грани, затенённая нижняя правая кромка. Эффект читается как поведение света, а не как украшение.
- 03
Тёмная тема — основная
Тёмные значения живут на :root, светлая тема переопределяет их через [data-theme="light"]. Токены, не зависящие от темы, объявлены один раз.
- 04
Контраст измеряют, а не предполагают
Акцентный текст подобран против худшей реальной подложки — свечения поверх стекла, — а не против чистого фона. Измеренное отношение записано в комментарии рядом с токеном.
- 05
У эффекта есть цена кадра
Кадр тормозили не backdrop-filter, а размытые тени, повторённые на 15–20 карточках: 35 мс против 17 мс. Повторяющиеся карточки идут по рецепту без размытия, полное стекло осталось у одиночных поверхностей.
Цвет
Один тон на весь интерфейс и две темы. Светлая — не осветлённая копия тёмной, а самостоятельный набор значений.
Вечнозелёный
Бренд, тон ≈162°
- Текст на акцентной заливке (--accent-ink)
- Начало градиента заголовков, тёмная тема
- Мелкий акцентный текст, тёмная тема (--accent-text)
- Главный яркий акцент: иконки, границы, свечения (--accent-bright)
- Начало градиента заголовков, светлая тема
- Мета-строки в светлой теме — самый зелёный из проходящих AA
- Заливка основной кнопки, ::selection, skip-link (--accent-btn)
- Мелкий акцентный текст, светлая тема (--accent-text)
- Глубокий базовый акцент (--accent)
- Конец градиента заголовков, светлая тема
Нейтральный
С зелёным подтоном — чистого серого на сайте нет
- Текст тёмной темы (--ink)
- Фон светлой темы — намеренно глубже белого, чтобы стекло было видно
- Глубокий фон светлой темы (--bg-deep)
- Описания тёмной темы (--ink-soft)
- Приглушённый текст тёмной темы (--muted)
- Приглушённый текст светлой темы (--muted)
- Описания светлой темы (--ink-soft)
- Фон тёмной темы — зелёный уголь, а не чёрный провал
- Глубокий фон тёмной темы (--bg-deep)
- Текст светлой темы (--ink)
Сигнальные
Единственные невечнозелёные цвета интерфейса
- Успех: отправленная форма, статус доступности
- Ошибка поля — мягкий коралл, читается на тёмном стекле
Семантические токены
То, чем оперируют компоненты. Имя называет роль, а не оттенок: цвет за ним меняется вместе с темой, а у акцентных токенов — ещё и на лету, под платформу открытого проекта.
Акцент
Не зависят от темы: объявлены один раз на :root. Их же переписывает useAccentHover, когда страница перекрашивается под платформу проекта, — поэтому в компонентах допустим только var(--accent-*), но не готовый hex.
| Токен | Тёмная | Светлая |
|---|---|---|
--accent | #1A4D3E | #1A4D3E |
--accent-bright | #2E8B6F | #2E8B6F |
--accent-btn | #256B56 | #256B56 |
--accent-ink | #F4FBF7 | #F4FBF7 |
--accent-glow | rgba(46, 139, 111, 0.28) | rgba(46, 139, 111, 0.28) |
--ok | #17C46B | #17C46B |
--ok-glow | rgba(23, 196, 107, 0.18) | rgba(23, 196, 107, 0.18) |
Текст и фон
| Токен | Тёмная | Светлая |
|---|---|---|
--bg | #191D1B | #E7ECE6 |
--bg-deep | #111413 | #DCE3DA |
--ink | #ECF2EF | #0B140F |
--ink-soft | #B9C4BE | #35423B |
--muted | #8B9891 | #4E5B54 |
--accent-text | #42AE8E | #1A5F4C |
--heading-grad-a | #4CBE9C | #2C7E66 |
--heading-grad-b | #2E8B6F | #14402D |
Поверхности и границы
Альфа-каналы различаются по темам намеренно: тёмное стекло подкрашено белым на 7–12 %, светлое — на 45–68 %, потому что панель чуть темнее белого на почти белой странице попросту не видна.
| Токен | Тёмная | Светлая |
|---|---|---|
--surface-glass | rgba(255, 255, 255, 0.07) | rgba(255, 255, 255, 0.45) |
--surface-glass-strong | rgba(255, 255, 255, 0.12) | rgba(255, 255, 255, 0.68) |
--surface-dock | rgba(16, 24, 20, 0.94) | rgba(252, 253, 252, 0.96) |
--border-glass | rgba(255, 255, 255, 0.14) | rgba(20, 32, 25, 0.16) |
--border-glass-strong | rgba(255, 255, 255, 0.26) | rgba(20, 32, 25, 0.28) |
--border-glass-lit | rgba(255, 255, 255, 0.3) | rgba(255, 255, 255, 0.92) |
Оптика стекла и атмосфера
| Токен | Тёмная | Светлая |
|---|---|---|
--glass-specular | rgba(255, 255, 255, 0.09) | rgba(255, 255, 255, 0.6) |
--glass-grad-a | rgba(255, 255, 255, 0.06) | rgba(255, 255, 255, 0.5) |
--glass-grad-b | rgba(46, 139, 111, 0.05) | rgba(46, 139, 111, 0.07) |
--glass-highlight | rgba(255, 255, 255, 0.35) | rgba(255, 255, 255, 0.9) |
--glass-inner-shadow | rgba(0, 0, 0, 0.22) | rgba(20, 35, 28, 0.12) |
--shadow-color | rgba(1, 4, 3, 0.65) | rgba(20, 35, 28, 0.18) |
--atmo-glow | rgba(46, 139, 111, 0.09) | rgba(46, 139, 111, 0.13) |
--atmo-light | rgba(255, 255, 255, 0.04) | rgba(255, 255, 255, 0.5) |
Контраст
Коэффициенты считает сама страница из значений таблицы выше — они не переписаны из отчёта и не могут разойтись с токенами.
| Пара | Где | Тёмная | Светлая |
|---|---|---|---|
--ink на --bg | Основной текст | 15.02:1AAA | 15.64:1AAA |
--ink-soft на --bg | Описания | 9.49:1AAA | 8.80:1AAA |
--muted на --bg | Подписи, лид секций | 5.68:1AA | 5.95:1AA |
--accent-text на --bg | Мелкий акцентный текст | 6.23:1AA | 6.30:1AA |
--accent-ink на --accent-btn | Основная кнопка, выделение текста | 6.03:1AA | 6.03:1AA |
--accent-bright на --bg | Только крупный текст и иконки | 4.09:1AA | 3.48:1AA |
В таблице только сплошные пары, где коэффициент считается точно. Стеклянные поверхности полупрозрачны, и их стопки — свечение поверх стекла, худший случай на сайте, — измерены вручную; результат записан в комментарии рядом с определением токена.
Типографика
Три начертания, и оба текстовых несут полную кириллицу и латиницу в одном файле — у русской и английской версий не разъезжаются глифы.
- Заголовки держат страницу
- Основной текст, кнопки и поля форм
- AIBROW / 01 — MONO 12PX
Статическая шкала
Кратность 4. После свёртки часть ролей совпала по значению — имена оставлены, чтобы код называл роль, а не пиксель.
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
- Съешь ещё этих мягких булок да выпей чаю
Плавная шкала
Заголовки заданы через clamp: нижняя граница, скорость роста и потолок.
- Заголовок главной (h1)
clamp(40px, 5.2vw, 72px) - Заголовок страницы
clamp(36px, 5vw, 64px) - Заголовок кейса
clamp(28px, 4.4vw, 52px) - Заголовок секции (h2)
clamp(28px, 3.4vw, 44px) - Заголовок блока
clamp(24px, 2.8vw, 36px) - Заголовок карточки
clamp(20px, 2.2vw, 28px)
Межстрочный интервал меняется вместе с ролью — 1.1 у крупных заголовков, 1.2 у младших. Единое плотное значение ломается на размере карточки, когда заголовок переносится: у двухстрочного названия выносные элементы почти касаются следующей строки.
Отступы и сетка
Одна шкала на весь сайт. Новые margin и padding берутся только из неё.
$space-18px$space-212px$space-320px$space-432px$space-540px$space-660px$space-780px$space-8120px
Раскладка
- Контейнер 1200px
- Поля 28px, на мобильном 16px. Модификатор --narrow сужает до 760px для сплошного текста
- Ритм секций 80 / 60 px
- Обычная и .section--tight. Планшет 68px, мобильный 48px
- Шапка секции 640px
- Отступ снизу 40px, волосяная линейка, моноиндекс /0N в правом нижнем углу
- Отступ под шапку 104px
- scroll-padding-top на html — единственный механизм. scroll-margin-top на секциях складывается с ним и потому запрещён
Контрольные точки
Все по max-width. Точка меню отделена от планшетной намеренно: «помещается ли главное меню» — это вопрос про 887 пикселей раскладки, а не про класс устройства.
$bp-mobile620px- Телефон
$bp-nav930px- «Помещается ли главное меню» — отдельный вопрос от «это планшет»
$bp-tablet980px- Планшет
$bp-laptop1080px- Промежуточная ступень, чтобы 621–980 не был одним ведром
Форма и стекло
Три радиуса и четыре рецепта поверхности. Выбор рецепта — это выбор цены кадра, а не вкуса.
$radius-sm12pxПоля ввода, чипы, skip-link$radius20pxСтеклянные карточки и панели$radius-pill999pxКнопки, ползунок прокрутки
Рецепты поверхностей
Карточки ниже показывают сами себя: каждая собрана тем рецептом, который описывает.
glass-effectОдиночные поверхности: шапка, терминал, форма, боковое меню
Блик + градиент от белого к зелёному + полупрозрачная заливка; светлые верхняя и левая грани; backdrop-filter: blur(20px) saturate(160%); мягкая тень, контактная тень и две внутренние кромки.
glass-card--no-blurПовторяющиеся карточки в сетках и каруселях
То же лицо, но без backdrop-filter и без размытой тени — только внутренние кромки. Именно размытая тень, повторённая на 15–20 карточках, стоила 35 мс на кадр против 17 мс.
glass-puckКонтейнеры иконок, повторяющиеся в сетках
Градиент, граница 1px и одна небольшая тень. Без backdrop-filter — иконка лежит в материале, а не на плоском цветном квадрате.
cursor-sheenИнтерактивные карточки
Мягкое радиальное пятно акцента ходит за курсором внутри карточки: 7 % в тёмной теме, 10 % в светлой. Координаты пишет useCursorSheen прямо в style, без состояния React. Сама карточка не двигается.
Если backdrop-filter не поддерживается, все рецепты откатываются на непрозрачную --surface-glass-strong — панель остаётся читаемой, просто перестаёт быть стеклом.
Движение
Один закон движения на Sass и на Framer Motion — четыре одинаковых числа в обоих местах.
Длительности
$dur-fast0.2s- Микровзаимодействия, ховер кнопки во Framer
$dur-base0.25s- Цвета кнопок, границы полей, блик курсора
$dur-slow0.6s- Крупные появления
Правила
- Нажатие scale(0.97)
- Единственная обратная связь на касании: нативная подсветка тапа отключена глобально, потому что рисовала прямоугольник поверх круглых кнопок
- Ховер кнопки y: −2
- Framer Motion; нажатие — scale 0.96
- Карточки без сдвига
- Подъём и скос сняты 13.09.2026. Ховер карточки — это свечение и блик за курсором, ничего больше
- Смена темы 0.3s
- Переезжает только фон, цвет текста переключается мгновенно: на середине кросс-фейда оба оказываются серыми, ~1.1:1, и текст на миг исчезает
- Пульс айброу 2.6s
- Кольцо расходится и гаснет по бесконечному циклу
- Переход страниц 0.35s
- Появление секций — 0.7s «осадкой в плоскость»
Компоненты
Живые, а не снимки: всё ниже импортировано из тех же файлов, что и на остальных страницах. Наведите и нажмите — состояния настоящие.
Button
Три варианта, четыре состояния. Пилюля, Manrope 600, 16px, отступы 15×28.
| Вариант | Обычное | Наведение | Нажатие | Заблокировано |
|---|---|---|---|---|
primary | --accent-btn / --accent-ink | Кольцо свечения 6px + мягкий ореол | scale(0.97) | opacity 0.6, cursor: progress |
ghost | Белый 4% + blur(20px), граница --border-glass-strong | Граница и текст --accent-bright | scale(0.97) | opacity 0.6, cursor: progress |
on-accent | --bg / --ink | --bg-deep | scale(0.97) | opacity 0.6, cursor: progress |
Нажатие — scale(0.97) у всех трёх вариантов: нативная подсветка тапа отключена глобально, потому что рисовала прямоугольник поверх круглой кнопки, а форму подсветки задать нельзя. Значит, обратную связь на касании обязан давать сам компонент.
GlassCard
Одна поверхность, три режима. Отличаются ценой кадра, а не лицом.
- <GlassCard />
Полное стекло: backdrop-filter и мягкие тени. Для одиночных поверхностей.
- noBlur
Без размытия и без размытой тени, только внутренние кромки. Для сеток и каруселей.
- hoverLift
Акцентная граница и свечение на наведении. Карточка при этом не двигается.
Айброу и шапка секции
Два сквозных паттерна страницы. Индекс шапки — счётчик CSS: разделы нумеруются сами, порядком в документе.
Заголовок секции
Подзаголовок на 640 пикселей, с волосяной линейкой и моноиндексом справа.
Поле формы
Радиус 12, стеклянная заливка, акцентная рамка на фокусе.
Размер текста 16px не декоративный: на меньшем значении Safari в iOS увеличивает страницу при фокусе. Фокусное кольцо остаётся отдельно от акцентной рамки — гасить outline вслепую нельзя.
Доступность
Правила, которые нельзя нарушить, не сломав систему. Первые четыре — про цвет, последние два — про фокус и движение.
- Мелкий акцентный текст — только --accent-text. --accent-bright никогда не несёт текст меньше крупного: 3.97:1 под --accent-ink и 3.48:1 на светлом фоне.
- Новое сочетание акцента с поверхностью измеряется на худшей стопке, на которой окажется, — свечение поверх стекла, — а не на чистом --bg.
- Заголовки берут более яркие остановки градиента только пока нижняя граница clamp и вес 600 удерживают их в категории крупного текста (≥18.66px bold → порог 3:1).
- Измеренное отношение записывается в комментарий рядом с определением токена — так устроен весь _variables.sass.
- Кольцо фокуса — 2px var(--accent-text) с отступом 3px и без переопределения радиуса: заданный радиус перекраивал сам элемент, пока тот держал фокус, и круглые точки карусели становились квадратными.
- Каждый эффект движения обязан попадать под глобальный выключатель prefers-reduced-motion — он гасит и анимации, и переходы, и плавную прокрутку, а MotionConfig reducedMotion="user" останавливает то, что Framer ведёт инлайновыми стилями.
Нужна такая же система?
Если у проекта уже есть макет или живой сайт, систему можно собрать из того, что есть, и дальше вести проект по ней.