maia24 Дизайн-система

Дизайн-система Maia — «стеклянный люкс»

Единственный источник истины по внешнему виду продукта. Кто правит интерфейс — правит сначала здесь, потом в коде.


1. Что это за язык

Полупрозрачные панели поверх живого цветного фона. Глубина строится не рамками и не заливками, а материалом: размытием подложки (backdrop-filter), светлой верхней кромкой и мягкой тенью. Так выглядит visionOS от Apple — отсюда и название направления.

Пять правил, из которых выводится всё остальное:

  1. Фон живёт, панели плавают. Фон — три цветных пятна на градиенте, он зафиксирован (background-attachment: fixed) и не скроллится вместе с содержимым. Панели над ним размывают то, что под ними.
  2. Стекло — материал, а не украшение. Панель, карточка, первичная кнопка, поле ввода — стекло. Разделитель, текст, иконка — нет. Стекло на всём подряд убивает иерархию: если светится каждый элемент, не светится ни один.
  3. Иерархия держится цветом, а не «плоское против стеклянного». Первичная кнопка — изумрудное стекло, вторичная — нейтральное стекло того же материала. Разница в цвете, а не в наличии материала.
  4. Две темы равноправны. Ни одна не «основная». Токены одинаковые, значения разные. Тема ставится атрибутом data-theme на <html>.
  5. Материал отключаем. Пользователь просит систему убрать прозрачность — стекло становится плотной панелью, дизайн не разваливается. Это не «поддержка на будущее», это уже работает.

Почему без оверлея

Вкусовой слой 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выполнить задачу и уйтиoperateReact 18 + Vite + Tailwind 4
Лендинг maia24.ruрешиться и оставить заявкуpersuadeAstro, статика
Виджет на сайте клиентаответ на вопрос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-cap13pxподписи, метки статусов, шапки таблиц
--t-sm14pxкнопки, поля, навигация, вторичный текст
--t-base16pxосновной текст
--t-lg18pxлид-абзац, имя бренда
--t-xl22pxподзаголовок раздела, h3
--t-2xl30pxзаголовок экрана, h2 на мобильном
--t-3xl42pxh2 на лендинге, h1 на мобильном
--t-4xl56pxгерой лендинга

Кабинет использует --t-cap … --t-2xl. Лендинг — всю шкалу.

Правила


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; на выдуманных строках макета ни одно не проявлялось):

  1. Плашка получает СВОЮ колонку — то же, что .has-meta у .opt. Плашка переменной ширины, поставленная между текстом и управлением, толкает управление: «каталог» и «текст компании» разной длины, и колонки начинаются в каждой строке по-своему.
  2. Вторая строка ячейки не переносится. Подпись и адрес под ней — обе в одну строку с многоточием. Иначе высота строк скачет от длины содержимого: у Депола 13 строк из 24 были двухэтажными, 11 одноэтажными, и вертикального ритма не оставалось вовсе.
  3. Шапка стоит над СВОЕЙ колонкой. Если содержимое слота прижато к краю, а шапка по центру — они разъезжаются ровно тогда, когда слот заполнен коротким текстом. Проверять надо на РЕДКОМ значении: у Депола «обновлять нечего» стояло в двадцати трёх строках из двадцати четырёх, а макет рисовался на противоположном наборе, где слот заполнял переключатель.

7. Материал стекла

Рецепт панели — четыре слоя, все обязательны:

  1. Размытие подложки: backdrop-filter: blur(--glass-blur) saturate(--glass-sat). Насыщенность выше 100% — иначе размытый фон сереет.
  2. Полупрозрачная заливка --panel-bg и рамка --panel-border.
  3. Светлая верхняя кромка внутренней тенью: inset 0 1px 0 --panel-top. Это она даёт ощущение толщины стекла.
  4. Блик — псевдоэлемент ::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. Доступность

Контраст: было и стало

Четыре провала найдены замером и закрыты 3 августа 2026.

МестоБылоСталоКак
--txt-3, тёмная3.904.58прозрачность .42 → .47
--txt-3, светлая3.534.52прозрачность .52 → .60
Белый текст на первичной кнопке3.545.49стопы градиента затемнены
Белый текст на опасной кнопке~3.05.53то же
--danger, светлая4.404.99#C4443E → #B93B35

Худший случай для кнопок посчитан честно: белый текст на верхнем, самом светлом стопе градиента, поверх самой светлой подложки. Тень под текстом в расчёт не берётся — она не контраст.


11. Расхождения кабинета и лендинга

Лендинг портирован из кабинета. Двенадцать мест разошлись; все сведены 3 августа 2026.

ЧтоБыло в кабинетеБыло на лендингеСтало
--t-base15px16px16px везде, таблицы на --t-table 15px
Шкала отступовs1…s10s1…s20 с пропускамиполная сплошная в обоих
Токены радиусовнет, литералычастичнополная лестница xs…pill в обоих
Высота кнопки42px46pxосознанно оставлено: плотность против пальца
.pillметка статусаакцентная плашкана лендинге переименовано в .tag
--divider тёмная.07.09.09
Статусные цветаестьнетдобавлены на лендинг
--card-solid, --accent-2нетестьдобавлены в кабинет
Inter 700не загружалсязагружалсязагружается в обоих
Толщина линии иконок1.5 – 21.8 – 2.51.6 везде, 30 иконок
Полоса прокруткистекляннаясистемная плоскаястеклянная в обоих
Нажатая кнопкаtranslateY(1px)scale(.975)scale(.975) в обоих

Заодно снялось то, что вскрылось по дороге: цвет первичной кнопки был вписан литералом прямо в правило .btn мимо токена --btn-grad, и правка цвета из одного места не работала. Теперь кнопка красится токеном.


12. Как этим пользоваться

  1. Новый экран или блок — открыть brand/brandbook.html, взять готовый компонент. Своего не изобретать.
  2. Значение цвета, отступа, радиуса берётся токеном. Литерал в коде — ошибка.
  3. Компонент, которого нет в брендбуке, сначала добавляется сюда, потом в код.
  4. Правка токена — правится brand/tokens.json, затем оба CSS-файла. Расхождение между ними означает, что продукт распался на два.