Files
kvarta_project/DESIGN.md
T

564 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: Кварта
description: Лендинг минской обжарки — «недельный календарь и штамп даты»
colors:
cream: "#F3EBDD"
paper: "#FBF7F0"
kraft: "#DCC7A1"
roast: "#1E120C"
ink: "#2A1810"
muted: "#5E4636"
placeholder: "#7A6352"
rule-soft: "#C9B595"
cherry: "#B3221B"
cherry-deep: "#8F1A14"
cherry-on-dark: "#E8604F"
muted-on-dark: "#BFA88A"
ok: "#2E5E3A"
typography:
display:
fontFamily: "Golos Text, system-ui, sans-serif"
fontSize: "clamp(2.75rem, 1.6rem + 5.2vw, 6rem)"
fontWeight: 900
lineHeight: 0.92
letterSpacing: "-0.035em"
headline:
fontFamily: "Golos Text, system-ui, sans-serif"
fontSize: "clamp(2rem, 1.4rem + 2.8vw, 3.5rem)"
fontWeight: 800
lineHeight: 1
letterSpacing: "-0.03em"
title:
fontFamily: "Golos Text, system-ui, sans-serif"
fontSize: "clamp(1.5rem, 1.3rem + 0.6vw, 1.875rem)"
fontWeight: 800
lineHeight: 1.05
letterSpacing: "-0.02em"
lead:
fontFamily: "Golos Text, system-ui, sans-serif"
fontSize: "clamp(1.125rem, 1rem + 0.5vw, 1.375rem)"
fontWeight: 400
lineHeight: 1.45
body:
fontFamily: "Golos Text, system-ui, sans-serif"
fontSize: "1.0625rem"
fontWeight: 400
lineHeight: 1.55
small:
fontFamily: "Golos Text, system-ui, sans-serif"
fontSize: "0.875rem"
fontWeight: 500
lineHeight: 1.45
mono-tag:
fontFamily: "Martian Mono, ui-monospace, monospace"
fontSize: "0.75rem"
fontWeight: 500
lineHeight: 1.3
letterSpacing: "0.08em"
mono-price:
fontFamily: "Martian Mono, ui-monospace, monospace"
fontSize: "clamp(1.375rem, 1.2rem + 0.8vw, 2rem)"
fontWeight: 600
lineHeight: 1
letterSpacing: "-0.02em"
fontFeature: "\"tnum\" 1"
mono-date:
fontFamily: "Martian Mono, ui-monospace, monospace"
fontSize: "clamp(1.75rem, 1.2rem + 2.2vw, 3.25rem)"
fontWeight: 700
lineHeight: 1
letterSpacing: "-0.04em"
fontVariation: "\"wdth\" 87.5"
stamp:
fontFamily: "Martian Mono, ui-monospace, monospace"
fontSize: "1rem"
fontWeight: 700
lineHeight: 1
letterSpacing: "0.04em"
fontVariation: "\"wdth\" 112.5"
rounded:
none: "0px"
hair: "2px"
spacing:
"1": "4px"
"2": "8px"
"3": "12px"
"4": "16px"
"5": "20px"
"6": "24px"
"8": "32px"
"12": "48px"
"18": "72px"
"32": "128px"
components:
button-primary:
backgroundColor: "{colors.cherry}"
textColor: "{colors.paper}"
rounded: "{rounded.none}"
padding: "0 24px"
height: "52px"
button-primary-hover:
backgroundColor: "{colors.cherry-deep}"
textColor: "{colors.paper}"
button-secondary:
backgroundColor: "transparent"
textColor: "{colors.ink}"
rounded: "{rounded.none}"
padding: "0 20px"
height: "52px"
button-secondary-hover:
backgroundColor: "{colors.ink}"
textColor: "{colors.cream}"
input:
backgroundColor: "{colors.paper}"
textColor: "{colors.ink}"
rounded: "{rounded.none}"
padding: "0 14px"
height: "52px"
label-card:
backgroundColor: "{colors.kraft}"
textColor: "{colors.ink}"
rounded: "{rounded.none}"
padding: "20px"
---
# Design System: Кварта
## Overview
**Creative North Star: «Четверг на этикетке»**
«Кварта» — четвёртая, и весь сайт стоит на четверге. Страница читается как настенный календарь и крафтовая пачка одновременно: неделя ПН…ВС набрана плотным гротеском с тонкими линейками, четверг (обжарка) и пятница (доставка) выделены красным кофейной ягоды, а на пачках и в первом экране стоит резиновый штамп с датой ближайшей обжарки. Свежесть доказывается датой, а не словом «свежий». Ассортимент — этикетки пачек: название гротеском, ноты строкой через «•», вес и цена моноширинным, по диагонали лента-наклейка «обжарено в чт».
Плотно, плоско, печатно. Никаких теней, градиентов и скруглений: глубину дают линейки, крафт и штамп. Красный — только там, где речь о дне, действии или ошибке.
**Key Characteristics:**
- Кремовый фон, тёмно-коричневые текст и линейки, один акцент — красный ягоды.
- Golos Text для всего текста, Martian Mono только для дат, цен, веса, часов и штампов.
- Радиус 0. Тени 0. Линейки 1px и 2px цвета `ink`.
- Подпись бренда — штамп даты с «неровной краской» (SVG-фильтр) и лента недели.
- Две авторские анимации: оттиск штампа и «заполнение» недели. Остальное — состояния.
### Словарь приёмов
| Приём | Что это | Где используется |
|---|---|---|
| **Лента недели** (`WeekStrip`) | 7 дней «буква + число», тонкие линейки, ЧТ и ПТ красным, подписи «обжарка»/«доставка» на линейке, скобка «1–3 день — в чашке» под ПТ–ВС | Блок «почему свежесть» (полная), OG-картинка (статичная), микро-версия в логотипе (7 штрихов, 4-й красный) |
| **Штамп** (`Stamp`) | Двойная прямоугольная рамка, текст Martian wdth 112.5 капсом, поворот −7…+5°, смещение, неровность краски | Первый экран (дата ближайшей обжарки, анимирован), поле «дата обжарки» на этикетках (статичный), успех формы «ПРИНЯТО» (анимирован), OG |
| **Этикетка** (`Label`) | Крафт-прямоугольник пачки, контент разделён линейками на поля, как на печатной этикетке | Карточки сортов, дрип-пакеты, подписка |
| ~~Лента-наклейка~~ | Снята в итерации критика 1: повторяла штамп «ЧТ 22.10» на каждой этикетке, 8 красных пятен подряд | Нигде |
| **Поле этикетки** | Подпись mono-tag слева, значение справа, линейка 1px снизу | Внутри Label, в блоке подписки, в часах самовывоза |
## Colors
Палитра печатной пачки: крем бумаги, крафт, кофейные чернила и одна красная краска.
### Primary
- **Красный ягоды** (`cherry` #B3221B): основная кнопка, ЧТ/ПТ в ленте недели, штамп, лента-наклейка, линия ошибки. Текст `cherry` допустим на `cream` (5.61:1) и `paper` (6.22:1), но не на `kraft` (4.03:1 — только крупный текст ≥24px или 700 ≥18.66px).
- **Красный глубокий** (`cherry-deep` #8F1A14): hover/active основной кнопки, текст ошибок полей (8.45:1 на `paper`, 7.63:1 на `cream`), красный мелкий текст на крафте (5.47:1).
- **Красный на тёмном** (`cherry-on-dark` #E8604F): акцентный текст и штрихи на `roast` (5.42:1). Только в тёмных секциях.
### Neutral
- **Крем** (`cream` #F3EBDD): фон страницы. Сплошной, без градиентов и шума.
- **Бумага** (`paper` #FBF7F0): фон полей формы, блока формы, уведомлений; текст на красных кнопках и ленте.
- **Крафт** (`kraft` #DCC7A1): поверхность этикеток и блока подписки. Только сплошной цвет, без текстуры-картинки.
- **Обжарка** (`roast` #1E120C): фон секции «доставка/самовывоз» и подвала.
- **Чернила** (`ink` #2A1810): весь основной текст, линейки, рамки, фокус-обводка на светлом.
- **Приглушённый** (`muted` #5E4636): второстепенный текст, подписи полей этикетки, рамка полей формы.
- **Плейсхолдер** (`placeholder` #7A6352): только `::placeholder` на `paper` (5.27:1).
- **Линейка мягкая** (`rule-soft` #C9B595): декоративные разделители внутри одной группы (1.69:1 к крему — не несёт смысла, не годится для рамок контролов). Фон disabled-кнопки.
- **Приглушённый на тёмном** (`muted-on-dark` #BFA88A): второстепенный текст на `roast` (8.01:1).
- **Успех** (`ok` #2E5E3A): только иконка-галочка и текст в уведомлении «заявка принята» (6.38:1 на `cream`, 7.08:1 на `paper`). Нигде больше.
### Контраст (WCAG 2.x, проверено)
| Пара | Контраст | Использование |
|---|---|---|
| ink / cream | 14.34 | основной текст |
| ink / paper | 15.90 | поля, форма |
| ink / kraft | 10.29 | этикетки |
| muted / cream | 7.37 | второстепенный |
| muted / kraft | 5.29 | подписи на этикетке |
| muted / paper | 8.18 | рамка поля (≥3:1 ок) |
| placeholder / paper | 5.27 | плейсхолдер |
| cherry / cream | 5.61 | красный текст, ЧТ/ПТ |
| paper / cherry | 6.22 | основная кнопка, лента |
| paper / cherry-deep | 8.45 | hover кнопки |
| cherry-deep / kraft | 5.47 | красный мелкий текст на крафте |
| cream / roast | 15.46 | текст на тёмном |
| muted-on-dark / roast | 8.01 | второстепенный на тёмном |
| cherry-on-dark / roast | 5.42 | акцент на тёмном |
| ok / paper | 7.08 | успех |
### Named Rules
**Правило одной краски.** Красный занимает ≤10% экрана. Если на экране уже есть красная кнопка, штамп рядом делается без анимации.
**Правило чёрного по крафту.** На `kraft` мелкий красный текст — только `cherry-deep`.
### Tailwind 4 `@theme` (вставить в `apps/landing/src/app/globals.css`)
```css
@import 'tailwindcss';
@theme {
--color-*: initial; /* отключить палитру Tailwind: никаких neutral-*/red-* */
--color-cream: #F3EBDD;
--color-paper: #FBF7F0;
--color-kraft: #DCC7A1;
--color-roast: #1E120C;
--color-ink: #2A1810;
--color-muted: #5E4636;
--color-placeholder: #7A6352;
--color-rule-soft: #C9B595;
--color-cherry: #B3221B;
--color-cherry-deep: #8F1A14;
--color-cherry-on-dark: #E8604F;
--color-muted-on-dark: #BFA88A;
--color-ok: #2E5E3A;
--color-transparent: transparent;
--color-current: currentColor;
--radius-*: initial;
--radius-none: 0px;
--radius-hair: 2px;
--shadow-*: initial;
--text-display: clamp(2.75rem, 1.6rem + 5.2vw, 6rem);
--text-display--line-height: 0.92;
--text-display--letter-spacing: -0.035em;
--text-headline: clamp(2rem, 1.4rem + 2.8vw, 3.5rem);
--text-headline--line-height: 1;
--text-headline--letter-spacing: -0.03em;
--text-title: clamp(1.5rem, 1.3rem + 0.6vw, 1.875rem);
--text-title--line-height: 1.05;
--text-title--letter-spacing: -0.02em;
--text-lead: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
--text-lead--line-height: 1.45;
--text-body: 1.0625rem;
--text-body--line-height: 1.55;
--text-small: 0.875rem;
--text-small--line-height: 1.45;
--text-tag: 0.75rem;
--text-tag--line-height: 1.3;
--text-tag--letter-spacing: 0.08em;
--text-price: clamp(1.375rem, 1.2rem + 0.8vw, 2rem);
--text-price--line-height: 1;
--text-date: clamp(1.75rem, 1.2rem + 2.2vw, 3.25rem);
--text-date--line-height: 1;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
--ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0);
--container-page: 1280px;
}
@theme inline {
--font-sans: var(--font-golos), system-ui, sans-serif;
--font-mono: var(--font-martian), ui-monospace, monospace;
}
@layer base {
html { background: var(--color-cream); color: var(--color-ink); scroll-padding-top: 16px; }
body { font-family: var(--font-sans); font-size: var(--text-body); line-height: 1.55; -webkit-tap-highlight-color: transparent; }
::selection { background: var(--color-cherry); color: var(--color-paper); }
:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 3px; }
.on-dark :focus-visible { outline-color: var(--color-cream); }
input, textarea { caret-color: var(--color-cherry); }
a { text-decoration-thickness: 1px; text-underline-offset: 3px; }
}
```
После `--color-*: initial` существующие `border-neutral-*`, `text-red-700` в `LeadForm.tsx` перестанут работать — классы заменить на токены при вёрстке (только `className`, логику и data-атрибуты не трогать).
## Typography
**Display/Body:** Golos Text (variable, wght 400–900), `--font-golos`.
**Mono:** Martian Mono (variable, wght 100–800, wdth 75–112.5), `--font-martian`.
```ts
// app/layout.tsx
import { Golos_Text, Martian_Mono } from 'next/font/google';
const golos = Golos_Text({ subsets: ['cyrillic', 'latin'], variable: '--font-golos', display: 'swap' });
const martian = Martian_Mono({ subsets: ['cyrillic', 'latin'], variable: '--font-martian', axes: ['wdth'], display: 'swap' });
// <html lang="ru" className={`${golos.variable} ${martian.variable}`}>
```
**Character:** плотный гротеск с тяжёлыми заголовками, как на настенном календаре; моно — как цифры датера и весов. Курсива нет вообще, serif нет вообще.
### Hierarchy (mobile 390 → desktop 1440)
| Роль | Шрифт | Размер 390 → 1440 | LH | Tracking | Вес | Где |
|---|---|---|---|---|---|---|
| display | Golos | 44 → 96px | 0.92 | −0.035em | 900 | только H1 первого экрана; `text-wrap: balance` |
| headline | Golos | 32 → 56px | 1.0 | −0.03em | 800 | H2 секций |
| title | Golos | 24 → 30px | 1.05 | −0.02em | 800 | название сорта, «Доставка»/«Самовывоз», H3 |
| lead | Golos | 18 → 22px | 1.45 | 0 | 400 | подзаголовок под H1/H2, ≤ 46ch |
| body | Golos | 17px | 1.55 | 0 | 400 (500 — выделение) | абзацы, ≤ 62ch |
| small | Golos | 14px | 1.45 | 0 | 500 | регион сорта, примечания, ошибки полей |
| label (UI) | Golos | 15px | 1.3 | 0 | 600 | подписи полей, кнопки (кнопки 16px/700) |
| tag | Martian | 12px | 1.3 | 0.08em | 500, wdth 100, CAPS | подписи полей этикетки, ноты, «обжарка»/«доставка» |
| price | Martian | 22 → 32px | 1.0 | −0.02em | 600, wdth 100, `tnum` | цены «32 BYN», вес «250 г» (вес — 16px/500) |
| date | Martian | 28 → 52px | 1.0 | −0.04em | 700, **wdth 87.5** | числа в ленте недели |
| day-letter | Golos | 14 → 16px | 1 | 0.04em | 800, CAPS | «ПН», «ВТ» в ленте |
| stamp | Martian | 14–22px (по размеру штампа) | 1 | 0.04em | 700, **wdth 112.5**, CAPS | текст штампа |
| hours | Martian | 15px | 1.4 | 0 | 500, wdth 100, `tnum` | часы самовывоза, телефон |
`wdth` задавать через `font-stretch: 87.5%` / `112.5%`. Если next/font не объявит диапазон `font-stretch`, использовать `font-variation-settings: "wdth" 87.5` (вес продолжает задаваться `font-weight`).
### Named Rules
**Правило моно-фактов.** Martian Mono — только для чисел, дат, веса, цен, часов, телефона и коротких CAPS-меток. Предложения и заголовки моно не набираются.
**Правило без сплошного капса.** CAPS — только tag, day-letter и stamp. Заголовки — в предложном регистре.
**Правило неразрывных.** Тексты из site.ts выводятся через `typo()` (`lib/typo.ts`): неразрывный пробел после коротких предлогов и союзов, между числом и единицей (5 кг, 250 г, 32 BYN), неразрывный дефис в двойных латинских фамилиях. Фразы, которые не должны рваться (ноты), — через `keep()`. Цена подписки: число и «BYN» моно, единица «в месяц» — Golos 16px `muted`.
## Layout
- **Mobile-first.** Брейкпоинты Tailwind по умолчанию: `md` 768 (две колонки), `lg` 1024 (12-колоночная сетка), `xl` 1280.
- **Сетка:** 390 — 4 колонки, гаттер 16px, поля 20px. ≥1024 — 12 колонок, гаттер 24px, поля 48px, контейнер `max-w-page` 1280px по центру (на 1440 поля ≈ 80px).
- **Ритм секций:** padding-y 48px (390) → 80px (1440), между соседними секциями 96 → 160px. Отступ над H2 больше, чем под ним: H2 → лид 16px, лид → контент 40px (390) / 64px (1440).
- **Разделение секций:** не фоном-плашкой, а линейкой 2px `ink` по ширине контейнера сверху секции + сменой поверхности только у тёмного блока и формы.
- **Шкала отступов:** 4, 8, 12, 16, 20, 24, 32, 48, 72, 128.
## Elevation & Depth
Плоско. `box-shadow` не используется нигде (токены теней обнулены). Глубину дают: смена поверхности (cream → kraft → paper → roast), линейки 1/2px, наложение штампа поверх фото и края колонки, лента-наклейка, срезанная краем этикетки (`overflow: hidden`).
**Правило печати.** Если хочется тени — нужна линейка или смена поверхности.
## Shapes
- Радиус 0 у всего: кнопки, поля, этикетки, фото, уведомления, фокус-обводка. `hair` 2px — запас для мелких SVG-квадратиков (маркер выбранного интереса), больше нигде.
- Линейки: `1px ink` — внутри этикеток и ленты недели; `1.5px` — рамки кнопок secondary и полей формы (`muted`, в фокусе/ошибке 2px); `2px ink` — верх секций, рамка штампа (внешняя 2px + внутренняя 1px, зазор 3px), рамка блока формы.
- Перфорация дрип-пакета: верхний край — SVG пунктир (dash 6, gap 4, 1px ink).
## Components
### Stamp
Резиновый оттиск даты. Inline SVG, чтобы фильтр краски жил внутри SVG, а анимировалась обёртка.
- **Props:** `lines: string[]` (1–3 строки, CAPS), `size: 'sm' | 'md' | 'lg'` (ширина 112 / 168 / 232px), `rotate` (−7…+5°, по умолчанию −6), `animate?: boolean`, `tone: 'cherry' | 'cherry-on-dark'`.
- **Вид:** двойная рамка (2px снаружи, 1px внутри, зазор 3px), цвет `cherry`, `opacity: 0.92`; текст Martian 700, wdth 112.5; строки разделены линейкой 1px. Смещение «мимо приводки»: внутренний текст сдвинут на `translate(1px, -1px)` относительно рамки.
- **Неровность краски:** один общий `<svg><defs><filter id="stamp-ink">` в layout: `feTurbulence type=fractalNoise baseFrequency=0.9 numOctaves=2 seed=4` → `feDisplacementMap scale=1.6` (дрожание края) + вторая `feTurbulence baseFrequency=0.06` → `feColorMatrix` в альфу → `feComposite operator=in` (пропуски краски ~10%). Фильтр статичен, не анимируется.
- **Экземпляры:** hero — `lg`, строки «ОБЖАРКА» / «ЧТ 22.10» (дата вычисляется) / «МИНСК»; этикетка — `sm`, «ЧТ 22.10»; успех формы — `md`, «ПРИНЯТО» и «ДОПИСАЛИ» для `clarified`.
- **Доступность:** SVG `aria-hidden`, рядом визуально скрытый текст «Ближайшая обжарка — четверг, 22 октября».
### Дата ближайшей обжарки
`lib/roastWeek.ts`: `getRoastWeek(now: Date)` в часовом поясе Europe/Minsk → `{ days: {letter, date, iso}[7] (пн–вс), roast: четверг (сегодня, если сегодня чт, иначе следующий), delivery: roast + 1 }`. Страница — серверный компонент с `export const revalidate = 3600`. Формат: `ЧТ 22.10`. Это следует из факта «обжарка по четвергам», ничего не добавляет.
### WeekStrip
- **390 (вертикально, «дни столбиком»):** 7 строк по 48px. Строка: слева колонка 64px — day-letter над числом date; справа линейка 1px `ink` по базовой линии, на ней tag-подпись. ЧТ: буква и число `cherry`, подпись «ОБЖАРКА · ПАРТИЯ 5 КГ». ПТ: `cherry`, «ДОСТАВКА ПО МИНСКУ». СБ, ВС и ПТ справа охвачены вертикальной скобкой 2px `cherry` с подписью «1–3 ДЕНЬ — ЗЕРНО У ВАС». Остальные дни `muted`, без подписи.
- **1440 (горизонтально):** 7 равных колонок на 12-колоночной сетке, высота ~200px. Сверху day-letter, под ним date (52px), снизу общая линейка 1px через всю ширину; подписи tag под линейкой в колонках ЧТ и ПТ. Под ПТ–ВС — горизонтальная скобка (2px, загнутые концы 8px) с подписью «1–3 ДЕНЬ ПОСЛЕ ОБЖАРКИ — ЗЕРНО У ВАС». Вертикальные разделители колонок — 1px `rule-soft`.
- **Разметка:** `<ol aria-label="Неделя обжарки">` из 7 `<li>`; у ЧТ и ПТ подпись — текст в `<li>`, не только цвет. Числа в `<time dateTime>`.
### Label (карточка сорта)
```
┌─────────────────────────────────┐ ← kraft, 1px ink, overflow hidden
│ КВАРТА · МИНСК │ tag, muted
│ │
│ Эфиопия │ title 800
│ Иргачеффе │ small 500, muted
│─────────────────────────────────│ 1px ink
│ НОТЫ ЦВЕТОЧНЫЙ • БЕРГАМОТ │ tag muted / tag ink
│─────────────────────────────────│
│ ОБЖАРКА светлая │ spec из site.ts, если есть
│─────────────────────────────────│
│ ДАТА ОБЖАРКИ [ЧТ 22.10] │ поле + Stamp sm
│─────────────────────────────────│
│ 250 г 32 BYN │ PriceTag
│ [ Заказать эту пачку ] │ button-secondary, 100%
└─────────────────────────────────┘
```
- Padding 20px (390) / 24px (1440). Поля этикетки — строки min-height 40px, подпись tag `muted` колонкой 96px, значение tag `ink`.
- Поле `spec` (site.ts) только из фактов: Эфиопия — «ОБЖАРКА — светлая», Бразилия — «ДЛЯ — эспрессо», дрип — «КАК — заваривается прямо в чашке», у Колумбии поля нет (в ТЗ нет — не выдумывать). Значение — Golos small 500.
- Строка «ДАТА ОБЖАРКИ»: min-height 72px, `align-items: center`, padding-y 12px; под ней сразу линейка цены (без зазора). Штамп `lg`-строкой шириной 136px; повороты −4°, 3°, −2°, 5° и сдвиги по X 0 / 12 / 4 / 20px (Эфиопия, Колумбия, Бразилия, дрип).
- Лент-наклеек нет (сняты в итерации критика 1).
- На ≥768 карточки сортов делят строки сетки через `grid-template-rows: subgrid` (шапка / поля / цена): линейка под названием на одной высоте у всех; строка «дата обжарки» растягивается, если полей меньше.
- Значения полей — line-height 1.3; «•» между нотами склеен неразрывным пробелом со следующей нотой.
- Вес «250 г»: число и единица — отдельные span, отступ 0.25em через CSS вместо моноширинного пробела.
- Ноты склеены неразрывным пробелом внутри ноты («красное яблоко» не рвётся).
- **Дрип-пакеты:** вариант `Label variant="drip"`: на 1440 горизонтальный (вся ширина, высота ~180px; слева название «Дрип-пакеты» + «ассорти», в центре поля «10 ШТ», справа цена и кнопка), сверху перфорация вместо сплошной линии. На 390 — как обычная этикетка, но ниже (без поля нот). Лента та же.
- Никаких фото внутри этикеток и никаких вложенных карточек.
### PriceTag
`<p>`: вес слева (Martian 16px/500), цена справа (price, 600, `tnum`), «BYN» тем же шрифтом на 60% размера, базовая линия общая. Подписка: «54 BYN» + «/ мес» (16px, `muted`). Сверху линейка 1px `ink`. Цена — всегда текст, не картинка.
### Кнопки
Тач-цель ≥ 52px высотой, на 390 основные CTA на всю ширину. Шрифт Golos 16px/700, без CAPS. Переходы только `background-color`, `color`, `transform`.
| Состояние | primary (`cherry`) | secondary (рамка 1.5px `ink`) | on-dark secondary |
|---|---|---|---|
| idle | bg cherry, текст paper | bg transparent, текст ink | рамка и текст cream |
| hover (`@media (hover:hover)`) | bg cherry-deep, 120ms ease-out | bg ink, текст cream, 120ms | bg cream, текст roast |
| focus-visible | outline 2px ink, offset 3px (на тёмном — cream) | то же | outline cream |
| active | bg cherry-deep, `translateY(1px)`, 60ms | `translateY(1px)` | `translateY(1px)` |
| disabled (не отправка) | bg rule-soft, текст ink (8.51:1), `cursor: not-allowed`, без hover | рамка rule-soft, текст muted | — |
| submitting (`aria-busy="true"`) | bg cherry остаётся (не «серая»), текст «Отправляем…» из FORM_TEXT, справа три квадратика 4px paper, мигают по очереди (opacity 0.3→1, шаг 160ms), `cursor: progress`; ширина кнопки фиксирована `min-width` по idle, чтобы не прыгала | — | — |
| success | кнопка исчезает вместе с формой (так в LeadForm); фокус уходит на «Отправить ещё одну» (secondary) | | |
| error | кнопка возвращается в idle; над ней блок уведомления (см. ниже) | | |
Стили submitting вешать на `[aria-busy="true"]`, disabled-вид — на `:disabled:not([aria-busy])`. Иконка-стрелка в secondary этикетки — SVG 16px, stroke 1.5.
### Поля формы (`LeadForm`, data-атрибуты уже есть)
- **Блок формы:** `paper`, рамка 2px `ink`, padding 20px (390) / 40px (1440). Шапка блока — строка «ЗАЯВКА» tag слева и Stamp-место справа (пусто до успеха).
- **Label:** Golos 15px/600 ink, отступ до поля 8px. Между полями 20px.
- **Input/textarea:** высота 52px (textarea min 112px), bg `paper`, рамка 1.5px `muted`, радиус 0, padding 0 14px, Golos 17px (≥16px — iOS не зумит). Плейсхолдер `placeholder`.
- **focus-visible / focus:** рамка 2px `ink` (padding компенсировать −0.5px), outline 2px `ink` offset 2px.
- **`[data-invalid]`:** рамка 2px `cherry`; текст ошибки `[data-field-error]` — small 500 `cherry-deep`, отступ 6px, слева SVG «!» в квадрате 14px `cherry`. Ошибка не только цветом: есть текст и `aria-describedby` (уже в коде).
- **Интерес (`fieldset[role=radiogroup]`):** три «талона» — `label` min-height 48px, рамка 1.5px `muted`, padding 0 16px; на 390 столбиком на всю ширину, ≥768 в ряд. Нативный radio визуально скрыт (`sr-only`, не `display:none`). `[data-checked]`: bg `ink`, текст `cream`, слева кружок радиокнопки 12px: в idle рамка 1.5px `muted`, в checked — кольцо 1.5px `cream` с точкой `cherry` 6px. Выбор одиночный, поэтому кружок, не квадрат. Фокус: `label:has(:focus-visible)` outline 2px ink offset 2px. `fieldset[data-invalid]` — у всех талонов рамка `cherry`.
- **Honeypot:** не трогать.
### Уведомления (`[data-lead-notice]`, по `data-notice` на обёртке)
- **created / clarified (`data-state="success"`):** блок paper, рамка 2px `ink`, padding 24px; Stamp `md` «ПРИНЯТО» (или «ДОПИСАЛИ» для clarified), поворот −5°, анимация оттиска; рядом текст title 800 из FORM_TEXT; галочка SVG `ok` 20px. Ниже — кнопка secondary «Отправить ещё одну».
- **failed / rate_limited (`data-state="error"`):** блок paper, линейка 2px `cherry` сверху (не слева), tag `cherry-deep` «НЕ ОТПРАВЛЕНО», текст body, телефон — Martian 18px/600, ссылка `tel:`, тач-цель 44px.
- `aria-live="polite"` уже стоит; блок не прыгает: при появлении — только opacity 0→1, 160ms.
### StickyCta (только < 768px)
- Фиксирован снизу, `padding: 8px 20px calc(8px + env(safe-area-inset-bottom))`, bg `cream`, линейка 1px `ink` сверху, без тени. Внутри слева подпись `site.stickyCta.note` («Обжарка в четверг, доставка в пятницу», small 500, `muted`, 2 строки максимум), справа CtaButton primary 52px «Заказать кофе» (`site.stickyCta.label`).
- `interest: 'one_time'`, без comment, `source="sticky"`.
- Показывается, когда главная CTA первого экрана ушла из вьюпорта; скрывается, когда в вьюпорте `#lead`, или фокус внутри формы, или открыта клавиатура (`visualViewport.height < innerHeight * 0.75`). Один IntersectionObserver на два якоря. В скрытом состоянии `aria-hidden="true"` и `inert`.
- Подвалу на мобиле — `padding-bottom: 88px`, чтобы бар не закрывал контакты.
### Шапка
Не липкая. Высота 64px. Слева логотип: SVG-знак (7 вертикальных штрихов 3×20px, шаг 3px, 4-й `cherry`, остальные `ink`) + «Кварта» Golos 900 22px. Справа на 390 — ссылка-иконка телефона 44×44 (`aria-label="Позвонить: +375 29 000-00-00"`); на ≥1024 — якоря «Сорта», «Подписка», «Доставка» (Golos 15px/600, подчёркивание при hover) и телефон Martian 15px.
## Секции
Тексты — из `content/site.ts` (пишет копирайтер), CTA-подписи — тоже из site.ts. Ниже — композиция и факты, которые обязаны попасть в блок.
### 1. Первый экран `#hero`
- **Факты:** обжарка по четвергам, развоз по пятницам, Минск. Дата ближайшей обжарки — в штампе.
- **390:** шапка 64 → H1 display (≤ 3 строк, ≤ 40 знаков) → lead (≤ 3 строк) → 24px → primary CTA на всю ширину → 12px → secondary CTA на всю ширину → 56px → фото зерна во всю ширину экрана (без полей), 390×300, Stamp на бумажной подложке (`backing`, иначе красный на тёмном зерне нечитаем) наезжает на верхний левый край фото (top −56px, left 20px, rotate −6°). Первый вьюпорт (≈ 750px) вмещает H1, lead, CTA и верх штампа.
- **1440:** 12 колонок. Кол. 1–7: H1 96px, lead (≤ 46ch), ряд CTA (primary + ссылка). Кол. 8–12: фото зерна 4:5 (≈ 500×625), выровнено по верху H1, обрезано правым полем контейнера. Stamp `lg` лежит на левом нижнем углу фото, заходя на 64px в кол. 7. Высота секции ≈ 760px.
- **CTA (по site.ts):** primary — CtaButton `site.hero.ctaPrimary` (`subscription`), secondary — CtaButton `site.hero.ctaSecondary` (`one_time`, без comment). `site.hero.eyebrow` не выводится: надзаголовки запрещены (см. Don't).
- **Фото:** GDmiFs9lfrQ (Mona Jain, зерно на весь кадр). `next/image`, `priority`, `fetchPriority="high"`, `sizes="(min-width: 1024px) 500px, 100vw"`, `object-fit: cover; object-position: center`. Alt: «Обжаренное кофейное зерно крупным планом».
### 2. Почему свежесть `#fresh`
- **Факты:** обжарка в четверг партиями по 5 кг; развоз по Минску в пятницу; зерно у вас на 1–3 день после обжарки.
- **390:** H2 → lead → WeekStrip вертикальный.
- **1440:** H2 и lead в кол. 1–6, WeekStrip горизонтальный на все 12 колонок ниже.
- **Анимация:** WeekStrip (см. «Моушн»). CTA нет. Фото нет. SVG: скобка, линейки.
### 3. Ассортимент `#coffee`
- **Факты:** три сорта и дрип-пакеты — названия, ноты, вес, цены точно по ТЗ; под сеткой строка small «Доставка по Минску от 50 BYN — бесплатно» со ссылкой на `#delivery`.
- **390:** H2 → 3 Label столбиком (gap 16px) → Label drip.
- **768:** 2 колонки, drip на всю ширину.
- **1440:** 3 Label по 4 колонки, ниже drip горизонтальный на 12 колонок.
- **CTA (secondary в каждой этикетке, `source="coffee"`):**
- Эфиопия: `one_time`, comment «Эфиопия Иргачеффе, 250 г»
- Колумбия: `one_time`, «Колумбия Уила, 250 г»
- Бразилия: `one_time`, «Бразилия Серрадо, 250 г»
- Дрип: `one_time`, «Дрип-пакеты ассорти, 10 шт»
- Фото нет. SVG: перфорация, стрелка, штампы (статичные, повороты и сдвиги — см. Label).
- Дрип на ≥1024: три зоны (название / поля / цена и кнопка) по центру по вертикали, между зонами вертикальная линейка 1px `ink`, верхних линеек у первой строки полей и у цены нет.
### 4. Подписка `#subscription`
- **Факты:** 2 пачки по 250 г раз в месяц, доставка бесплатно, 54 BYN/мес.
- **Вид:** одна большая этикетка-абонемент на `kraft`, без ленты. Внутри поля: «СОСТАВ — 2 ПАЧКИ × 250 Г», «КАК ЧАСТО — РАЗ В МЕСЯЦ», «ДОСТАВКА — БЕСПЛАТНО»; справа/снизу PriceTag крупно: «54 BYN / мес» (price ×1.5 = 48px на 1440). SVG: два силуэта пачки (прямоугольник с клапаном, 1.5px ink, без заливки), во второй пачке маленький знак «7 штрихов».
- **390:** H2 → lead → этикетка (поля столбиком, цена внизу) → primary CTA во всю ширину.
- **1440:** H2 + lead в кол. 1–5; этикетка в кол. 6–12: слева поля, справа цена и primary CTA.
- Цена без своей линейки: поля подписки замкнуты линейкой снизу, цена и CTA выровнены по низу полей; примечание — строкой под всей этикеткой.
- **CTA:** CtaButton `site.subscription.cta` (`subscription`, comment «Подписка: 2 пачки в месяц»).
### 5. Кто мы `#about`
- **Факты:** с 2019 года, основатели Аня и Дмитрий, бывшие бариста, партии по 5 кг. Ничего больше (ни цитат, ни наград).
- **390:** фото на всю ширину 390×260 → H2 → body → одна tag-строка `site.about.factsLine` (Martian 12px `muted`, CAPS) между линейками 1px `ink`, padding-y 12px. Ряд крупных цифр не делаем (hero-метрики).
- **1440:** фото кол. 1–6 (3:2, ≈ 610×407), текст кол. 8–12, выровнен по низу фото.
- **Фото:** _7CVm353m7A (Scott Soltys-Curry, охладитель ростера). `sizes="(min-width: 1024px) 610px, 100vw"`, `object-position: center 40%`, lazy. Alt: «Свежеобжаренное зерно остывает в охладителе ростера». Подпись/текст не утверждает, что это цех «Кварты».
- CTA нет.
### 6. Доставка и самовывоз `#delivery` (тёмная, класс `on-dark`)
- **Фон:** `roast`, текст `cream`. Факты: доставка по Минску в пятницу, от 50 BYN бесплатно, дешевле — стоимость уточнит менеджер; самовывоз ул. Короля, 9; пн–пт 8:00–20:00, сб–вс 10:00–18:00.
- **<768:** фото мешка скрыто (рядом уже охладитель), секция начинается с H2, padding-top 48px. **768–1023:** фото полосой сверху. **≥1024:** фото абсолютно слева на 42% ширины экрана, контент в колонках 7–12 контейнера 1280. **390:** H2 → блок «Доставка» (title; поля этикетки: подпись `muted-on-dark`, значение `cream`, линейки 1px `muted-on-dark`) → блок «Самовывоз»: адрес title, часы таблицей Martian (`<dl>`: «ПН–ПТ 8:00–20:00», «СБ–ВС 10:00–18:00»), ссылка «Открыть в Яндекс Картах» (`https://yandex.by/maps/?text=Минск, ул. Короля, 9`, `target=_blank rel=noopener`) → secondary on-dark CTA.
- **1440:** фото кол. 1–5 на всю высоту секции (bleed влево до края экрана), справа кол. 7–12: H2, два блока рядом (по 3 колонки), CTA снизу.
- Строки доставки — поля с tag-подписью `muted-on-dark` (`site.delivery.delivery.labels`); акцент `cherry-on-dark` только на слове `accent` («бесплатно»).
- Под сеткой ассортимента — строка small `site.catalog.deliveryNote` ссылкой на `#delivery`.
- **Фото:** obV_LM0KjxY (Tina Guina, мешок на тёмном). `sizes="(min-width: 1024px) 40vw, 100vw"`, `object-position: center`, lazy. Alt: «Открытый мешок с кофейным зерном».
- **CTA:** CtaButton secondary on-dark, `interest: 'question'`, без comment (подпись `site.delivery.cta`). Акценты (ПТ, «бесплатно») — `cherry-on-dark`.
- Карты-iframe нет (производительность).
### 7. Форма `#lead`
- Существующий `LeadForm` в блоке paper (см. «Поля формы»). `scroll-mt-4` уже есть.
- **390:** H2 → lead → «Или позвоните:» отдельной строкой small `muted` → номер Martian 22px/600 ссылкой `tel:`, `nowrap`, высота 44px → блок формы на всю ширину.
- **1440:** кол. 1–5: H2, lead, телефон Martian 22px, часы работы tag; кол. 6–12: блок формы.
- Комментарий, пришедший из CTA, виден в textarea — посетитель его видит и может стереть.
### 8. Подвал (`roast`, `on-dark`)
Логотип (знак + «Кварта» cream), адрес, часы, телефон; строка small `muted-on-dark`: «Фото: Mona Jain, Tina Guina, Scott Soltys-Curry — Unsplash». Без соцсетей (их нет в ТЗ). На 390 — колонкой, на 1440 — 4 колонки по 3.
### Фото: общая техника
Скачать три фото в `apps/landing/public/photos/` (`beans.jpg`, `sack.jpg`, `cooler.jpg`, ширина 2000px, JPEG q≈78) — без `remotePatterns`. Фото без фильтров и цветокора «под бежевый»; допустим только кроп.
## Моушн
Только CSS (`@keyframes`, `transition`) + один общий хук `useInView` на IntersectionObserver (`threshold: 0.35`, `once`). Анимируются только `transform` и `opacity` (у скобки — `transform: scaleX/scaleY`). Bounce, overshoot, spring — нет.
**Видимое по умолчанию.** Без JS всё в финальном состоянии. Инлайн-скрипт в `<head>` ставит `document.documentElement.dataset.motion = ''` только если `!matchMedia('(prefers-reduced-motion: reduce)').matches && 'IntersectionObserver' in window`. Начальные состояния анимаций пишутся под `[data-motion] .x:not([data-inview])`.
| Движение | Триггер | Длительность / задержка | Easing | Что | Reduced motion |
|---|---|---|---|---|---|
| Оттиск штампа (hero) | загрузка, CSS-анимация, без IO | 380ms, delay 250ms | `--ease-out-expo` | opacity 0→0.92, scale 1.18→1, rotate (r−3°)→r | статичен с начала |
| Оттиск штампа (успех формы) | `data-state="success"` | 380ms, delay 80ms | `--ease-out-expo` | то же | статичен |
| Штампы на этикетках | — | — | — | не анимируются | — |
| Лента недели | IO 0.35 | линейка scaleX 0→1 600ms (origin left); ЧТ: цвет-подложка opacity 0→1 + подпись translateY(6px)→0, opacity, 320ms, delay 300ms; ПТ — то же, delay 480ms; скобка scaleX (на 390 scaleY) 0→1 500ms, delay 680ms, подпись скобки opacity 240ms, delay 1000ms | `--ease-out-quart` | красный текст ЧТ/ПТ — отдельный слой поверх серого, меняется opacity (не `color`) | всё в финале сразу |
| Кнопка hover | pointer | 120ms | ease-out | background-color | без изменений (это не движение) |
| Кнопка active | press | 60ms | linear | translateY(1px) | убрать translate |
| Submitting-точки | `aria-busy` | цикл 480ms, шаг 160ms | steps(1) | opacity 0.3↔1 | точки статичны (все 1), текст «Отправляем…» остаётся |
| Уведомление | появление | 160ms | ease-out | opacity | мгновенно |
| StickyCta | IO по якорям | показ 240ms `--ease-out-quart`, скрытие 180ms `--ease-in-quart` | | translateY(100%)↔0 | только мгновенное появление/скрытие |
| Скролл к форме | CTA | нативный smooth (уже в prefillLead) | | | `auto` (уже в коде) |
`will-change` не ставить заранее. Никаких parallax, scroll-linked, marquee, анимаций на каждой секции.
## Do's and Don'ts
### Do:
- **Do** показывать свежесть датой: штамп «ЧТ 22.10», лента недели.
- **Do** держать все цены и условия из ТЗ текстом, видимыми без клика.
- **Do** использовать линейки 1/2px `ink` для структуры вместо плашек.
- **Do** давать Martian Mono только числам и коротким меткам.
- **Do** разные повороты штампов (−7…+5°), смещение текста относительно рамки.
- **Do** проверять на 390 и 1440 с реальными текстами из site.ts.
### Don't (impeccable + «бежевый AI»):
- **Don't** мягкие бежевые градиенты, шум/зерно-оверлеи, «тёплую» дымку на фото.
- **Don't** скруглённые карточки с тенями; `box-shadow` любой, включая жёсткую смещённую.
- **Don't** generic hero с чашкой/латте-артом, руками, людьми, паром.
- **Don't** фиолетовые, градиентные, неоновые акценты; второй акцентный цвет; градиентный текст.
- **Don't** курсив и serif — ни в заголовках, ни в цитатах.
- **Don't** надзаголовки-«кикеры» над H2 и номера секций «01/02/03».
- **Don't** сетку «иконка + заголовок + текст» в одинаковых карточках, hero-метрики «5 кг / 2019 / 1–3 дня» крупными цифрами.
- **Don't** вложенные карточки (этикетка внутри этикетки).
- **Don't** сплошной CAPS и тотальный моно (берём из FreshColor/Altura только поле даты и ноты).
- **Don't** пёструю палитру Altura, белый фон и сетку месяца календаря Rationale.
- **Don't** `border-left` цветной на уведомлениях; эмодзи и юникод-глифы вместо иконок (иконки — свой SVG, stroke 1.5).
- **Don't** выдумывать отзывы, рейтинги, «лучший кофе в Минске», соцсети, адреса, цены доставки ниже 50 BYN.
- **Don't** тяжёлые библиотеки анимаций (framer-motion, GSAP, Lottie), карты-iframe, видео.
## Доступность
- Порядок фокуса = визуальный порядок: шапка → hero CTA → … → форма → подвал. StickyCta в DOM последней; скрытая — `inert`.
- Фокус: `:focus-visible` outline 2px `ink` offset 3px; на тёмном — `cream`; у талонов интереса — на `label:has(:focus-visible)`. Никогда `outline: none` без замены.
- Тач-цели ≥ 44×44 (кнопки 52px, талоны 48px, ссылки-якоря с padding до 44px, телефон в шапке 44×44).
- Ошибки — текстом + `aria-invalid` + `aria-describedby` (уже в коде), не только цветом. ЧТ/ПТ в ленте — подписью, не только цветом.
- `lang="ru"`. Один H1. H2 у каждой секции, у секций `aria-labelledby`.
- Alt фото — см. секции; декоративные SVG `aria-hidden`.
- Поля 17px (iOS без зума). `inputmode`/`autocomplete` уже стоят.
- Контраст — таблица в «Colors». Масштаб текста 200% без горизонтального скролла на 390.
## OG-картинка 1200×630 (`app/opengraph-image.tsx`, next/og)
```
┌────────────────────────────────────────────────────────────┐ cream
│ ▮▮▮▮▮▮▮ Кварта ┌──────────────┐ │ знак + Golos 900 64px
│ │ ОБЖАРКА ПО │ │ Stamp cherry,
│ Свежая обжарка │ ЧЕТВЕРГАМ │ │ rotate −6°, 300px
│ в Минске └──────────────┘ │ Golos 900 88px, ink, lh .92
│────────────────────────────────────────────────────────────│ 2px ink, y=380
│ ПН ВТ СР [ЧТ] [ПТ] СБ ВС │ day-letter 26px; ЧТ/ПТ cherry
│ ОБЖАРКА ДОСТАВКА │ Martian 20px
│ └──── 1–3 день — зерно у вас ──┘ │ скобка 2px cherry
│ kvarta.unuchak.com +375 29 000-00-00 │ Martian 20px muted
└────────────────────────────────────────────────────────────┘
```
- Тексты из site.ts: заголовок — `site.hero.title`, штамп — `site.hero.stamp`, подписи ленты — `site.freshness.days`, `alt` — `site.meta.ogTitle` (скетч выше — композиция, не текст).
- Поля 64px. Без фото и без дат (картинка кешируется), только дни недели.
- Satori не поддерживает SVG-фильтры и variable-шрифты: штамп — двойная рамка без «краски», шрифты — статичные TTF (Golos Text 900 и 500, Martian Mono 700), загружаются `fetch` из файлов в `src/app/og-fonts/` (подмножество кириллица+латиница). `alt`: «Кварта — свежая обжарка в Минске, обжарка по четвергам».