Files

52 KiB
Raw Permalink Blame History

name, description, colors, typography, rounded, spacing, components
name description colors typography rounded spacing components
Кварта Лендинг минской обжарки — «недельный календарь и штамп даты»
cream paper kraft roast ink muted placeholder rule-soft cherry cherry-deep cherry-on-dark muted-on-dark ok
#F3EBDD #FBF7F0 #DCC7A1 #1E120C #2A1810 #5E4636 #7A6352 #C9B595 #B3221B #8F1A14 #E8604F #BFA88A #2E5E3A
display headline title lead body small mono-tag mono-price mono-date stamp
fontFamily fontSize fontWeight lineHeight letterSpacing
Golos Text, system-ui, sans-serif clamp(2.75rem, 1.6rem + 5.2vw, 6rem) 900 0.92 -0.035em
fontFamily fontSize fontWeight lineHeight letterSpacing
Golos Text, system-ui, sans-serif clamp(2rem, 1.4rem + 2.8vw, 3.5rem) 800 1 -0.03em
fontFamily fontSize fontWeight lineHeight letterSpacing
Golos Text, system-ui, sans-serif clamp(1.5rem, 1.3rem + 0.6vw, 1.875rem) 800 1.05 -0.02em
fontFamily fontSize fontWeight lineHeight
Golos Text, system-ui, sans-serif clamp(1.125rem, 1rem + 0.5vw, 1.375rem) 400 1.45
fontFamily fontSize fontWeight lineHeight
Golos Text, system-ui, sans-serif 1.0625rem 400 1.55
fontFamily fontSize fontWeight lineHeight
Golos Text, system-ui, sans-serif 0.875rem 500 1.45
fontFamily fontSize fontWeight lineHeight letterSpacing
Martian Mono, ui-monospace, monospace 0.75rem 500 1.3 0.08em
fontFamily fontSize fontWeight lineHeight letterSpacing fontFeature
Martian Mono, ui-monospace, monospace clamp(1.375rem, 1.2rem + 0.8vw, 2rem) 600 1 -0.02em "tnum" 1
fontFamily fontSize fontWeight lineHeight letterSpacing fontVariation
Martian Mono, ui-monospace, monospace clamp(1.75rem, 1.2rem + 2.2vw, 3.25rem) 700 1 -0.04em "wdth" 87.5
fontFamily fontSize fontWeight lineHeight letterSpacing fontVariation
Martian Mono, ui-monospace, monospace 1rem 700 1 0.04em "wdth" 112.5
none hair
0px 2px
1 2 3 4 5 6 8 12 18 32
4px 8px 12px 16px 20px 24px 32px 48px 72px 128px
button-primary button-primary-hover button-secondary button-secondary-hover input label-card
backgroundColor textColor rounded padding height
{colors.cherry} {colors.paper} {rounded.none} 0 24px 52px
backgroundColor textColor
{colors.cherry-deep} {colors.paper}
backgroundColor textColor rounded padding height
transparent {colors.ink} {rounded.none} 0 20px 52px
backgroundColor textColor
{colors.ink} {colors.cream}
backgroundColor textColor rounded padding height
{colors.paper} {colors.ink} {rounded.none} 0 14px 52px
backgroundColor textColor rounded padding
{colors.kraft} {colors.ink} {rounded.none} 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)

@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.

// 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: «Кварта — свежая обжарка в Минске, обжарка по четвергам».