# Новая главная: «Пульс»

Статус: принята · 12.10.2026 · issue #101
Вид выбрал Никита 12.10.2026: светлый первый экран по его референсу, собранный кодом без растровых картинок. Прототип (одна HTML-страница с нашей моделью) лежит в общих файлах проекта, `research/home-prototype-code-only.html`; скриншоты `visuals/home-prototype-light.png` и `visuals/home-prototype-dark-valves.png`.
Опирается на спеку вьюера ([2026-10-11-stage-1-viewer.md](2026-10-11-stage-1-viewer.md)), часы сердца ([2026-10-11-stage-2-heart-clock.md](2026-10-11-stage-2-heart-clock.md)) и бюджеты этапа 4 ([2026-10-11-stage-4-release.md](2026-10-11-stage-4-release.md)).

## Цель

Сейчас главная это заголовок, абзац и четыре ссылки: «выглядит как учебник». Новая главная сразу показывает, что в атласе есть живая 3D-модель, ЭКГ, 20 статей с источниками, и ведёт в разделы. Анимация красивая на сильных устройствах и отключается на слабых и по желанию читателя.

## Что входит

1. Дизайн-токены сайта: палитра светлой и тёмной темы, шрифты, радиусы, тени «стекла».
2. Настройка анимации «Полная / Спокойная / Стоп» в шапке каждой страницы, с режимом «Авто» по умолчанию.
3. Первый экран главной: текст и кнопки, силуэт тела с сосудами, 3D-сердце в центре, шесть карточек вокруг него с линиями к структурам модели, колонка с ЭКГ.
4. Ряд «Исследуйте ключевые разделы» из шести карточек с рисунками в SVG/CSS.
5. Новые шапка и подвал на всех страницах в тех же токенах.

## Что не входит

- Фотореалистичное «стеклянное» тело из референса: нужна 3D-модель тела с совместимой лицензией, её нет. Силуэт стилизованный, SVG.
- Разделы «Лечение», «Операции», «Учебные материалы» и видео из референса: их нет на сайте. Карточки ведут только на то, что существует; новые разделы добавят карточку, когда появятся.
- Переделка страниц статей и вьюера сверх шапки, подвала и токенов.
- Признаки болезней на модели: это спека [2026-10-12-disease-signs-3d.md](2026-10-12-disease-signs-3d.md), другой тред. Главная использует модель нормального сердца.

## Токены

В `apps/web/src/styles/base.css`, в том же слое `base`. Имена старых переменных (`--color-bg`, `--color-text`, `--color-muted`, `--color-accent`, `--color-border`, `--color-notice-bg`) остаются, их используют статьи и встраивания; новые добавляются рядом. `base.css` грузит и `EmbedLayout.astro`, поэтому новые фон и акцент получают и встраивания на чужих сайтах; это намеренно, фон остаётся почти белым. Значения из прототипа, акцент подобран под контраст:

| Токен | Светлая | Тёмная | Где |
|---|---|---|---|
| `--color-bg` | `#f3f7fc` | `#070d1c` | фон |
| `--color-bg-2` | `#e6effa` | `#0d1932` | радиальная подсветка фона |
| `--color-text` | `#10213f` | `#e8eefc` | текст |
| `--color-muted` | `#5a6b86` | `#9fb0d4` | вторичный текст |
| `--color-blue` | `#1f5fd1` | `#7aa7ff` | акцент «знания»: ссылки в шапке, ЭКГ, «сердца» в заголовке |
| `--color-accent` | `#c2332e` | `#ff6b70` | акцент «кровь»: ссылки, главная кнопка, линии к структурам, активный пункт меню |
| `--color-on-accent` | `#fff` | `#070d1c` | текст на акценте (главная кнопка, нажатые переключатели) |
| `--color-vessel-a` / `--color-vessel-v` | `#ef4b4b` / `#3b78e7` | `#ff4d5e` / `#4c7dff` | артерии и вены на силуэте; та же условность, что у модели |
| `--glass`, `--glass-line`, `--shadow` | как в прототипе | как в прототипе | карточки |

Контраст по WCAG AA (4,5:1 для текста): ссылки `#c2332e` на `#f3f7fc` 5,1:1 и на `#e6effa` 4,7:1; `#ff6b70` на `#070d1c` 7,0:1; белый на `#c2332e` 5,5:1; `#070d1c` на `#ff6b70` 7,0:1; `--color-muted` 5,0:1 и 8,9:1. Если значение меняют, его меняют в этой таблице, а не в компоненте. Тема по-прежнему следует системе (`prefers-color-scheme`); ручного переключателя темы в этой работе нет.

Шрифты: Manrope (заголовки), Onest (текст), Caveat (одна рукописная строка на главной, 600). Все три под SIL OFL 1.1, с кириллицей. Manrope и Onest берём вариативными (`@fontsource-variable/*`, один файл на подмножество вместо файла на каждый вес), Caveat подключается только на главной: на сборке из него вырезаются символы рукописной строки языка страницы, и этот кусок (около 10 КБ) встраивается в HTML, чтобы строка не перерисовывалась при загрузке шрифта. Всё через Fontsource, без Google Fonts: сайт работает офлайн и не отдаёт адреса читателей третьей стороне. `font-display: swap`, у каждого семейства системный запасной стек. Бюджет шрифтов на главной: не больше 150 КБ woff2 для кириллицы и латиницы вместе; `scripts/js-budget.ts` начинает считать и шрифты главной. Языки без кириллицы и латиницы получают системный шрифт, пока не добавят своё подмножество.

## Настройка анимации

Уровень пишется в `<html data-motion="full|calm|off">` скриптом в `<head>` до отрисовки, как уже делает `LevelScript.astro` для уровня объяснения. Выбор хранится в `localStorage['heart-atlas:motion']` (`auto`, `full`, `calm`, `off`); недоступное хранилище значит `auto`.

`auto` выбирает так, первое совпадение побеждает:

| Сигнал | Уровень |
|---|---|
| `prefers-reduced-motion: reduce` | `off`. Системная настройка человека главнее устройства |
| `navigator.connection.saveData` | `calm` |
| `navigator.deviceMemory ≤ 4` или `navigator.hardwareConcurrency ≤ 4` | `calm` |
| `(pointer: coarse), (max-width: 48rem)`, как `prefersLowDetail` во вьюере | `calm` |
| иначе | `full` |

Ручной выбор (`full`, `calm`, `off`) уважается, кроме одного случая: при `prefers-reduced-motion: reduce` выбор `full` даёт `calm`, а не полную анимацию. Человек может включить 3D кнопкой, но ничего не начнёт двигаться само в полную силу.

Что значит уровень:

| | Полная | Спокойная | Стоп |
|---|---|---|---|
| 3D-сердце на главной | WebGL, бьётся от своих часов сердца, медленно поворачивается, пока его не трогают | постер модели, лёгкая пульсация CSS; кнопка «Покрутить в 3D» загружает модель | постер без движения; кнопка «Покрутить в 3D» |
| ЭКГ в колонке | бежит | перерисовка ~15 кадров/с | неподвижная запись |
| Кровоток по сосудам, кольца, плавание карточек | есть | нет | нет |
| Наведение на карточку | подсветка структуры на модели и линия | то же на постере (подсветка линии и точки) | то же |

Модуль `apps/web/src/lib/motion.ts`: `resolveMotion(choice, signals)` (чистая функция, тестируется), `currentMotion()`, событие `heart-atlas:motion` на `window` при смене. Острова (сердце, ЭКГ) подписываются на событие и останавливают кадр, когда блок вне экрана (`IntersectionObserver`) или вкладка скрыта.

Переключатель в шапке: группа из четырёх кнопок «Авто / Полная / Спокойная / Стоп» с `aria-pressed`, нажата ровно одна, подпись группы «Анимация». В режиме «Авто» уровень, который он выбрал, отмечен рамкой на своей кнопке. Строки в `messages/<locale>.json`: `motion.label`, `motion.auto`, `motion.full`, `motion.calm`, `motion.off`; `motion.load3d` добавляет PR 3. Вьюер на `/anatomy/` и часы сердца сейчас смотрят только на `prefers-reduced-motion`; в PR 4 они начинают учитывать и `data-motion="off"`.

## Первый экран

Сетка: текст | сцена | колонка (с 1280 px колонка уходит под сцену строкой, с 900 px всё в один столбец, карточки сцены сеткой под сердцем, линии скрыты). Только логические свойства, сцена зеркалится при `dir="rtl"`.

**Текст.** Надзаголовок, `h1` «Атлас сердца» (второе слово градиентом `--color-blue`), три строки подзаголовка, абзац, кнопки «Исследовать атлас» (→ `/anatomy/`) и «Часы сердца» (→ `/heart-clock/`), три факта. Факт «N статей с источниками» считается из коллекции на сборке, как сейчас считаются разделы на главной, чтобы число не устаревало. Тексты в `home.*` словарей; цифр о сердце в текстах нет (в прототипе было «около ста тысяч ударов в сутки», убираем: без источника на главной чисел о физиологии не пишем).

**Силуэт тела.** Компонент Astro `BodyBackdrop.astro` строит SVG на сборке: контур торса, рёбра и дерево сосудов из детерминированного генератора с фиксированным зерном. В HTML уходит готовый SVG, JS не нужен. Кровоток в режиме `full` это CSS-анимация `stroke-dashoffset` по главным стволам. Рисунок декоративный (`aria-hidden`) и не претендует на анатомическую точность сосудов тела; это сказано в комментарии компонента.

**Сердце.** Остров `HeroHeart` в `packages/viewer` (новый экспорт рядом с `HeartViewer`, общий загрузчик модели и шейдер сердцебиения из `Scene.tsx`): прозрачный фон, без панели управления, вращение мышью, касанием и стрелками, `role="img"` с подписью. Ритм от собственных часов сердца, как `cycle="own"` во вьюере (ЧСС 64). Загружается только при `data-motion="full"`, когда сцена видна и страница простаивает (`requestIdleCallback`), чтобы Lighthouse и первый экран не ждали three.js. До загрузки и в других режимах виден постер модели; переход постер → 3D без сдвига разметки и без смены фона.

Нынешний постер (`assets/models/heart/poster.png`) непрозрачный, на белом, и снят неизвестной камерой. PR 2 снимает новый с прозрачным фоном скриптом `packages/models/scripts/render-poster.ts`: он открывает вьюер в Chromium (как e2e) с камерой из общих констант `HOME`, `TARGET` и fov в `packages/viewer/src/camera.ts`, сохраняет `assets/models/heart/hero-poster.webp` с альфа-каналом (WebP: около 25 КБ против сотен КБ у PNG) и тем же расчётом пишет `assets/models/heart/anchors.json` (см. «Линии»): точку каждой видимой структуры, найденную по буферу id. Оба файла в репозитории, запись в `assets/models/LICENSES.yaml` обновляется. Скрипт запускают руками при смене модели, а тест проверяет, что якоря есть для каждой карточки.

Как сделано в PR 3: страница импортирует `@heart-atlas/viewer/hero` динамически, поэтому React, three.js и модель не входят в JS первого экрана (`js-budget` не считает этот вход). Холст той же высоты, что постер, и шире него на 30 % с каждой стороны: при том же вертикальном поле зрения модель стоит ровно там, где на постере, а повёрнутое сердце не обрезается по бокам. Точки линий находятся лучом из камеры постера через точку `anchors.json` до видимой поверхности структуры и каждый кадр проецируются живой камерой. Поворот только вокруг вертикальной оси; на сенсорных экранах вертикальная прокрутка страницы остаётся за браузером. Лёгкая CSS-пульсация постера в «Спокойной» переходит в PR 4.

Вьюер и признаки болезней: PR 3 не выносит и не меняет код загрузки и шейдер в `Scene.tsx`, над которыми работает тред признаков болезней ([2026-10-12-disease-signs-3d.md](2026-10-12-disease-signs-3d.md)). `HeroHeart` рендерит существующий `Model` без пропа `disease`, со своим правилом «призрака»; если тот PR ещё не в `main`, PR 3 вливает `main` после него.

**Карточки вокруг сердца.** Шесть ссылок. При наведении и фокусе карточка подсвечивает свои структуры событием `heart-atlas:highlight`; событие принимает id структур, поэтому группы раскрываются в id на сборке из `structures.yaml`, а не перечисляются в коде.

| Карточка | Ссылка | Подсветка (группы `structures.yaml`) | Остальная модель | Точка линии |
|---|---|---|---|---|
| Анатомия | `anatomy/heart-structure/` | нет | как есть | верхушка сердца (`left-ventricle`) |
| Камеры сердца | `anatomy/heart-chambers/` | `chamber` | как есть | `right-ventricle` |
| ЭКГ | `diagnostic/ecg/` | `conduction` | полупрозрачная | `sa-node` |
| Клапаны | `anatomy/heart-valves/` | `valve` | полупрозрачная | `pulmonary-valve` |
| Кровоток | `anatomy/coronary-circulation/` | `coronary-artery`, `cardiac-vein` | как есть | `right-coronary-artery` |
| Заболевания | `disease/` | нет | как есть | `left-atrium` |

Полупрозрачность у `HeroHeart` своя: становится прозрачным всё, кроме подсвеченного (во вьюере «призрак» касается только камер). Если статьи нет на языке страницы, карточка ведёт на раздел: для статей анатомии это `/anatomy/` (у анатомии нет страницы-списка), для ЭКГ `diagnostic/`. Карточки «Заболевания» нет, если в языке нет статей о болезнях, как сейчас на главной.

**Линии.** От карточки к точке на структуре из таблицы. Берётся не центр рамки группы (у клапанов и коронаров он внутри сердца), а одна видимая структура. Атриовентрикулярный узел и аортальный клапан спереди не видны, поэтому линия ЭКГ идёт к синусовому узлу, а линия клапанов к клапану лёгочного ствола. Для постера точки в долях кадра лежат в `anchors.json` (их пишет скрипт постера). В 3D линии пересчитываются каждый кадр и следуют за ударом и поворотом.

**Колонка.** Рукописная строка (Caveat), ЭКГ, короткий текст и «Открытый проект для всего мира». ЭКГ та же синтетическая запись, что на странице часов: форма волн из `beatWaves` пакета `@heart-atlas/heart-clock`, а не своя формула. В PR 2 это неподвижный SVG, посчитанный на сборке (работает без JS); в PR 4 поверх него бежит canvas, рисованный кодом `apps/web/src/components/heart-clock/ecg-canvas.ts`.

Как сделано в PR 4: колонка с ЭКГ рисует на canvas ту же синтетическую запись через `traceEcg` из `ecg-canvas.ts` (общий с монитором часов код линии); часы сердца подгружаются динамически, только если уровень не `off`. В `full` кадр каждый раз, в `calm` около 15 кадров/с, в `off` и вне экрана остаётся SVG. Карточки плавают на несколько пикселей в `full`, их линии следуют за ними; от точек линий расходятся кольца; по стволам сосудов бежит кровоток (по венам к сердцу). Постер слегка бьётся в `full` и `calm`, пока его не заменило 3D. Картинки ряда разделов двигаются только в `full`. Вьюер без инерции при `off`, часы сердца при `off` не запускаются сами и встают на паузу при переключении на `off`, схемы в статьях при `off` неподвижны, как при `prefers-reduced-motion`.

## Шапка и подвал

Как сделано в PR 5: `SiteHeader.astro` и `SiteFooter.astro` в `BaseLayout.astro`, на всех страницах, кроме встраиваний. В шапке знак (сердце с линией ЭКГ, SVG), название и подпись `site.tagline`, справа поиск, настройка анимации и языки; второй строкой меню разделов. Меню собирает `siteNav` из `lib/site-nav.ts`: главная, анатомия (страница с 3D есть в каждом языке), разделы статей, которые есть в языке страницы (в том числе статьи из запасного языка), часы сердца. Текущий пункт отмечен `aria-current="page"`, раздел статьи `aria-current="true"`. На узком экране меню прокручивается вбок внутри себя, страница не прокручивается. Шапка не липкая: иначе она закрывала бы цели якорей в статьях. Подвал: название, дисклеймер слово в слово, списки «Разделы» и «О проекте» (дисклеймер, исходный код, встраивание) и строка лицензий; списки в подвале не отдельные ориентиры `nav`, чтобы у страницы не было двух навигаций с одним именем. Ширина шапки и подвала `--site-width` (76rem, на главной 90rem), колонка текста статей прежняя.

## Ряд разделов

Шесть карточек: Анатомия, Болезни, Диагностика, 3D-модель, Часы сердца, Открытый код (GitHub). У каждой рисунок в inline SVG или CSS, анимированный только в `full`. Подписи и счётчики статей из коллекции и словарей.

## Производительность и доступность

- JS главной без 3D укладывается в бюджет этапа 4 (100 КБ gzip). Сейчас `scripts/js-budget.ts` считает только статьи; PR 2 добавляет `/<locale>/index.html`. three.js (около 270 КБ gzip) и модель (409 КБ lod1 на телефонах, 594 КБ lod0) грузятся только в `full` после простоя.
- Lighthouse на `/ru/` и `/en/` держит пороги этапа 4 (performance и accessibility ≥ 0,9).
- axe без нарушений на главной в светлой и тёмной теме и во всех трёх режимах.
- Всё, что двигается, дублируется статичным смыслом: подписи карточек, постер, неподвижная ЭКГ.
- Нет горизонтальной прокрутки на ширине 320 px.

## Проверки

- Unit: `resolveMotion` по таблице сигналов; скрипт до отрисовки работает как исходная функция; генератор силуэта детерминирован (снимок); якорь есть для каждой карточки и лежит внутри кадра постера.
- e2e (дополняем `e2e/home.spec.ts`): главная в `full`, `calm`, `off` и при эмуляции reduced motion; в `off` и при reduced motion three.js не запрашивается; выбор сохраняется между страницами; наведение на «Клапаны» ставит подсветку; все ссылки карточек отдают 200 с `BASE_URL`; нет горизонтальной прокрутки на 320 px; axe.
- Пиксельная проверка, что в `full` сердце двигается, а в `off` нет (`e2e/pixels.ts`).

## План PR

1. Токены, шрифты, настройка анимации в шапке и `lib/motion.ts` (issue на PR).
2. Главная без 3D: текст, факты из коллекции, силуэт на сборке, новый прозрачный постер и якоря, карточки с линиями, ряд разделов, колонка с неподвижной ЭКГ в SVG, бюджет JS и шрифтов главной. Работает без JS.
3. `HeroHeart`: 3D-сердце на главной, подсветка по карточкам, линии за моделью, загрузка только в `full`.
4. Движение: ЭКГ из часов сердца, кровоток, кольца; `calm` и `off` для всего сайта, вьюер и часы сердца учитывают `data-motion`.
5. Новые шапка и подвал на всех страницах.

Каждый PR: тесты, линт, типы, e2e, зелёный CI, независимое ревью. PR 3 добавляет `HeroHeart` в `packages/viewer` отдельным файлом и не трогает загрузку модели и шейдер, над которыми работает тред признаков болезней.
