diff --git a/DESIGN.md b/DESIGN.md index 00d36ff..00cd4b6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -409,8 +409,8 @@ const martian = Martian_Mono({ subsets: ['cyrillic', 'latin'], variable: '--font - `aria-live="polite"` уже стоит; блок не прыгает: при появлении — только opacity 0→1, 160ms. ### StickyCta (только < 768px) -- Фиксирован снизу, `padding: 8px 20px calc(8px + env(safe-area-inset-bottom))`, bg `cream`, линейка 1px `ink` сверху, без тени. Внутри CtaButton primary на всю ширину, 52px: «Оформить подписку» + справа Martian «54 BYN/мес». -- `interest: 'subscription'`, без comment, `source="sticky"`. +- Фиксирован снизу, `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`, чтобы бар не закрывал контакты. @@ -423,9 +423,9 @@ const martian = Martian_Mono({ subsets: ['cyrillic', 'latin'], variable: '--font ### 1. Первый экран `#hero` - **Факты:** обжарка по четвергам, развоз по пятницам, Минск. Дата ближайшей обжарки — в штампе. -- **390:** шапка 64 → H1 display (≤ 3 строк, ≤ 40 знаков) → lead (≤ 3 строк) → 24px → primary CTA на всю ширину → 12px → текстовая ссылка «Смотреть сорта» (якорь `#coffee`, 44px высота) → 32px → фото зерна во всю ширину экрана (без полей), 390×300, Stamp `lg` наезжает на верхний левый край фото (top −56px, left 20px, rotate −6°). Первый вьюпорт (≈ 750px) вмещает H1, lead, CTA и верх штампа. +- **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:** primary — CtaButton `interest: 'subscription'` (подпись CTAS.subscription). Ссылка «Смотреть сорта» — обычный ``. +- **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` @@ -451,7 +451,7 @@ const martian = Martian_Mono({ subsets: ['cyrillic', 'latin'], variable: '--font - **Вид:** одна большая этикетка-абонемент на `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:** CtaButton `subscription`, без comment. +- **CTA:** CtaButton `site.subscription.cta` (`subscription`, comment «Подписка: 2 пачки в месяц»). ### 5. Кто мы `#about` - **Факты:** с 2019 года, основатели Аня и Дмитрий, бывшие бариста, партии по 5 кг. Ничего больше (ни цитат, ни наград). diff --git a/apps/landing/public/photos/beans.jpg b/apps/landing/public/photos/beans.jpg new file mode 100644 index 0000000..7b5cd09 Binary files /dev/null and b/apps/landing/public/photos/beans.jpg differ diff --git a/apps/landing/public/photos/cooler.jpg b/apps/landing/public/photos/cooler.jpg new file mode 100644 index 0000000..41c7aac Binary files /dev/null and b/apps/landing/public/photos/cooler.jpg differ diff --git a/apps/landing/public/photos/sack.jpg b/apps/landing/public/photos/sack.jpg new file mode 100644 index 0000000..d5fb391 Binary files /dev/null and b/apps/landing/public/photos/sack.jpg differ diff --git a/apps/landing/src/app/globals.css b/apps/landing/src/app/globals.css index d4b5078..064ce11 100644 --- a/apps/landing/src/app/globals.css +++ b/apps/landing/src/app/globals.css @@ -1 +1,485 @@ @import 'tailwindcss'; + +/* Токены — DESIGN.md. Палитра Tailwind отключена: только цвета «Кварты». */ +@theme { + --color-*: initial; + --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-ui: 0.9375rem; + --text-ui--line-height: 1.3; + --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-price--letter-spacing: -0.02em; + --text-date: clamp(1.75rem, 1.2rem + 2.2vw, 3.25rem); + --text-date--line-height: 1; + --text-date--letter-spacing: -0.04em; + + --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; + -webkit-text-size-adjust: 100%; + } + body { + font-family: var(--font-sans); + font-size: var(--text-body); + line-height: 1.55; + background: var(--color-cream); + -webkit-tap-highlight-color: transparent; + font-synthesis: none; + } + ::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; + } + h1, + h2, + h3 { + text-wrap: balance; + } + p { + text-wrap: pretty; + } +} + +@layer components { + /* моно-шрифт: ширина через wdth-ось Martian Mono */ + .mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + } + .mono-narrow { + font-family: var(--font-mono); + font-stretch: 87.5%; + font-variation-settings: 'wdth' 87.5; + } + .tag { + font-family: var(--font-mono); + font-size: var(--text-tag); + line-height: 1.3; + letter-spacing: 0.08em; + font-weight: 500; + text-transform: uppercase; + } + + .page-x { + padding-inline: 20px; + } + @media (min-width: 1024px) { + .page-x { + padding-inline: 48px; + } + } + .container-page { + margin-inline: auto; + max-width: var(--container-page); + } + .section-y { + padding-block: 72px; + } + @media (min-width: 1024px) { + .section-y { + padding-block: 128px; + } + } + + /* Кнопки */ + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + min-height: 52px; + padding: 0 24px; + font-family: var(--font-sans); + font-size: 1rem; + font-weight: 700; + line-height: 1.2; + text-align: center; + border-radius: 0; + cursor: pointer; + user-select: none; + transition: + background-color 120ms ease-out, + color 120ms ease-out, + border-color 120ms ease-out, + transform 60ms linear; + } + .btn:active:not(:disabled) { + transform: translateY(1px); + } + .btn-primary { + background: var(--color-cherry); + color: var(--color-paper); + } + .btn-primary:active:not(:disabled) { + background: var(--color-cherry-deep); + } + .btn-secondary { + background: transparent; + color: var(--color-ink); + box-shadow: inset 0 0 0 1.5px var(--color-ink); + padding: 0 20px; + } + .btn-on-dark { + background: transparent; + color: var(--color-cream); + box-shadow: inset 0 0 0 1.5px var(--color-cream); + padding: 0 20px; + } + @media (hover: hover) { + .btn-primary:hover:not(:disabled) { + background: var(--color-cherry-deep); + } + .btn-secondary:hover:not(:disabled) { + background: var(--color-ink); + color: var(--color-cream); + } + .btn-on-dark:hover:not(:disabled) { + background: var(--color-cream); + color: var(--color-roast); + } + } + .btn:disabled:not([aria-busy]) { + background: var(--color-rule-soft); + color: var(--color-ink); + box-shadow: none; + cursor: not-allowed; + } + .btn[aria-busy='true'] { + cursor: progress; + } + .busy-dots { + display: inline-flex; + gap: 4px; + } + .busy-dots i { + width: 4px; + height: 4px; + background: currentColor; + } + + /* Поля формы */ + .field-label { + font-size: var(--text-ui); + font-weight: 600; + line-height: 1.3; + } + .field { + display: block; + width: 100%; + min-height: 52px; + padding: 12px 14px; + background: var(--color-paper); + color: var(--color-ink); + font-size: 1.0625rem; + line-height: 1.45; + border: 1.5px solid var(--color-muted); + border-radius: 0; + transition: border-color 120ms ease-out; + } + .field::placeholder { + color: var(--color-placeholder); + opacity: 1; + } + .field:focus { + border-color: var(--color-ink); + box-shadow: inset 0 0 0 0.5px var(--color-ink); + } + .field:focus-visible { + outline-offset: 2px; + } + .field[data-invalid] { + border-color: var(--color-cherry); + box-shadow: inset 0 0 0 0.5px var(--color-cherry); + } + textarea.field { + min-height: 112px; + resize: vertical; + } + .field-error { + display: flex; + gap: 6px; + align-items: flex-start; + font-size: var(--text-small); + font-weight: 500; + line-height: 1.45; + color: var(--color-cherry-deep); + } + .talon { + position: relative; + display: flex; + align-items: center; + gap: 10px; + min-height: 48px; + padding: 0 16px; + font-size: var(--text-ui); + font-weight: 600; + box-shadow: inset 0 0 0 1.5px var(--color-muted); + cursor: pointer; + transition: + background-color 120ms ease-out, + color 120ms ease-out; + } + .talon::before { + content: ''; + width: 10px; + height: 10px; + flex: none; + box-shadow: inset 0 0 0 1.5px currentColor; + } + .talon[data-checked] { + background: var(--color-ink); + color: var(--color-cream); + box-shadow: inset 0 0 0 1.5px var(--color-ink); + } + .talon[data-checked]::before { + background: var(--color-cherry); + box-shadow: none; + } + .talon:has(:focus-visible) { + outline: 2px solid var(--color-ink); + outline-offset: 2px; + } + [data-invalid] .talon:not([data-checked]) { + box-shadow: inset 0 0 0 2px var(--color-cherry); + } + @media (hover: hover) { + .talon:not([data-checked]):hover { + background: var(--color-cream); + } + } + + /* Этикетка: лента-наклейка, срезанная краем */ + .ribbon { + position: absolute; + top: 34px; + right: -62px; + width: 220px; + padding: 6px 0; + background: var(--color-cherry); + color: var(--color-paper); + text-align: center; + transform: rotate(45deg); + pointer-events: none; + } + .perforation { + background-image: linear-gradient(to right, var(--color-ink) 0 6px, transparent 6px 10px); + background-size: 10px 1px; + background-repeat: repeat-x; + height: 1px; + } +} + +/* Штамп */ +@layer components { + .stamp { + display: inline-block; + transform: rotate(var(--stamp-rotate, -6deg)); + opacity: 0.92; + } + .stamp-text { + font-family: var(--font-mono); + font-weight: 700; + font-stretch: 112.5%; + font-variation-settings: 'wdth' 112.5; + letter-spacing: 0.04em; + } +} + +/* Лента недели: 390 — столбиком, ≥1024 — строкой */ +@layer components { + .week-days { + display: grid; + grid-template-columns: minmax(0, 1fr) 128px; + column-gap: 12px; + } + .week-day { + grid-column: 1; + display: grid; + grid-template-columns: 64px minmax(0, 1fr); + column-gap: 12px; + align-items: center; + min-height: 60px; + padding-block: 10px; + position: relative; + } + .week-head { + display: flex; + flex-direction: column; + gap: 2px; + } + .week-letter { + font-size: 0.875rem; + font-weight: 800; + letter-spacing: 0.04em; + line-height: 1; + } + .week-date { + font-size: var(--text-date); + font-weight: 700; + line-height: 1; + letter-spacing: -0.04em; + } + .week-rule { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1px; + background: var(--color-ink); + transform-origin: left; + } + .week-mark { + position: absolute; + left: 0; + width: 64px; + bottom: -1px; + height: 4px; + background: var(--color-cherry); + transform-origin: left; + } + .week-note { + display: flex; + flex-direction: column; + gap: 2px; + } + .week-bracket { + grid-column: 2; + grid-row: 5 / 8; + display: flex; + gap: 10px; + padding-block: 12px; + } + .week-bracket-line { + flex: none; + width: 8px; + border: 2px solid var(--color-cherry); + border-left: 0; + transform-origin: top; + } + .week-bracket-note { + display: flex; + flex-direction: column; + gap: 4px; + align-self: center; + } + + @media (min-width: 1024px) { + .week-days { + grid-template-columns: repeat(7, minmax(0, 1fr)); + column-gap: 0; + } + .week-day { + grid-column: auto; + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0; + min-height: 0; + } + .week-head { + position: relative; + gap: 10px; + padding: 0 16px 20px; + border-left: 1px solid var(--color-rule-soft); + } + .week-day:first-child .week-head { + border-left: 0; + padding-left: 0; + } + .week-letter { + font-size: 1rem; + } + .week-mark { + left: 16px; + right: 16px; + width: auto; + } + .week-note { + padding: 16px 16px 0; + } + .week-bracket { + grid-column: 5 / 8; + grid-row: 2; + flex-direction: column; + gap: 12px; + padding: 28px 16px 0; + } + .week-bracket-line { + width: auto; + height: 10px; + border: 2px solid var(--color-cherry); + border-top: 0; + transform-origin: left; + } + .week-bracket-note { + align-self: auto; + } + } +} diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index 78ea58b..bd95dcb 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -1,16 +1,27 @@ -import type { Metadata } from 'next'; +import type { Metadata, Viewport } from 'next'; +import { Golos_Text, Martian_Mono } from 'next/font/google'; +import { StampInkFilter } from '@/components/Stamp'; import { TouchRecorder } from '@/components/TouchRecorder'; +import { site } from '@/content/site'; import './globals.css'; +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' }); + export const metadata: Metadata = { - title: 'Кварта — свежая обжарка в Минске', - description: 'Обжариваем по четвергам, привозим по пятницам.', + title: site.meta.title, + description: site.meta.description, +}; + +export const viewport: Viewport = { + themeColor: '#f3ebdd', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + + {children} diff --git a/apps/landing/src/app/page.tsx b/apps/landing/src/app/page.tsx index de8d2d4..a9e5302 100644 --- a/apps/landing/src/app/page.tsx +++ b/apps/landing/src/app/page.tsx @@ -1,31 +1,33 @@ -// Заглушка страницы: проверяем форму и CTA. Секции и вёрстку сделает дизайнер. -import { CtaButton } from '@/components/CtaButton'; -import { LeadForm } from '@/components/LeadForm'; -import { CTAS, FORM_TEXT } from '@/content/form'; -import { LEAD_SECTION_ID } from '@/lib/leadPrefill'; +import { About } from '@/components/sections/About'; +import { Catalog } from '@/components/sections/Catalog'; +import { Delivery } from '@/components/sections/Delivery'; +import { Footer } from '@/components/sections/Footer'; +import { Freshness } from '@/components/sections/Freshness'; +import { Header } from '@/components/sections/Header'; +import { Hero } from '@/components/sections/Hero'; +import { LeadSection } from '@/components/sections/LeadSection'; +import { Subscription } from '@/components/sections/Subscription'; +import { formatShortDate, getRoastWeek } from '@/lib/roastWeek'; -const ctaClass = 'rounded border border-neutral-900 px-4 py-2'; +// дата ближайшей обжарки пересчитывается раз в час +export const revalidate = 3600; export default function Page() { + const week = getRoastWeek(); + const roastDate = formatShortDate(week.roast); return ( -
-
-

Кварта — свежая обжарка в Минске

-

Обжариваем по четвергам, привозим по пятницам.

-
- {Object.values(CTAS).map((cta) => ( - - {cta.label} - - ))} -
-
- -
-

{FORM_TEXT.title}

-

{FORM_TEXT.lead}

- -
-
+ <> +
+
+ + + + + + + +
+