# opopa.top — Design System

> Ночная мастерская инженера: почти чёрный холст, один холодный луч и одна тёплая искра.
> Всё остальное — воздух и текст, который удобно читать сорок минут подряд.

**Theme:** dark-only (не mixed)
**Донор:** Huly — Style Reference (`design/donors/huly-DESIGN.md`, styles.refero.design)
**Структурный референс:** Intercom onboarding (`design/references/intercom-onboarding/`)

## Манифест

Блог о LLM для программистов, предпринимателей и вайбкодеров. Главный объект на экране —
**текст и код**, а не оформление. Дизайн решает три задачи: не мешать чтению, отличать наши
выводы от чужих цитат, и делать посты узнаваемыми в ленте соцсетей.

Отсюда три правила системы:

1. **Один режим.** Только тёмная тема. Донор чередует тёмные и светлые полосы — мы это
   не берём: для чтения длинных текстов переключение фона внутри страницы вредно.
2. **Два акцента, и всё.** Холодный `iris` — навигация, ссылки, активные состояния.
   Тёплый `ember` — «наш голос»: врезки-выводы, блок «Артефакт», цифры. Третьего цвета нет.
3. **Аврора один раз.** Фирменный градиентный луч живёт только на главной и в OG-картинках.
   На странице поста его нет — там работает типографика.

## Отличия от донора и почему

| Донор Huly | У нас | Причина |
|---|---|---|
| canvas `#303236` (светло-графитовый) | canvas `#0b0c0e` | На `#303236` ссылка-iris даёт контраст 3.46 и вторичный текст 4.40 — оба ниже 4.5. Проверено расчётом. |
| theme: mixed, чередование dark/light полос | dark-only | Длинные тексты; смена фона внутри статьи сбивает чтение. |
| body 14px | body 18px, `--text-body` 18/1.65 | SaaS-лендинг против блога. 14px нечитабелен на длинной дистанции. |
| display Esbuild (проприетарный) | Unbounded | Нужна кириллица. Esbuild её не имеет и недоступен. |
| radius 9999px на всём | pill только теги/кнопки, карточки 12px, код 8px | Pill на карточке с кодом ломает моноширинную сетку. |
| белый текст на primary-кнопке | тёмный текст `#0b0c0e` на iris/ember | Белый на `#5683da` = 3.71, на `#ff8964` = 2.33 — FAIL. Тёмный даёт 5.27 и 8.40. |
| нет моноширинного шрифта | JetBrains Mono | Код — половина контента. |

## Tokens — Colors

Все значения проверены на контраст (WCAG AA, порог 4.5:1 для текста, 3:1 для границ UI).

### Поверхности

| Name | Value | Token | Role |
|------|-------|-------|------|
| Canvas | `#0b0c0e` | `--color-canvas` | Фон страницы, доминирующая поверхность |
| Surface | `#131518` | `--color-surface` | Шапка, подвал, полосы-секции на один шаг выше холста |
| Card | `#1a1d21` | `--color-card` | Карточки постов, врезки, панели |
| Code | `#0f1114` | `--color-code` | Фон блоков кода — темнее карточки, код «уходит вглубь» |

### Текст

| Name | Value | Token | Role | Контраст на canvas |
|------|-------|-------|------|---|
| Text | `#eceef1` | `--color-text` | Основной текст, заголовки | 16.83 |
| Text Muted | `#b6bac1` | `--color-text-muted` | Подзаголовки, описания, лиды | 10.05 |
| Text Subtle | `#8d929b` | `--color-text-subtle` | Даты, время чтения, подписи к картинкам, метаданные | 6.26 |

Ниже `#8d929b` не опускаться: `#6b7280` даёт 4.05 на холсте и 3.5 на карточке — для текста мало.

### Акценты

| Name | Value | Token | Role | Контраст на canvas |
|------|-------|-------|------|---|
| Iris | `#7ea6f0` | `--color-iris` | Ссылки в тексте, активный пункт навигации, фокус-кольцо | 8.01 |
| Iris Deep | `#5683da` | `--color-iris-deep` | Заливка primary-кнопки, градиентный стоп (холодный) | 5.27 с тёмным текстом |
| Ember | `#ff8964` | `--color-ember` | Блок «Артефакт», врезка-вывод, цифры, тёплый стоп авроры | 8.40 |
| Ember Deep | `#5a250a` | `--color-ember-deep` | Фон тёплых тегов и заливок, где `ember` слишком ярок | — |

### Подложки тегов

Акцент на 12% поверх холста, посчитанный заранее. Не `color-mix()` в рантайме:
пересчёт на каждой пилюле заметен в styleLayout на слабом CPU.

| Name | Value | Token | Role |
|------|-------|-------|------|
| Iris Wash | `#171c26` | `--color-iris-wash` | Фон холодной пилюли-тега |
| Ember Wash | `#2a1a16` | `--color-ember-wash` | Фон тёплой пилюли «Артефакт» |

### Границы

| Name | Value | Token | Role |
|------|-------|-------|------|
| Border | `#2b2f35` | `--color-border` | Разделители, границы карточек (декоративные, не интерактивные) |
| Border Strong | `#6b7280` | `--color-border-strong` | Границы инпутов, ghost-кнопок и всего кликабельного (≥3:1 на всех поверхностях) |

## Tokens — Typography

### Unbounded — display · `--font-display`
- **Роль:** только заголовки от 28px: h1 поста, hero главной, крупные секции.
- **Кириллица:** да (обязательное требование — блог на ru).
- **Замена:** Onest, Manrope.
- **Weights:** 400, 500, 600
- **Никогда:** в тексте, навигации, подписях, ниже 28px.

### Inter — интерфейс и тело текста · `--font-body`
- **Роль:** весь читаемый текст, навигация, кнопки, подписи, таблицы.
- **Weights:** 400, 500, 600
- **Tracking:** -0.01em в теле, normal в подписях, сжимается с ростом кегля.

### JetBrains Mono — код · `--font-mono`
- **Роль:** блоки кода, инлайн-код, цифры в таблицах метрик, `--text-caption` в технических подписях.
- **Weights:** 400, 500
- **Кириллица:** да (комментарии в примерах бывают на русском).

### Type Scale

| Role | Size | Line Height | Letter Spacing | Token |
|------|------|-------------|----------------|-------|
| caption | 13px | 1.45 | 0 | `--text-caption` |
| small | 15px | 1.55 | -0.01em | `--text-small` |
| body | 18px | 1.65 | -0.01em | `--text-body` |
| lead | 21px | 1.55 | -0.015em | `--text-lead` |
| h4 | 20px | 1.35 | -0.015em | `--text-h4` |
| h3 | 24px | 1.3 | -0.02em | `--text-h3` |
| h2 | 30px | 1.25 | -0.025em | `--text-h2` |
| h1 | 44px | 1.1 | -0.03em | `--text-h1` |
| display | 72px | 0.95 | -0.04em | `--text-display` |

На мобильных `display` → 44px, `h1` → 32px, `h2` → 25px. Тело текста не уменьшается никогда.

## Tokens — Spacing & Shapes

**Base unit:** 4px · **Density:** comfortable (блог, а не дашборд)

### Spacing Scale

| Name | Value | Token |
|------|-------|-------|
| 1 | 4px | `--spacing-1` |
| 2 | 8px | `--spacing-2` |
| 3 | 12px | `--spacing-3` |
| 4 | 16px | `--spacing-4` |
| 5 | 24px | `--spacing-5` |
| 6 | 32px | `--spacing-6` |
| 7 | 48px | `--spacing-7` |
| 8 | 64px | `--spacing-8` |
| 9 | 96px | `--spacing-9` |
| 10 | 144px | `--spacing-10` |

### Border Radius

| Element | Value | Token |
|---------|-------|-------|
| теги, чипы | 9999px | `--radius-pill` |
| кнопки | 9999px | `--radius-pill` |
| карточки, картинки | 12px | `--radius-card` |
| блоки кода, инпуты | 8px | `--radius-code` |

### Shadows

Тени — только для того, что реально всплывает над страницей. Карточки разделяются
границей и цветом поверхности, не тенью (правило донора, оставляем).

| Name | Value | Token |
|------|-------|-------|
| sm | `rgba(0,0,0,0.25) 0 2px 8px 0` | `--shadow-sm` |
| lg | `rgba(0,0,0,0.5) 0 6px 25px 0` | `--shadow-lg` |
| focus | `rgba(126,166,240,0.45) 0 0 0 3px` | `--shadow-focus` |

### Layout

- **Page max-width:** 1200px (`--max-page`)
- **Content max-width (текст поста):** 68ch (`--max-prose`)
- **Section gap:** 96px (`--spacing-9`)
- **Card padding:** 24px (`--spacing-5`)
- **Element gap:** 12px (`--spacing-3`)

## Components

### Primary Button
Заливка `--color-iris-deep`, текст `--color-canvas` (тёмный, не белый), Inter 15px/500,
`--radius-pill`, паддинг 12px 24px. Один на экран.

### Ghost Button
Прозрачный фон, 1px `--color-border-strong`, текст `--color-text`, Inter 15px/500,
`--radius-pill`, паддинг 10px 20px. На hover — фон `--color-card`.

### Post Row
Строка ленты постов — основной способ показа поста (решение концепта home-b).
Заголовок `--text-body` слева, справа — пилюля «Артефакт» (только если он есть)
и дата моноширинным `--text-caption` в `--color-text-subtle`.
Разделитель — хайрлайн `--color-border` снизу, паддинг `--spacing-4` по вертикали.
На hover заголовок уходит в `--color-iris`. На узких экранах — в две строки.

Обложки в ленте не показываются: концепт-раунд отклонил карточную сетку (home-f) —
однотипные обложки делают ленту неразличимой. Обложка живёт только в OG и соцсетях.

### Intent Nav
Навигация по намерению читателя вместо навигации по тегам. Строка «Что хотите сделать?»
в `--text-small` `--color-text-muted`, под ней ряд ghost-кнопок с задачами:
1px `--color-border-strong`, `--radius-pill`, паддинг `--spacing-2` `--spacing-4`,
текст `--text-small` `--color-text`. Активная — заливка `--color-iris-wash`,
текст и граница `--color-iris`.

Показываются только те намерения, под которые есть хотя бы один пост. Пустых кнопок нет.

Причина: тег описывает, про что мы написали; намерение — зачем читатель пришёл.
Читатель ищет «как собрать агентный пайплайн», а не «разборы».

### Month Group
Заголовок месяца в ленте: `--font-mono`, `--text-caption`, `--color-text-subtle`,
заглавные буквы, отступ сверху `--spacing-6`. Под ним — строки `Post Row`.

Нужен, когда постов больше десяти: без разбивки по месяцам архив читается как свалка.

### Feed Filter
Не визуальный компонент, а общее состояние ленты. Intent Nav и Search сообщают ему
своё условие, он один решает, какие строки видимы. Без этого поиск затирал фильтр
по намерению, потому что каждый компонент управлял `row.hidden` сам.

### Search
Одна строка поиска над лентой. Фон `--color-surface`, 1px `--color-border`,
`--radius-code`, паддинг `--spacing-3` `--spacing-4`, шрифт `--text-body`.
Плейсхолдер называет объём: «Поиск по N записям» — сообщает и функцию, и масштаб.
Фокус — `--shadow-focus`.

Поиск клиентский, по заголовку, описанию и тегам. Без JS поле скрыто:
неработающий инпут хуже отсутствующего.

### Related
Блок связанных записей в конце поста, перед подпиской. Заголовок «Читать дальше»
в `--text-h4` `--font-body`. Ниже до трёх строк формата `Post Row` без даты:
заголовок `--text-small` `--color-text-muted`, при наведении `--color-iris`.
Под каждым — причина связи в `--text-caption` `--color-text-subtle`:
общий тег или общее намерение.

Подбор автоматический: сначала совпадение по намерению (у читателя та же задача),
потом по тегам. Ручные ссылки внутри текста остаются главным способом связи —
этот блок только страховка для тех, кто дочитал.

Если связанных нет, блок не выводится. Пустой заголовок «Читать дальше» хуже отсутствия.

### Subscribe
Блок удержания в конце главной и каждого поста. Фон `--color-card`, 1px `--color-border`,
`--radius-card`, паддинг `--spacing-5`, отступ сверху `--spacing-8`.
Заголовок `--text-h4` в `--font-body` (ниже 28px — display запрещён), текст `--text-small`
в `--color-text-muted`, дальше Primary Button. Канал один — Telegram, email-формы нет.

### Footer
Фон `--color-surface`, верхняя граница `--color-border`, отступ сверху `--spacing-9`.
Слева — строка `--text-caption` в `--color-text-subtle`, справа — ссылки RSS и Telegram
в `--color-text-muted`, на hover `--color-iris`.

### Table of Contents
Только на странице поста, появляется от двух заголовков. На десктопе — липкая колонка
справа, на мобильных — `<details>` сверху в рамке `--radius-code`.
Пункты `--text-caption` в `--color-text-subtle`, активный — `--color-iris` с левым
индикатором 2px. Без JS остаётся обычным списком якорей.

### Prose
Контейнер статьи. Ширина `--max-prose`, абзацы `--text-body`, вертикальный ритм `--spacing-5`
между абзацами и `--spacing-7` перед заголовками. Ссылки — `--color-iris` с подчёркиванием
`text-underline-offset: 3px`. Изображения на всю ширину prose с подписью `--text-caption`
в `--color-text-subtle`.

На узком экране сетка статьи имеет одну сжимаемую колонку `minmax(0, 1fr)`;
колонки текста и оглавления не удерживают собственный `min-content` минимум.
Длинные ссылки, инлайн-код и ячейки таблиц переносятся при нехватке ширины
(`overflow-wrap: anywhere`). Блочный код сохраняет пробелы и прокручивается внутри
`pre`; горизонтальную прокрутку всей страницы не скрывать как обходной путь.

### Code Block
Фон `--color-code`, `--radius-code`, паддинг `--spacing-4`, 1px `--color-border`,
JetBrains Mono 15px/1.6, горизонтальный скролл без переноса. Подсветка — Shiki на нашей палитре.
Инлайн-код: фон `--color-card`, паддинг `--spacing-1` `--spacing-2`, `--radius-code`, без границы.

### Artifact Block
Фирменный блок «Артефакт» в конце каждого поста. Фон `--color-card`, левая граница 3px
`--color-ember`, `--radius-card` (кроме левого края), паддинг `--spacing-5`.
Заголовок «Артефакт» — Inter 15px/600 в `--color-ember`. Внутри — ссылка на скрипт/промпт/конфиг.
Это единственное место, где `ember` берёт на себя структурную роль.

### Callout
Врезка-вывод внутри текста. Фон `--color-surface`, левая граница 3px `--color-iris`,
паддинг `--spacing-4`. Для нашей личной позиции — граница `--color-ember`.

### Tag Pill
`--radius-pill`, паддинг 4px 12px, Inter 13px/500. Фон — акцент на 12% прозрачности,
текст — тот же акцент в полную силу.

### Header
Фон `--color-canvas`, ширина `--max-page`. Слева логотип в `--font-display` с точкой
в `--color-ember`, справа навигация `--text-small`, дальше переключатель языка и ссылка на TG
моноширинным `--text-caption`. Активный пункт — `--color-iris`.

**Переключатель языка показывается только там, где перевод реально существует** —
иначе он ведёт в 404. То же правило для `hreflang`.

RU/EN используют одни и те же утверждённые макеты home-b/post-h и общие компоненты.
Коллекции разделены по языку; `translationOf` задаёт взаимную пару, а canonical
указывает на страницу текущего языка. Каждая публикуемая статья имеет опубликованную
взаимную уникальную пару; черновик не заменяет партнёра. Неполная пара блокирует сборку,
а не скрывается за равными счётчиками. EN меняет подписи и содержимое, не токены,
размеры или расположение элементов.

### Aurora Strip
Только главная, один раз на страницу. **Горизонтальная полоса 3px под навигацией**:
linear-gradient 90deg от `--color-iris-deep` через `--color-iris` и `--color-ember` к `--color-text`.

Изначально спека описывала вертикальный луч на 15-25% ширины, как у донора.
Концепт-раунд run-01 выбрал home-b, где аврора — тонкая горизонтальная полоса: она не отбирает
место у текста и при этом узнаваема в превью. Вертикальный луч остался только в OG-картинках
как верхняя полоса 6-8px (`src/lib/og.ts`).

Никогда как заливка всего фона. На странице поста авроры нет.

### Reading Progress
Тонкая полоса 2px сверху страницы поста, заливка `--color-iris`, без тени и анимации кроме ширины.

## Do's and Don'ts

### Do
- Сначала выбрать поверхность (`canvas` / `surface` / `card` / `code`), потом цвет текста на ней.
- Ссылки в тексте — `--color-iris` с подчёркиванием; полагаться на один цвет без подчёркивания нельзя.
- `ember` использовать точечно: «Артефакт», цифры-результаты, врезка с нашей позицией.
- Держать тело текста на 18px и `--max-prose` 68ch — это не настройка, а требование.
- `--font-display` только от 28px и выше.
- Фокус видимый всегда: `:focus-visible` → `--shadow-focus`, никакого `outline: none`.
- Вертикальный ритм строить на `--spacing-*`, а не на подобранных вручную значениях.

### Don't
- Не вводить третий акцентный цвет. Пара iris/ember — весь словарь.
- Не ставить белый текст на `iris-deep` или `ember` — контраст 3.71 и 2.33, это FAIL.
- Не делать светлых секций и светлой темы. Система dark-only.
- Не заливать аврорным градиентом фон целиком и не повторять его на странице дважды.
- Не использовать тени для разделения карточек — граница и поверхность.
- Не опускать текст ниже `--color-text-subtle` (`#8d929b`).
- Не использовать `--font-display` в теле, навигации и подписях.
- Не писать цвет, размер или радиус мимо токена. Ни одного `#hex` и ни одного «магического» px в компонентах.

## Surfaces

| Level | Name | Value | Purpose |
|-------|------|-------|---------|
| 0 | Canvas | `#0b0c0e` | Фон страницы |
| 1 | Code | `#0f1114` | Блоки кода — единственная поверхность «вглубь» |
| 2 | Surface | `#131518` | Шапка, подвал, полосы-секции |
| 3 | Card | `#1a1d21` | Карточки, врезки, панели |

## Elevation

- **Всплывающая панель / меню:** `--shadow-lg`
- **Карточка при hover:** `--shadow-sm` + граница `--color-border-strong`
- **Фокус:** `--shadow-focus`
- Всё остальное — плоское.

## Imagery

Обложки постов генерятся конвейером из этих же токенов (Task 5 / Task 9d) — своя аврора,
своя типографика, никакого стока и 3D-рендеров. Внутри статей — только скриншоты реальных
терминалов, дашбордов и кода, обёрнутые в `--radius-card` с границей `--color-border`.
Иконки — монохромные линейные в `--color-text-subtle` или `--color-text`, никогда цветные.
Лица и лайфстайл-фото не используем (решение по бренду: фокус на продукте, не на персоне).

## Layout

Главная: аврорный hero с заголовком в `--font-display` слева, ниже — max-width 1200px и лента
карточек постов в одну колонку на мобильных, две на десктопе, с шагом `--spacing-9` между секциями.

Страница поста: без авроры. Шапка → h1 в `--font-display` → строка метаданных → лид `--text-lead`
→ `Prose` шириной `--max-prose` по центру → блок «Артефакт» → prev/next → подвал.
Оглавление для постов длиннее 1500 слов — липкой колонкой справа на десктопе, схлопнутым
`<details>` сверху на мобильных.

Композиционный приём взят у Intercom-референса: одна задача на экран, один явный CTA,
крупная типографика, микрокопирайт под элементами. Палитра оттуда НЕ берётся.

## Agent Prompt Guide

Перед вёрсткой любого экрана агент обязан:

1. Прочитать `design/llms.txt` — там запреты в исполняемом виде.
2. Выбрать поверхность из раздела Surfaces, затем цвет текста из таблицы контрастов.
3. Взять готовый компонент из раздела Components; если нужного нет — сначала описать его здесь,
   потом верстать. Компоненты не изобретаются на ходу.
4. Проверить себя: `grep -nE '#[0-9a-fA-F]{3,8}|[0-9]+px' src/components src/layouts` должен
   давать пусто (кроме `tokens.css`).

## Quick Color Reference

- Фон: `#0b0c0e` · карточка: `#1a1d21` · код: `#0f1114`
- Текст: `#eceef1` · вторичный: `#b6bac1` · тихий: `#8d929b`
- Ссылка / холодный акцент: `#7ea6f0` · заливка кнопки: `#5683da`
- Тёплый акцент (артефакт, цифры): `#ff8964`
- Граница: `#2b2f35` · интерактивная граница: `#6b7280`
- Текст на любой заливке-акценте: `#0b0c0e` (тёмный, не белый)
