feat(landing): штамп, лента недели, состояния кнопки, липкая CTA

Оттиск штампа при загрузке и при успехе формы, заполнение ленты
недели по IntersectionObserver, мигающие точки при отправке.
Только CSS и transform/opacity; анимации включает скрипт в head,
поэтому без JS и при prefers-reduced-motion всё сразу в финале.
Липкая CTA на мобиле: «Заказать кофе», прячется у формы, при фокусе
в ней и при открытой клавиатуре. Ритм секций плотнее.
This commit is contained in:
2026-10-17 16:56:52 +03:00
parent dc70ebccd8
commit d8d67c036f
5 changed files with 209 additions and 4 deletions
+105 -2
View File
@@ -140,11 +140,11 @@
max-width: var(--container-page);
}
.section-y {
padding-block: 72px;
padding-block: 48px;
}
@media (min-width: 1024px) {
.section-y {
padding-block: 128px;
padding-block: 80px;
}
}
@@ -483,3 +483,106 @@
}
}
}
/* Моушн (DESIGN.md → «Моушн»). Без JS и при reduced-motion — сразу финальное состояние. */
@keyframes stamp-in {
from {
opacity: 0;
transform: rotate(calc(var(--stamp-rotate, -6deg) - 3deg)) scale(1.18);
}
to {
opacity: 0.92;
transform: rotate(var(--stamp-rotate, -6deg)) scale(1);
}
}
@keyframes busy-dot {
0%,
100% {
opacity: 0.3;
}
33% {
opacity: 1;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
}
@media (prefers-reduced-motion: no-preference) {
[data-motion] .stamp[data-stamp='load'] {
animation: stamp-in 380ms var(--ease-out-expo) 250ms both;
}
.stamp[data-stamp='event'] {
animation: stamp-in 380ms var(--ease-out-expo) 80ms both;
}
.busy-dots i {
animation: busy-dot 480ms steps(1) infinite;
}
.busy-dots i:nth-child(2) {
animation-delay: 160ms;
}
.busy-dots i:nth-child(3) {
animation-delay: 320ms;
}
[data-lead-notice] > * {
animation: fade-in 160ms ease-out;
}
/* лента недели */
[data-motion] .week .week-rule {
transition: transform 600ms var(--ease-out-quart);
}
[data-motion] .week .week-mark {
transition: transform 320ms var(--ease-out-quart) 300ms;
}
[data-motion] .week .week-note {
transition:
opacity 320ms var(--ease-out-quart) 300ms,
transform 320ms var(--ease-out-quart) 300ms;
}
[data-motion] .week .week-day:nth-child(5) .week-mark,
[data-motion] .week .week-day:nth-child(5) .week-note {
transition-delay: 480ms;
}
[data-motion] .week .week-bracket-line {
transition: transform 500ms var(--ease-out-quart) 680ms;
}
[data-motion] .week .week-bracket-note {
transition: opacity 240ms ease-out 1000ms;
}
[data-motion] .week:not([data-inview]) .week-rule,
[data-motion] .week:not([data-inview]) .week-mark {
transform: scaleX(0);
}
[data-motion] .week:not([data-inview]) .week-note {
opacity: 0;
transform: translateY(6px);
}
[data-motion] .week:not([data-inview]) .week-bracket-line {
transform: scaleY(0);
}
[data-motion] .week:not([data-inview]) .week-bracket-note {
opacity: 0;
}
@media (min-width: 1024px) {
[data-motion] .week:not([data-inview]) .week-bracket-line {
transform: scaleX(0);
}
}
/* липкая CTA */
.sticky-cta {
transition: transform 180ms var(--ease-in-quart);
}
.sticky-cta[data-visible] {
transition: transform 240ms var(--ease-out-quart);
}
}
.sticky-cta:not([data-visible]) {
transform: translateY(100%);
visibility: hidden;
transition-property: transform, visibility;
}
+9 -1
View File
@@ -1,6 +1,8 @@
import type { Metadata, Viewport } from 'next';
import { Golos_Text, Martian_Mono } from 'next/font/google';
import { MOTION_BOOT, RevealObserver } from '@/components/RevealObserver';
import { StampInkFilter } from '@/components/Stamp';
import { StickyCta } from '@/components/StickyCta';
import { TouchRecorder } from '@/components/TouchRecorder';
import { site } from '@/content/site';
import './globals.css';
@@ -19,11 +21,17 @@ export const viewport: Viewport = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru" className={`${golos.variable} ${martian.variable}`}>
<html lang="ru" className={`${golos.variable} ${martian.variable}`} suppressHydrationWarning>
<head>
{/* до первой отрисовки: иначе блоки мигнут в начальном состоянии анимации */}
<script dangerouslySetInnerHTML={{ __html: MOTION_BOOT }} />
</head>
<body>
<StampInkFilter />
{children}
<StickyCta />
<TouchRecorder />
<RevealObserver />
</body>
</html>
);