Дизайн-система Maia — «стеклянный люкс»
Единственный источник истины по внешнему виду продукта. Кто правит интерфейс — правит сначала здесь, потом в коде.
- Машиночитаемые токены:
brand/tokens.json - Живой брендбук (открыть в браузере):
brand/brandbook.html - Реализация в кабинете:
prototype/src/index.css - Реализация на лендинге: репозиторий
maia24-site, файлsrc/styles/theme.css
1. Что это за язык
Полупрозрачные панели поверх живого цветного фона. Глубина строится не рамками и не заливками, а материалом: размытием подложки (backdrop-filter), светлой верхней кромкой и мягкой тенью. Так выглядит visionOS от Apple — отсюда и название направления.
Пять правил, из которых выводится всё остальное:
- Фон живёт, панели плавают. Фон — три цветных пятна на градиенте, он зафиксирован (
background-attachment: fixed) и не скроллится вместе с содержимым. Панели над ним размывают то, что под ними. - Стекло — материал, а не украшение. Панель, карточка, первичная кнопка, поле ввода — стекло. Разделитель, текст, иконка — нет. Стекло на всём подряд убивает иерархию: если светится каждый элемент, не светится ни один.
- Иерархия держится цветом, а не «плоское против стеклянного». Первичная кнопка — изумрудное стекло, вторичная — нейтральное стекло того же материала. Разница в цвете, а не в наличии материала.
- Две темы равноправны. Ни одна не «основная». Токены одинаковые, значения разные. Тема ставится атрибутом
data-themeна<html>. - Материал отключаем. Пользователь просит систему убрать прозрачность — стекло становится плотной панелью, дизайн не разваливается. Это не «поддержка на будущее», это уже работает.
Почему без оверлея
Вкусовой слой soft из design-ultimate прямо предупреждает: плохо ложится на плотные операционные интерфейсы. Ядро продукта — кабинет агентства, там глубина уходит в точность деталей, а не в пластику. Вместо оверлея действует пол качества impeccable/reference/craft-floor.md.
2. Знак
Плавная незавершённая волна: в ней спрятаны буквы M, A и I. Точка внутри первого изгиба формирует A, верхняя точка продолжает наклонную i. Написание — maia24 строчными без пробела, Inter 700, трекинг -.04em. Файлы комплекта — brand/assets/logos/, живая страница со всеми состояниями — брендбук, раздел «Знак».
Два токена, и они не равны акценту. Знак и число 24 — --logo-accent, написание — --logo-word. Оба инвертируются по яркости темы: на тёмном фоне #34D399 и белое написание, на светлом #0C7E57 и тёмное. Число 24 всегда повторяет цвет знака.
Почему отдельные токены, а не --accent: акцент партнёр меняет под своего клиента прямо в кабинете. Если бы логотип платформы красился им, у бота с малиновым профилем наш знак стал бы малиновым — то есть перестал быть нашим знаком.
| Размер | Где |
|---|---|
| 64 | обложка, письмо-приглашение |
| 40 | шапка кабинета, экраны входа |
| 32 | компактная шапка |
| 24 | строка списка |
| 16 | иконка вкладки, подпись в виджете |
Толщина линии и обе точки масштабируются вместе со знаком, пропорция 136:104 фиксирована. Минимум 16px: ниже точки перестают отделяться от линии. До 32px допускается знак без написания.
Чего с ним не делают: не дорисовывают волну и не переставляют точки, не растягивают по одной оси, не меняют толщину линии, не отделяют 24 бейджем или верхним индексом, не набирают название капсом. Иконка вкладки и аватар — знак на плотной тёмной подложке (maia24-avatar-dark.svg): на прозрачном фоне тонкая линия теряется в тёмной теме браузера.
Подпись в виджете
В окне чата на сайте клиента знак и подпись «Сделано на maia24.ru» идут профильным цветом бота, а не фирменным зелёным: панель одета в цвет клиента, и наш зелёный в ней читался бы заплаткой. Правило делит поверхности пополам — фирменный зелёный на наших (кабинет, вход, письма, иконка вкладки), профильный цвет во всём, что стоит на сайте клиента.
Контраст при этом не отдаётся на волю партнёра: цвет подписи считается на месте, и если акцент не берёт 4.5:1 на подложке панели, в него подмешивается цвет текста той же темы шагами по 15%. Насыщенный цвет проходит порог сразу и остаётся чистым.
3. Поверхности
| Поверхность | Что здесь успех | Режим | Реализация |
|---|---|---|---|
Кабинет platform.maia24.ru | выполнить задачу и уйти | operate | React 18 + Vite + Tailwind 4 |
Лендинг maia24.ru | решиться и оставить заявку | persuade | Astro, статика |
| Виджет на сайте клиента | ответ на вопрос | operate | встраиваемый скрипт |
Режим меняет плотность и типографику, но не палитру и не материал. Лендинг берёт ту же систему и достраивает шкалу вверх (герой), кабинет — вниз (плотные таблицы).
4. Цвет
Полный список значений — в brand/tokens.json. Здесь смысл, а не перечисление.
Бренд
| Роль | Тёмная тема | Светлая тема |
|---|---|---|
Акцент (--accent) | #34D399 | #0C7E57 |
Акцент светлее (--accent-2) | #6EE7B7 | #10B981 |
Знак и первичная кнопка (--mark-grad) | #6EE7B7 → #10B981 | #10C98C → #0BB078 |
Акцент инвертируется по яркости: на тёмном фоне светлый изумруд, на светлом — глубокий. Поэтому текст на акценте тоже разный: на тёмной теме почти чёрный #04130D, на светлой белый. Это --on-accent, и брать вместо него #fff вслепую нельзя — на светло-зелёной плашке белый текст нечитаем.
Фон
Три радиальных пятна (изумруд, синий, фиолетовый) поверх диагонального градиента. Пятна одинаковые в обеих темах по позиции, разные по светлоте. Резервный плотный цвет — --solid-bg, включается при prefers-reduced-transparency.
Текст
Три ступени: --txt (основной), --txt-2 (вторичный), --txt-3 (третичный).
--txt-3 раньше не проходил контраст ни в одной теме (3.9 и 3.53 при пороге 4.5), а им набраны плейсхолдеры и подписи — то есть текст, а не декор. Прозрачность поднята до .47 в тёмной теме и .60 в светлой: теперь 4.58 и 4.52.
Иерархия при этом сохранилась — три ступени по-прежнему различимы. Но правило остаётся: --txt-3 для того, что можно не прочитать. Всё, что несёт смысл, — минимум --txt-2.
Статусы
Четыре семейства, каждое из четырёх токенов (фон / рамка / текст / точка): ok — работает, tr — пробный период, pa — на паузе, danger — необратимо. Статус всегда несёт цветную точку рядом с текстом: цвет один не должен быть единственным носителем смысла (дальтонизм).
5. Типографика
Inter — и заголовки, и текст. Начертания 400 / 500 / 600 / 700, самохостинг, кириллические подмножества включены.
Google Fonts не подключается. Проект работает под РФ, внешний CDN может отдать таймаут, и страница молча упадёт на системный шрифт — это другой дизайн, о котором никто не узнает.
Шкала
| Токен | Размер | Где |
|---|---|---|
--t-cap | 13px | подписи, метки статусов, шапки таблиц |
--t-sm | 14px | кнопки, поля, навигация, вторичный текст |
--t-base | 16px | основной текст |
--t-lg | 18px | лид-абзац, имя бренда |
--t-xl | 22px | подзаголовок раздела, h3 |
--t-2xl | 30px | заголовок экрана, h2 на мобильном |
--t-3xl | 42px | h2 на лендинге, h1 на мобильном |
--t-4xl | 56px | герой лендинга |
Кабинет использует --t-cap … --t-2xl. Лендинг — всю шкалу.
Правила
- Трекинг заголовков
-.02em. Крупный Inter без сжатия выглядит рыхло. - Межстрочный: 1.6 в тексте, 1.05–1.2 в заголовках.
- Числа в таблицах и метриках —
.tnum(моноширинные цифры), иначе колонка «дышит» при обновлении. - Ширина строки текста 65–75 символов. Шире — глаз теряет начало следующей.
6. Ритм и форма
Отступы — одна шкала с шагом 4px: --s1 4 · --s2 8 · --s3 12 · --s4 16 · --s5 20 · --s6 24 · --s7 28 · --s8 32 · --s9 36 · --s10 40 · --s12 56 · --s16 80 · --s20 120.
Значений вне шкалы в коде быть не должно. Исключение одно — оптические поправки в 1–2px, и каждая объясняется комментарием в месте применения.
Радиусы: --radius-xs 9px (внутренние чипы, ползунки) · --radius-sm 11px (поля, пункты меню) · --radius 13px (кнопки, сегменты) · --radius-md 16px (панели, контекстное меню) · --radius-lg 20px (карточки, модальные окна) · --radius-pill 999px (метки статусов).
Радиус растёт вместе с элементом. Маленький радиус на большой карточке читается как обрезанный угол, большой на кнопке — как капсула.
Единственное исключение из шкалы — знак M и логотип клиента: их скругление пропорционально стороне (около 30%) и меняется вместе с размером, поэтому там литерал с комментарием, а не шаг шкалы.
Высота управляющих элементов: 42px в кабинете (плотность), 46px на лендинге (палец на телефоне). Это осознанное расхождение, а не рассинхрон.
Вторая ступень — 34px, и она про количество, а не про важность. Полный размер задан для элемента, который на экране один: фильтр в панели инструментов, кнопка действия. Как только такой элемент появляется в КАЖДОЙ строке списка, восемь пикселей умножаются на число строк — на списке страниц сайта это сотни строк, то есть экран, съеденный прокруткой. Ступень уже работает у кнопки (.fb-btn-sm) и у сегментного переключателя (.seg-sm); в обоих случаях меняются только размеры — высота, индикатор, шрифт --t-cap, радиус --radius-sm. Материал, цвет и движение остаются правилами полного размера, иначе это уже второй компонент.
Контролы выбора — 14px, и это третья ступень, а не «мелкий тумблер». Тумблер 26px: его тащат пальцем, он про «включить весь блок». Кружок выбора и галочка стоят в строке списка рядом с подписью в 13px, и на размере плашки тумблера (20px) выглядят крупнее текста, который описывают. Материал при этом общий с тумблером — подложка --lite-bg, кромка --lite-border, внутренний блик, мягкая тень; отмеченное состояние — цвета «включено» и то же свечение 6px. Меняется только размер, иначе это был бы второй язык.
Родные input[type=radio|checkbox] не используются: их рисует операционная система, они не знают ни стекла, ни токенов и в тёмной теме выглядят инородно. Настоящий контрол остаётся в разметке ради клавиатуры и чтения с экрана, но прячется, а форма рисуется рядом — тот же приём, что у тумблера.
**Строка списка с управлением — компонент .opt, и у него три варианта.** База: управление справа, текст слева, описание под названием. .opt-lead — управление слева (так читается список выбора). .has-meta — плашка или кнопка получает СВОЮ колонку.
Последнее не косметика, а единственный способ удержать границу текста: пока плашка лежит внутри первой строки текстовой колонки, описание на второй строке занимает всю ширину колонки и проходит под плашкой. Глазу это читается как наложение — и правильно читается. Колонка текста обязана иметь одну границу на все свои строки.
Мелкий контрол в колонке управления встаёт НА ГРАНИЦУ между названием и описанием, а не по центру строки: по центру он уезжает вниз, как только описание переносится на вторую строку, и соседние строки списка перестают выглядеть одинаково. Отсчёт — от высоты первой строки (26px) плюс половина зазора.
Раскладка: строка, ряд, колонка
Этого раздела здесь не было до 20 августа 2026, и обошлось это дорого: правил ритма и компонентов документ давал, а правил РАСКЛАДКИ — нет, поэтому каждый экран собирал сетку заново, своими классами и инлайновыми стилями.
Две утилиты, и обе живут в системе, а не в экране. .row — элементы в строку по центру, зазор --s3. .row-between — то же плюс разгон краёв. Нужен другой зазор — задаётся в месте применения; но БАЗА обязана быть в css.
Почему это правило, а не мелочь: до 20.08 обоих классов не существовало вовсе — при том, что разметка на них ссылалась в шести местах двух экранов кабинета. Класс, которого нет, не падает и не ругается: элементы просто текут строкой без зазора. На карте сайта из-за этого метки разделов стояли слипшимися, а в материале знаний соседние строки передавали gap и выравнивание инлайном — и они молча не работали, потому что без display:flex не значат ничего. Невидимо до скриншота.
**Ряд фильтров — .tabbar с .tab, и никогда не .pill.** Метка статуса и кнопка фильтра похожи на глаз — капсула с текстом, — но это разные вещи. .pill несёт СОСТОЯНИЕ объекта четырьмя семействами, стоит в строке одна и зазора не имеет; собранный из меток фильтр не только стоит вплотную, но и врёт: невыбранный раздел красится «на паузе», хотя ни на какой паузе он не стоит.
Сетка списка — три правила, каждое куплено живыми данными (карта сайта Депола, 24 строки, 20.08; на выдуманных строках макета ни одно не проявлялось):
- Плашка получает СВОЮ колонку — то же, что
.has-metaу.opt. Плашка переменной ширины, поставленная между текстом и управлением, толкает управление: «каталог» и «текст компании» разной длины, и колонки начинаются в каждой строке по-своему. - Вторая строка ячейки не переносится. Подпись и адрес под ней — обе в одну строку с многоточием. Иначе высота строк скачет от длины содержимого: у Депола 13 строк из 24 были двухэтажными, 11 одноэтажными, и вертикального ритма не оставалось вовсе.
- Шапка стоит над СВОЕЙ колонкой. Если содержимое слота прижато к краю, а шапка по центру — они разъезжаются ровно тогда, когда слот заполнен коротким текстом. Проверять надо на РЕДКОМ значении: у Депола «обновлять нечего» стояло в двадцати трёх строках из двадцати четырёх, а макет рисовался на противоположном наборе, где слот заполнял переключатель.
7. Материал стекла
Рецепт панели — четыре слоя, все обязательны:
- Размытие подложки:
backdrop-filter: blur(--glass-blur) saturate(--glass-sat). Насыщенность выше 100% — иначе размытый фон сереет. - Полупрозрачная заливка
--panel-bgи рамка--panel-border. - Светлая верхняя кромка внутренней тенью:
inset 0 1px 0 --panel-top. Это она даёт ощущение толщины стекла. - Блик — псевдоэлемент
::afterс радиальным градиентом из правого верхнего угла. Имитирует источник света, один и тот же для всей страницы.
Плюс внешняя тень 0 20px 50px — со смещением и мягким размытием. Цветное свечение без смещения — это декорация, а не глубина.
Облегчённый вариант .glass-lite — размытие 16px, без блика и внешней тени. Для вложенных элементов: поля ввода, вторичные кнопки, ячейки.
Где стекло запрещено: контекстное меню и выпадающие списки. Меню всплывает поверх произвольного содержимого, и размытие делает его нечитаемым. Там --solid-panel — плотная заливка.
8. Движение
Одна кривая на весь проект: --ease: cubic-bezier(.22,1,.36,1) — резкий старт, долгий выход. Две длительности: --dur .2s (цвет, фон) и --dur-slow .28s (сдвиг, тень).
Что анимируется: подъём кнопки на 1px при наведении, скольжение индикатора в переключателе тем и сегментном фильтре, появление контекстного меню (.12s), проявление блоков лендинга при скролле.
Что не анимируется: смена экрана, появление данных в таблице, переключение темы. Кабинет открывают ежедневно — анимация, милая на первом запуске, на сотом становится задержкой.
При prefers-reduced-motion всё сокращается до .01ms. Не отключается совсем — нулевая длительность ломает некоторые переходы состояний.
9. Иконки
Контурные SVG, сетка 24×24, viewBox="0 0 24 24", скругления на концах линий. Толщина линии — 1.6.
Эмодзи вместо иконки не используются никогда: они рендерятся системным шрифтом, то есть выглядят по-разному на Windows, Mac и Android, и не подчиняются цвету темы.
Размеры в интерфейсе: 20px в навигации, 18px в кнопках и полях, 17px в контекстном меню.
Начертание одно на весь продукт — контурное. Набор — Phosphor, начертание regular; залитых значков в интерфейсе нет. Один залитый значок ломает ряд сильнее, чем кажется: кнопка отправки в виджете стояла залитой самолёткой рядом с контурными «во весь экран» и «свернуть», и читалась чужой наклейкой — при том что рисунок был из того же набора. Сейчас там paper-plane-right в контуре.
10. Доступность
- Фокус с клавиатуры — изумрудное кольцо 2px с отступом 2px. Фокус от мыши не показывается (
:focus:not(:focus-visible)). prefers-reduced-transparency— стекло становится плотным, блик убирается.prefers-reduced-motion— переходы обнуляются.color-schemeвыставляется по теме, иначе системные попапы<select>приходят в чужой гамме.- Статус не кодируется одним цветом — рядом всегда точка и текст.
Контраст: было и стало
Четыре провала найдены замером и закрыты 3 августа 2026.
| Место | Было | Стало | Как |
|---|---|---|---|
--txt-3, тёмная | 3.90 | 4.58 | прозрачность .42 → .47 |
--txt-3, светлая | 3.53 | 4.52 | прозрачность .52 → .60 |
| Белый текст на первичной кнопке | 3.54 | 5.49 | стопы градиента затемнены |
| Белый текст на опасной кнопке | ~3.0 | 5.53 | то же |
--danger, светлая | 4.40 | 4.99 | #C4443E → #B93B35 |
Худший случай для кнопок посчитан честно: белый текст на верхнем, самом светлом стопе градиента, поверх самой светлой подложки. Тень под текстом в расчёт не берётся — она не контраст.
11. Расхождения кабинета и лендинга
Лендинг портирован из кабинета. Двенадцать мест разошлись; все сведены 3 августа 2026.
| Что | Было в кабинете | Было на лендинге | Стало |
|---|---|---|---|
--t-base | 15px | 16px | 16px везде, таблицы на --t-table 15px |
| Шкала отступов | s1…s10 | s1…s20 с пропусками | полная сплошная в обоих |
| Токены радиусов | нет, литералы | частично | полная лестница xs…pill в обоих |
| Высота кнопки | 42px | 46px | осознанно оставлено: плотность против пальца |
.pill | метка статуса | акцентная плашка | на лендинге переименовано в .tag |
--divider тёмная | .07 | .09 | .09 |
| Статусные цвета | есть | нет | добавлены на лендинг |
--card-solid, --accent-2 | нет | есть | добавлены в кабинет |
| Inter 700 | не загружался | загружался | загружается в обоих |
| Толщина линии иконок | 1.5 – 2 | 1.8 – 2.5 | 1.6 везде, 30 иконок |
| Полоса прокрутки | стеклянная | системная плоская | стеклянная в обоих |
| Нажатая кнопка | translateY(1px) | scale(.975) | scale(.975) в обоих |
Заодно снялось то, что вскрылось по дороге: цвет первичной кнопки был вписан литералом прямо в правило .btn мимо токена --btn-grad, и правка цвета из одного места не работала. Теперь кнопка красится токеном.
12. Как этим пользоваться
- Новый экран или блок — открыть
brand/brandbook.html, взять готовый компонент. Своего не изобретать. - Значение цвета, отступа, радиуса берётся токеном. Литерал в коде — ошибка.
- Компонент, которого нет в брендбуке, сначала добавляется сюда, потом в код.
- Правка токена — правится
brand/tokens.json, затем оба CSS-файла. Расхождение между ними означает, что продукт распался на два.