лэндинга (см. b2c-tokens.ts — единственный источник правды
+ * по hex, сюда литералы не копируются). Прозрачность/тени — через
+ * `color-mix(in srgb, var(--b2c-*) N%, transparent)`, а не rgba(hex-в-цифрах):
+ * так шапка/sticky-бар остаются на токенах даже там, где макет просит
+ * полупрозрачный фон.
+ *
+ * Брейкпоинты — тот же mobile-first паттерн, что в landing.module.css (v1/v2
+ * лэндинга): база — узкий экран, затем 720px (планшет), макет v3 свёрстан под
+ * 1280 — где нужно, добавлен третий шаг.
+ *
+ * ------------------------------------------------------------------------
+ * ЭТОТ БЛОК (шапка / подвал / sticky-CTA) — префиксы hdr / ftr / bar.
+ * Секция «Шапка» = HeaderV3.tsx + CityPicker.tsx.
+ * Секция «Подвал» = FooterV3.tsx.
+ * Секция «Sticky CTA» = StickyCtaV3.tsx.
+ * ------------------------------------------------------------------------
+ */
+
+/* ==========================================================================
+ * Шапка (HeaderV3 + CityPicker)
+ * ========================================================================== */
+
+.hdrRoot {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ /* Фоллбэк на непрозрачный pageBg для браузеров без color-mix() — вторая
+ декларация того же свойства просто перезаписывает первую там, где
+ color-mix() поддерживается. */
+ background: var(--b2c-page-bg);
+ background: color-mix(in srgb, var(--b2c-page-bg) 94%, transparent);
+ backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--b2c-line);
+}
+
+.hdrInner {
+ max-width: 1280px;
+ margin-inline: auto;
+ padding: 10px clamp(20px, 5vw, 56px);
+ min-height: 64px;
+ display: flex;
+ align-items: center;
+ gap: 14px 26px;
+ flex-wrap: wrap;
+}
+
+.hdrLogo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: var(--b2c-ink);
+}
+
+.hdrLogoIcon {
+ width: 28px;
+ height: 28px;
+ flex: 0 0 auto;
+ border: 1.5px solid var(--b2c-accent);
+ border-radius: 4px;
+ display: grid;
+ place-items: center;
+}
+
+.hdrLogoDot {
+ width: 9px;
+ height: 9px;
+ display: block;
+ background: var(--b2c-accent);
+ border-radius: 2px;
+}
+
+.hdrLogoText {
+ display: flex;
+ flex-direction: column;
+ line-height: 1;
+}
+
+.hdrLogoName {
+ font-weight: 800;
+ font-size: 18px;
+ letter-spacing: 0.06em;
+}
+
+.hdrLogoSub {
+ margin-top: 3px;
+ font-family: var(--b2c-font-mono);
+ font-size: 9px;
+ letter-spacing: 0.18em;
+ color: var(--b2c-muted);
+}
+
+.hdrNav {
+ display: flex;
+ gap: 10px 22px;
+ flex-wrap: wrap;
+ font-size: 14.5px;
+ font-weight: 600;
+}
+
+.hdrNavLink {
+ color: var(--b2c-ink);
+ text-decoration: none;
+}
+
+.hdrNavLink:hover {
+ /* accentText, не accent: раскраска текста самим accent проваливает AA на
+ pageBg (2.8:1, см. b2c-tokens.ts) — accentText затемнён до 5.8:1. */
+ color: var(--b2c-accent-text);
+}
+
+/* «Статьи» — раздела нет в коде (ни маршрута, ни контента): пункт нарочно
+ неинтерактивен (aria-disabled, без href), а не ссылка в 404. */
+.hdrNavDisabled {
+ color: var(--b2c-muted);
+ opacity: 0.65;
+ cursor: not-allowed;
+}
+
+.hdrRight {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ flex-wrap: wrap;
+ margin-left: auto;
+}
+
+.hdrBizLink {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--b2c-muted);
+ text-decoration: none;
+}
+
+.hdrBizLink:hover {
+ color: var(--b2c-ink);
+}
+
+.hdrCta,
+.barBtn {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+ border: none;
+ border-radius: 6px;
+ font-weight: 700;
+ text-decoration: none;
+ cursor: pointer;
+ /* accentDeep, не accent (макет красил кнопку `#16A9BC`): белый текст на
+ accent даёт ~2.8:1, ниже AA. accentDeep — токен, специально заведённый
+ под белый текст кнопок (4.6:1, см. b2c-tokens.ts). Затемнение на hover —
+ через filter, чтобы не заводить ещё один цветовой литерал. */
+ background: var(--b2c-accent-deep);
+ color: var(--b2c-on-accent);
+}
+
+.hdrCta {
+ padding: 11px 22px;
+ font-size: 14px;
+}
+
+.hdrCta:hover,
+.barBtn:hover {
+ filter: brightness(0.92);
+}
+
+/* -- Селектор города (CityPicker) ---------------------------------------- */
+
+.hdrCityWrap {
+ position: relative;
+}
+
+.hdrCityBtn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: none;
+ border: 1px solid var(--b2c-line);
+ border-radius: 6px;
+ padding: 9px 14px;
+ cursor: pointer;
+ font-family: var(--b2c-font-sans);
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--b2c-ink);
+}
+
+.hdrCityBtn:hover {
+ border-color: var(--b2c-accent);
+ color: var(--b2c-accent-text);
+}
+
+.hdrCityDot {
+ width: 6px;
+ height: 6px;
+ flex: 0 0 auto;
+ display: block;
+ border-radius: 50%;
+ background: var(--b2c-accent);
+}
+
+.hdrCityCaret {
+ font-family: var(--b2c-font-mono);
+ font-size: 11px;
+ color: var(--b2c-muted);
+}
+
+.hdrCityPanel {
+ position: absolute;
+ top: calc(100% + 10px);
+ left: 0;
+ z-index: 80;
+ width: min(84vw, 340px);
+ max-width: calc(100vw - 40px);
+ overflow: hidden;
+ background: var(--b2c-surface);
+ border: 1px solid var(--b2c-line);
+ border-radius: 10px;
+ box-shadow: 0 24px 64px color-mix(in srgb, var(--b2c-ink) 18%, transparent);
+}
+
+.hdrCitySearchRow {
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--b2c-line-soft);
+}
+
+.hdrCityInput {
+ width: 100%;
+ box-sizing: border-box;
+ border: 1px solid var(--b2c-line);
+ border-radius: 6px;
+ padding: 11px 14px;
+ font-family: var(--b2c-font-sans);
+ font-size: 14.5px;
+ color: var(--b2c-ink);
+ background: var(--b2c-page-bg);
+ outline: none;
+}
+
+.hdrCityInput:focus {
+ border-color: var(--b2c-accent);
+ background: var(--b2c-surface);
+}
+
+.hdrCityList {
+ max-height: 300px;
+ overflow-y: auto;
+ padding: 6px 0;
+}
+
+.hdrCityItem {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ background: none;
+ border: none;
+ padding: 11px 18px;
+ text-align: left;
+ cursor: pointer;
+ font-family: var(--b2c-font-sans);
+}
+
+.hdrCityItem:hover {
+ background: var(--b2c-page-bg);
+}
+
+/* Текущий выбранный город (проставляется из CityPicker.tsx как data-атрибут,
+ не класс — состояние живёт в JS, а не в CSS-модуле). */
+.hdrCityItem[data-active="true"] {
+ background: var(--b2c-surface-soft);
+}
+
+.hdrCityItem[data-active="true"] .hdrCityItemName {
+ font-weight: 800;
+}
+
+.hdrCityItemName {
+ font-size: 14.5px;
+ font-weight: 600;
+ color: var(--b2c-ink);
+}
+
+.hdrCityItemNote {
+ font-family: var(--b2c-font-mono);
+ font-size: 10.5px;
+ color: var(--b2c-muted);
+ white-space: nowrap;
+}
+
+.hdrCityEmpty {
+ margin: 0;
+ padding: 22px 18px;
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--b2c-muted);
+}
+
+.hdrCityFooter {
+ padding: 12px 18px;
+ border-top: 1px solid var(--b2c-line-soft);
+ font-family: var(--b2c-font-mono);
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ color: var(--b2c-muted);
+}
+
+/* ==========================================================================
+ * Подвал (FooterV3)
+ * ========================================================================== */
+
+.ftrRoot {
+ background: var(--b2c-surface);
+ border-top: 1px solid var(--b2c-line);
+}
+
+.ftrGrid {
+ max-width: 1280px;
+ margin-inline: auto;
+ padding: 44px clamp(20px, 5vw, 56px) 0;
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 32px;
+}
+
+@media (min-width: 720px) {
+ .ftrGrid {
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
+ gap: 36px 48px;
+ }
+}
+
+.ftrBrandCol {
+ max-width: 340px;
+}
+
+.ftrBrand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 14px;
+}
+
+.ftrBrandDot {
+ width: 7px;
+ height: 7px;
+ display: block;
+ border-radius: 50%;
+ background: var(--b2c-accent);
+}
+
+.ftrBrandName {
+ font-weight: 800;
+ font-size: 16px;
+ letter-spacing: 0.14em;
+ color: var(--b2c-ink);
+}
+
+.ftrBrandText {
+ margin: 0;
+ font-size: 15px;
+ line-height: 1.65;
+ color: var(--b2c-muted);
+}
+
+.ftrColTitle {
+ margin: 0 0 16px;
+ font-family: var(--b2c-font-mono);
+ font-size: 10px;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--b2c-muted);
+}
+
+.ftrLinks {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+/* min-height 44 — та же зона нажатия, что в landing.module.css `.footerLinks
+ .link` (WCAG 2.5.8): пункт списка — самостоятельная цель, а не часть фразы,
+ строка 14.5px даёт вдвое меньше ориентира ~44px без явного добора высоты. */
+.ftrLink {
+ display: inline-flex;
+ align-items: center;
+ min-height: 44px;
+ font-size: 14.5px;
+ font-weight: 600;
+ color: var(--b2c-muted);
+ text-decoration: none;
+}
+
+.ftrLink:hover {
+ color: var(--b2c-accent-text);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.ftrLinkDisabled {
+ display: inline-flex;
+ align-items: center;
+ min-height: 44px;
+ font-size: 14.5px;
+ font-weight: 600;
+ color: var(--b2c-muted);
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.ftrText {
+ margin: 0;
+ font-size: 14.5px;
+ line-height: 1.6;
+ color: var(--b2c-muted);
+}
+
+.ftrText + .ftrText {
+ margin-top: 6px;
+}
+
+/* Ссылка ВНУТРИ предложения («Почта: …») — под inline-исключением из того же
+ правила зоны нажатия, что и в landing.module.css: раздувать строку текста
+ было бы хуже, чем оставить как есть. */
+.ftrInlineLink {
+ color: var(--b2c-accent-text);
+ font-weight: 600;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+.ftrInlineLink:hover {
+ color: var(--b2c-ink);
+}
+
+.ftrBottom {
+ max-width: 1280px;
+ margin-inline: auto;
+ margin-top: 36px;
+ padding: 22px clamp(20px, 5vw, 56px) 28px;
+ border-top: 1px solid var(--b2c-line-soft);
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px 32px;
+}
+
+.ftrBottomReqs {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ font-size: 13px;
+ color: var(--b2c-muted);
+}
+
+/* ==========================================================================
+ * Sticky CTA (StickyCtaV3)
+ * ========================================================================== */
+
+/* Компенсация fixed-позиционирования `.barRoot` (тот вырезан из потока
+ документа): без спейсера панель легла бы поверх последних пикселей
+ страницы при докрутке до конца (реквизиты в подвале, последний вопрос
+ FAQ). Высота — на мобильном тексте+кнопка чаще переносятся на две строки
+ (flex-wrap), поэтому база выше; на 720px+ строка одна. */
+.barSpacer {
+ height: 96px;
+}
+
+@media (min-width: 720px) {
+ .barSpacer {
+ height: 68px;
+ }
+}
+
+.barRoot {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 60;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16px 24px;
+ flex-wrap: wrap;
+ padding: 12px clamp(16px, 4vw, 32px);
+ /* Фоллбэк на непрозрачный surfaceDark для браузеров без color-mix(). */
+ background: var(--b2c-surface-dark);
+ background: color-mix(in srgb, var(--b2c-surface-dark) 97%, transparent);
+ backdrop-filter: blur(8px);
+}
+
+.barText {
+ margin: 0;
+ text-align: center;
+ font-weight: 700;
+ font-size: 14.5px;
+ /* Токенов «текст на тёмной поверхности» в b2c-tokens.ts нет — onAccent
+ (#FFFFFF) переиспользован ради значения, не семантики: тот же белый,
+ который нужен здесь, просто заведён под другим именем. */
+ color: var(--b2c-on-accent);
+}
+
+.barBtn {
+ padding: 12px 24px;
+ font-size: 14.5px;
+}
+
+/* ==========================================================================
+ * HERO (hero*) — первый экран: герой-текст + карточка «Бесплатная проверка».
+ * Секция «Герой» = HeroV3.tsx + FreeCheckCard.tsx + FreeResultV3.tsx.
+ * ========================================================================== */
+
+.heroSection {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: clamp(32px, 5vw, 60px) clamp(20px, 5vw, 56px) clamp(28px, 4vw, 44px);
+ box-sizing: border-box;
+}
+
+.heroGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr));
+ gap: clamp(28px, 4vw, 56px);
+ align-items: start;
+}
+
+.heroBadge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--b2c-surface-soft);
+ border-radius: 999px;
+ padding: 7px 14px;
+ margin-bottom: 20px;
+}
+
+.heroBadgeDot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--b2c-accent);
+ display: block;
+ animation: heroPulse 2s ease-in-out infinite;
+}
+
+.heroBadgeText {
+ font-family: var(--b2c-font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.12em;
+ color: var(--b2c-accent-text);
+}
+
+.heroTitle {
+ font-size: clamp(34px, 5vw, 56px);
+ line-height: 1.05;
+ letter-spacing: -0.025em;
+ font-weight: 800;
+ color: var(--b2c-ink);
+ margin: 0 0 18px;
+}
+
+.heroLead {
+ font-size: clamp(17px, 1.5vw, 20px);
+ line-height: 1.55;
+ color: var(--b2c-muted);
+ margin: 0 0 24px;
+ max-width: 540px;
+ text-wrap: pretty;
+}
+
+.heroChecklist {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-width: 540px;
+ margin-bottom: 28px;
+}
+
+.heroPerks {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-width: 520px;
+}
+
+.heroListItem {
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+}
+
+.heroListIcon {
+ color: var(--b2c-accent);
+ font-size: 15px;
+ line-height: 1.5;
+ flex: none;
+}
+
+.heroListText {
+ font-size: 15.5px;
+ line-height: 1.55;
+ color: var(--b2c-ink);
+}
+
+.heroNoBreak {
+ white-space: nowrap;
+}
+
+/* -- Карточка «Бесплатная проверка» -------------------------------------- */
+
+.heroCard {
+ background: var(--b2c-surface);
+ border: 1px solid var(--b2c-line);
+ border-radius: 14px;
+ box-shadow: 0 24px 64px color-mix(in srgb, var(--b2c-ink) 10%, transparent);
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+.heroCardHeader {
+ padding: 14px 26px;
+ background: var(--b2c-surface-dark);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.heroCardHeaderTitle {
+ color: var(--b2c-on-accent);
+ font-weight: 800;
+ font-size: 15px;
+}
+
+.heroCardBody {
+ padding: clamp(22px, 3vw, 30px);
+ box-sizing: border-box;
+}
+
+.heroForm {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.heroField {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+}
+
+.heroFieldRow {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
+ gap: 12px;
+}
+
+.heroFieldLabel {
+ font-family: var(--b2c-font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--b2c-muted);
+}
+
+.heroInput {
+ border: 1px solid var(--b2c-line);
+ border-radius: 6px;
+ padding: 13px 15px;
+ font-size: 15px;
+ font-family: var(--b2c-font-sans);
+ background: var(--b2c-page-bg);
+ outline: none;
+ color: var(--b2c-ink);
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.heroInput:focus-visible {
+ border-color: var(--b2c-accent);
+ background: var(--b2c-surface);
+ /* Base-стиль убирает outline (outline: none), поэтому видимый фокус здесь
+ держится на кольце из box-shadow, не на браузерном outline. */
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--b2c-accent) 25%, transparent);
+}
+
+.heroInputInvalid {
+ border-color: var(--b2c-danger);
+}
+
+.heroConditionGroup {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.heroConditionBtn {
+ font-family: var(--b2c-font-sans);
+ font-size: 13.5px;
+ font-weight: 700;
+ padding: 10px 16px;
+ border-radius: 6px;
+ cursor: pointer;
+ background: var(--b2c-page-bg);
+ color: var(--b2c-ink);
+ border: 1px solid var(--b2c-line);
+}
+
+.heroConditionBtn:hover {
+ border-color: var(--b2c-accent);
+}
+
+.heroConditionBtnActive,
+.heroConditionBtnActive:hover {
+ background: var(--b2c-surface-dark);
+ color: var(--b2c-on-accent);
+ border-color: var(--b2c-surface-dark);
+}
+
+.heroSubmitBtn {
+ /* accentDeep, не accent: белый текст на #16A9BC даёт ~2.8:1 (провал AA),
+ на accentDeep — см. обоснование в b2c-tokens.ts. Затемнение на hover —
+ через filter, чтобы не заводить ещё один цветовой литерал (тот же приём,
+ что в .hdrCta выше). */
+ background: var(--b2c-accent-deep);
+ color: var(--b2c-on-accent);
+ font-weight: 700;
+ font-size: 16px;
+ font-family: var(--b2c-font-sans);
+ padding: 16px 20px;
+ border-radius: 6px;
+ border: none;
+ cursor: pointer;
+ margin-top: 6px;
+}
+
+.heroSubmitBtn:hover {
+ filter: brightness(0.92);
+}
+
+.heroSubmitBtn:focus-visible {
+ outline: 2px solid var(--b2c-ink);
+ outline-offset: 2px;
+}
+
+.heroSubmitCaption {
+ font-family: var(--b2c-font-mono);
+ font-size: 10px;
+ color: var(--b2c-muted);
+ text-align: center;
+ letter-spacing: 0.06em;
+ margin: 0;
+}
+
+.heroFormFeedback {
+ border: 1px solid var(--b2c-line);
+ border-radius: 8px;
+ padding: 14px 16px;
+ background: var(--b2c-surface-soft);
+}
+
+.heroFormFeedbackError {
+ border-color: var(--b2c-danger);
+ background: var(--b2c-surface);
+}
+
+.heroFormFeedbackTitle {
+ font-weight: 700;
+ font-size: 14px;
+ color: var(--b2c-ink);
+ margin: 0 0 6px;
+}
+
+.heroFormFeedbackText {
+ font-size: 13.5px;
+ line-height: 1.5;
+ color: var(--b2c-muted);
+ margin: 0;
+}
+
+.heroFormFeedbackText + .heroFormFeedbackText {
+ margin-top: 6px;
+}
+
+/* -- Ветка результата (FreeResultV3, сегодня не подключена) -------------- */
+
+.heroResultEyebrow {
+ font-family: var(--b2c-font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.14em;
+ color: var(--b2c-accent-text);
+ margin: 0 0 14px;
+}
+
+.heroResultGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.heroResultTile {
+ background: var(--b2c-surface-soft);
+ border-radius: 8px;
+ padding: 18px;
+}
+
+.heroResultTileValue {
+ font-size: 34px;
+ font-weight: 800;
+ line-height: 1;
+ color: var(--b2c-ink);
+}
+
+.heroResultTileLabel {
+ font-size: 13px;
+ color: var(--b2c-accent-text);
+ margin-top: 5px;
+}
+
+.heroResultLocked {
+ position: relative;
+ border: 1px solid var(--b2c-line);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.heroResultSkeleton {
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.heroResultSkeletonLine1,
+.heroResultSkeletonLine2,
+.heroResultSkeletonBar {
+ background: var(--b2c-line-soft);
+ border-radius: 4px;
+}
+
+.heroResultSkeletonLine1 {
+ width: 46%;
+ height: 22px;
+}
+
+.heroResultSkeletonLine2 {
+ width: 70%;
+ height: 12px;
+}
+
+.heroResultSkeletonBar {
+ width: 100%;
+ height: 8px;
+ margin-top: 4px;
+}
+
+.heroResultOverlay {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ text-align: center;
+ background: color-mix(in srgb, var(--b2c-surface) 35%, transparent);
+ padding: 16px;
+ box-sizing: border-box;
+}
+
+.heroResultOverlayTitle {
+ font-weight: 800;
+ font-size: 15.5px;
+ color: var(--b2c-ink);
+ margin: 0 0 12px;
+}
+
+.heroResultOverlayBtn {
+ display: inline-block;
+ background: var(--b2c-line);
+ color: var(--b2c-muted);
+ font-weight: 700;
+ font-size: 15px;
+ font-family: var(--b2c-font-sans);
+ padding: 13px 24px;
+ border-radius: 6px;
+ border: none;
+ cursor: not-allowed;
+}
+
+.heroResultOverlayCaption {
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--b2c-muted);
+ margin: 10px 0 0;
+ max-width: 260px;
+}
+
+.heroResultBackBtn {
+ background: none;
+ border: none;
+ color: var(--b2c-muted);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ font-family: var(--b2c-font-sans);
+ margin-top: 14px;
+ padding: 0;
+}
+
+.heroResultBackBtn:hover {
+ color: var(--b2c-accent-text);
+}
+
+@keyframes heroPulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.45;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .heroBadgeDot {
+ animation: none;
+ }
+}
+
+@media (max-width: 480px) {
+ .heroCardHeader {
+ padding: 14px 18px;
+ }
+
+ .heroResultGrid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* ==========================================================================
+ * StepsV3 — три шага «что делает сервис» (макет ~210-228). Префикс steps.
+ * ========================================================================== */
+
+/* Визуально скрытый заголовок секции (в макете нет видимого
, но у
+ секции обязан быть доступный aria-labelledby — общий паттерн "screen-reader
+ only", НЕ tailwind-класс: маршрут mera-public красится CSS-модулями, у
+ этого фронтенда Tailwind в src/ не подключён вовсе. */
+.stepsSrOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.stepsSection {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 0 clamp(20px, 5vw, 56px) clamp(40px, 5vw, 64px);
+}
+
+.stepsList {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
+ gap: 20px;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.stepsItem {
+ background: var(--b2c-surface);
+ border: 1px solid var(--b2c-line);
+ border-radius: 10px;
+ padding: 26px;
+}
+
+/* Третий шаг (платный) выделен акцентной подложкой в макете. */
+.stepsItemAccent {
+ background: var(--b2c-surface-soft);
+ border-color: var(--b2c-accent);
+}
+
+.stepsLabel {
+ font-family: var(--b2c-font-mono);
+ font-size: 10px;
+ letter-spacing: 0.16em;
+ color: var(--b2c-accent-text);
+ margin-bottom: 14px;
+}
+
+/* Макет красит бейдж шага 3 более тёмным оттенком акцента (он и на светлой
+ подложке .stepsItemAccent, и сам по себе уже проходит AA — в отличие от
+ accent-text здесь не требуется затемнение). */
+.stepsLabelAccent {
+ color: var(--b2c-accent-deep);
+}
+
+.stepsTitle {
+ font-weight: 800;
+ font-size: 18px;
+ margin: 0 0 8px;
+}
+
+.stepsText {
+ font-size: 14.5px;
+ line-height: 1.6;
+ color: var(--b2c-muted);
+ margin: 0;
+}
+
+/* Макет даёт тексту третьей карточки более тёмный оттенок (#3A4A4F), которого
+ нет среди токенов — ближайший официальный вариант в палитре это ink. */
+.stepsTextAccent {
+ color: var(--b2c-ink);
+}
+
+/* ==========================================================================
+ * AccuracyV3 — «Точность»: KPI + таблица сверок (макет ~231-283, id="accuracy").
+ * Префикс acc.
+ * ========================================================================== */
+
+.accSection {
+ background: var(--b2c-surface);
+ border-top: 1px solid var(--b2c-line);
+ border-bottom: 1px solid var(--b2c-line);
+}
+
+.accContainer {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: clamp(48px, 6vw, 80px) clamp(20px, 5vw, 56px);
+}
+
+.accHead {
+ max-width: 700px;
+ margin-bottom: 36px;
+}
+
+.accEyebrow {
+ font-family: var(--b2c-font-mono);
+ font-size: 11px;
+ letter-spacing: 0.22em;
+ color: var(--b2c-accent-text);
+ margin-bottom: 14px;
+}
+
+.accTitle {
+ font-size: clamp(26px, 3.4vw, 38px);
+ line-height: 1.12;
+ letter-spacing: -0.015em;
+ font-weight: 800;
+ margin: 0 0 14px;
+}
+
+.accLead {
+ font-size: 16.5px;
+ line-height: 1.6;
+ color: var(--b2c-muted);
+ margin: 0;
+ text-wrap: pretty;
+}
+
+.accKpiGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
+ gap: 20px;
+ margin: 0 0 32px;
+ padding: 0;
+ list-style: none;
+}
+
+.accKpiTile {
+ background: var(--b2c-page-bg);
+ border: 1px solid var(--b2c-line);
+ border-radius: 10px;
+ padding: 26px;
+}
+
+.accKpiTileDark {
+ background: var(--b2c-surface-dark);
+ border-color: var(--b2c-surface-dark);
+ color: var(--b2c-on-accent);
+}
+
+.accKpiValue {
+ font-size: clamp(30px, 3.4vw, 40px);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ line-height: 1;
+}
+
+.accKpiLabel {
+ font-weight: 700;
+ font-size: 15px;
+ margin-top: 10px;
+}
+
+.accKpiNote {
+ font-size: 13.5px;
+ line-height: 1.55;
+ color: var(--b2c-muted);
+ margin-top: 6px;
+}
+
+/* Приглушённый текст на тёмной подложке 4-й плитки: токена «muted-on-dark» в
+ палитре нет (--b2c-muted на --b2c-surface-dark не проходит AA), поэтому
+ берём on-accent (белый) с пониженной непрозрачностью — тот же приём, что и
+ ниже в .costTextDark. */
+.accKpiNoteDark {
+ composes: accKpiNote;
+ color: var(--b2c-on-accent);
+ opacity: 0.72;
+ margin-top: 6px;
+}
+
+/* Таблица сверок — те же ряды-гриды, что и в макете, но с явными
+ role="table/row/columnheader/cell" вместо implicit table-ролей: у div-грида
+ их и не было бы, а Safari к тому же снимает implicit-роли при смене display
+ на брейкпоинте (тот же квирк, что описан в HowItWorks.tsx у списков), так
+ что для мобильной перестройки в карточки роли всё равно нужны явные. */
+.accTable {
+ border: 1px solid var(--b2c-line);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.accTableHead {
+ display: grid;
+ grid-template-columns: 1.8fr 1.1fr 1.1fr 0.8fr 1.2fr;
+ gap: 12px;
+ padding: 14px 22px;
+ background: var(--b2c-page-bg);
+ font-family: var(--b2c-font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.12em;
+ color: var(--b2c-muted);
+}
+
+.accTableHeadCellRight {
+ text-align: right;
+ color: var(--b2c-accent-deep);
+}
+
+.accRow {
+ display: grid;
+ grid-template-columns: 1.8fr 1.1fr 1.1fr 0.8fr 1.2fr;
+ gap: 12px;
+ padding: 16px 22px;
+ border-top: 1px solid var(--b2c-surface-soft);
+ align-items: center;
+}
+
+.accObjectAddr {
+ display: block;
+ font-weight: 700;
+ font-size: 14.5px;
+ color: var(--b2c-ink);
+}
+
+.accObjectMeta {
+ display: block;
+ font-size: 12.5px;
+ color: var(--b2c-muted);
+ margin-top: 2px;
+}
+
+.accCellSaid {
+ font-family: var(--b2c-font-mono);
+ font-size: 13px;
+ color: var(--b2c-muted);
+}
+
+.accCellFact {
+ font-family: var(--b2c-font-mono);
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--b2c-ink);
+}
+
+.accCellErr {
+ font-family: var(--b2c-font-mono);
+ font-size: 12px;
+ color: var(--b2c-muted);
+}
+
+.accCellGain {
+ text-align: right;
+}
+
+.accGainStack {
+ display: inline-block;
+}
+
+.accGainValue {
+ display: block;
+ font-weight: 800;
+ font-size: 16px;
+ color: var(--b2c-accent-deep);
+}
+
+.accGainPct {
+ display: block;
+ font-size: 12px;
+ color: var(--b2c-muted);
+ margin-top: 2px;
+}
+
+.accFootnote {
+ font-size: 12.5px;
+ line-height: 1.5;
+ color: var(--b2c-muted);
+ margin-top: 14px;
+}
+
+/* Мобильный брейк: строки превращаются в карточки-flex, подпись поля берётся
+ из data-label через content: attr() — без дублирования текста в JSX.
+ Адрес/метраж не подписываем: они самоописательные и так. */
+@media (max-width: 719px) {
+ .accTableHead {
+ display: none;
+ }
+
+ .accRow {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ padding: 18px 20px;
+ }
+
+ .accCellSaid,
+ .accCellFact,
+ .accCellErr,
+ .accCellGain {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+ text-align: left;
+ }
+
+ .accGainStack {
+ text-align: right;
+ }
+
+ .accCellSaid::before,
+ .accCellFact::before,
+ .accCellErr::before,
+ .accCellGain::before {
+ content: attr(data-label);
+ flex: 0 0 auto;
+ font-family: var(--b2c-font-mono);
+ font-size: 10px;
+ letter-spacing: 0.06em;
+ color: var(--b2c-muted);
+ }
+}
+
+/* ==========================================================================
+ * CostOfErrorV3 — «Цена ошибки» (макет ~376-400). Префикс cost.
+ * ========================================================================== */
+
+.costSection {
+ background: var(--b2c-surface);
+ border-top: 1px solid var(--b2c-line);
+ border-bottom: 1px solid var(--b2c-line);
+}
+
+.costContainer {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: clamp(48px, 6vw, 80px) clamp(20px, 5vw, 56px);
+}
+
+.costHead {
+ max-width: 680px;
+ margin-bottom: 36px;
+}
+
+.costEyebrow {
+ font-family: var(--b2c-font-mono);
+ font-size: 11px;
+ letter-spacing: 0.22em;
+ color: var(--b2c-accent-text);
+ margin-bottom: 14px;
+}
+
+.costTitle {
+ font-size: clamp(26px, 3.4vw, 38px);
+ line-height: 1.12;
+ letter-spacing: -0.015em;
+ font-weight: 800;
+ margin: 0;
+}
+
+.costGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
+ gap: 20px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.costTile {
+ background: var(--b2c-page-bg);
+ border: 1px solid var(--b2c-line);
+ border-radius: 10px;
+ padding: 28px;
+}
+
+/* Третья плитка (150 ₽) — тёмная, как в макете. */
+.costTileDark {
+ background: var(--b2c-surface-dark);
+ border-color: var(--b2c-surface-dark);
+ color: var(--b2c-on-accent);
+}
+
+.costValue {
+ font-size: clamp(28px, 3vw, 36px);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ line-height: 1;
+}
+
+.costItemTitle {
+ font-weight: 700;
+ font-size: 15.5px;
+ margin-top: 12px;
+}
+
+.costText {
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--b2c-muted);
+ margin-top: 8px;
+}
+
+/* Тот же приём, что и .accKpiNoteDark — see комментарий выше. */
+.costTextDark {
+ composes: costText;
+ color: var(--b2c-on-accent);
+ opacity: 0.72;
+ margin-top: 8px;
+}
diff --git a/tradein-mvp/frontend/src/app/mera-public/layout.tsx b/tradein-mvp/frontend/src/app/mera-public/layout.tsx
index a690d775..bae99e6d 100644
--- a/tradein-mvp/frontend/src/app/mera-public/layout.tsx
+++ b/tradein-mvp/frontend/src/app/mera-public/layout.tsx
@@ -5,8 +5,16 @@ import { REGION_NAME } from "./content";
import styles from "./landing.module.css";
import { landingVars } from "./theme";
-import { SiteFooter } from "./_components/SiteFooter";
-import { SiteHeader } from "./_components/SiteHeader";
+/**
+ * ШАПКА И ПОДВАЛ ЖИВУТ В СТРАНИЦАХ, А НЕ ЗДЕСЬ (15.08.2026). Раньше layout
+ * рендерил SiteHeader/SiteFooter для всего поддерева. С появлением второй
+ * версии лэндинга (`v3/`, макет владельца) это стало блокером: вложенный
+ * layout в Next добавляется К родительскому, а не заменяет его, поэтому
+ * страница с собственной шапкой получала бы ДВЕ шапки сразу. Теперь layout
+ * отвечает только за обёртку — шрифты, токены, skip-link, — а каждая страница
+ * сама выбирает свой хром: v1 берёт SiteHeader/SiteFooter, v3 — HeaderV3/
+ * FooterV3/StickyCtaV3.
+ */
/**
* Публичный (B2C) периметр «МЕРЫ»: лэндинг для собственника квартиры.
@@ -78,9 +86,7 @@ export default function MeraPublicLayout({
Перейти к содержимому
-
{children}
-
);
}
diff --git a/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts b/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts
new file mode 100644
index 00000000..3db0f1df
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/mera-public/marketing-v3.ts
@@ -0,0 +1,166 @@
+/**
+ * marketing-v3 — ПЛЕЙСХОЛДЕРНЫЕ маркетинговые величины лэндинга (макет v3).
+ *
+ * ⚠️ ЧИТАТЬ ПЕРЕД ИСПОЛЬЗОВАНИЕМ. Ни одно число в этом файле не подтверждено
+ * замером продукта. Они взяты из дизайн-макета владельца (15.08.2026) как
+ * продающие формулировки и держатся ЗДЕСЬ, в одном месте, ровно для того,
+ * чтобы их можно было заменить одной правкой, когда появятся вычисляемые
+ * метрики.
+ *
+ * Что говорят наши собственные замеры на ту же тему (зафиксировано, чтобы
+ * расхождение не потерялось — issue «compliance: заменить маркетинговые
+ * метрики лэндинга на вычисляемые до снятия noindex»):
+ *
+ * | здесь | факт по замеру |
+ * |------------------------|-----------------------------------------------|
+ * | ошибка 1,8% | 13,2% медиана |%| (бэктест n=277), held-out 16,1% |
+ * | 83% в диапазон | 81,95% — но коридор шириной 74% от точки |
+ * | ±6 дн по сроку | сверки срока с фактом НЕ СУЩЕСТВУЕТ |
+ * | 42 700 проверок за год | 1 084 оценки всего (база живёт 2,5 месяца) |
+ * | 42 города РФ | 6 городов области, полное покрытие — 1 (ЕКБ) |
+ *
+ * ГЕЙТ: пока `robots: { index: false }` в `layout.tsx` стоит и домен не открыт
+ * наружу, эти строки никому не показываются. ПЕРЕД снятием noindex каждая
+ * величина обязана быть либо заменена вычисляемой, либо снята со страницы —
+ * публикация недостоверных характеристик платной услуги подпадает под ст. 5
+ * ФЗ «О рекламе» и ЗоЗПП.
+ *
+ * Правило для новых секций: не вписывай число в JSX руками — заведи константу
+ * здесь. Иначе замена «одной правкой» перестанет работать ровно тогда, когда
+ * понадобится.
+ */
+
+/** Точность прогноза — блок «Точность» и бейдж героя. */
+export const ACCURACY_PLACEHOLDER = {
+ /** Средняя ошибка по цене. */
+ priceError: "1,8%",
+ priceErrorNote: "медиана расхождения прогноза и цены сделки",
+ /** Точность по сроку продажи. */
+ daysError: "±6 дн.",
+ daysErrorNote: "отклонение факта от прогнозного окна",
+ /** Доля сделок внутри рекомендованного коридора. */
+ coverage: "83%",
+ coverageNote: "цена сделки внутри рекомендованного коридора",
+ /** Объём проверок за год. */
+ checks: "42 700",
+ checksNote: "из них 3 180 доведены до сделки и сверены",
+} as const;
+
+/** Блок «Почему это важно» — цена ошибки в цене. */
+export const COST_OF_ERROR_PLACEHOLDER = [
+ {
+ value: "−4,3%",
+ title: "теряет цена за каждый месяц простоя",
+ text: "Покупатели видят «зависшее» объявление и начинают торговаться сильнее.",
+ },
+ {
+ value: "×2,4",
+ title: "дольше продаётся квартира с завышенной ценой",
+ text: "Даже +7% к рыночной цене выбивают вас из выдачи по фильтрам покупателей.",
+ },
+ {
+ value: "150 ₽",
+ title: "стоит узнать правильную цену заранее",
+ text: "Один платёж. Без подписки. Отчёт остаётся у вас по ссылке.",
+ },
+] as const;
+
+/**
+ * Пример бесплатной части в макете. Это ИЛЛЮСТРАЦИЯ на статичной странице, а
+ * не расчёт по введённому адресу: анонимного расчёта сегодня нет вовсе
+ * (`PUBLIC_ESTIMATE_ENABLED = false`, ручки закрыты RBAC).
+ *
+ * Как только появится `POST /trade-in/coverage`, эти значения обязаны уехать —
+ * показывать одинаковые «14 объявлений» любому, кто ввёл свой адрес, это уже
+ * не продающая формулировка, а обман конкретного человека.
+ */
+export const FREE_SAMPLE_PLACEHOLDER = {
+ analogs: "14",
+ analogsLabel: "похожих объявлений рядом",
+ days: "47 дн.",
+ daysLabel: "средний срок продажи",
+} as const;
+
+/** Отчётный период сводки в блоке «Точность» (лид над KPI-плитками, id="accuracy"). */
+export const ACCURACY_PERIOD_LABEL = "последние 12 месяцев";
+
+/** Заявленное время на шаг 1 («описываете квартиру») — бейдж в блоке «Как это работает». */
+export const STEP1_DURATION_LABEL = "30 секунд";
+
+/**
+ * Строки таблицы сверок в блоке «Точность» (id="accuracy"). Это ИЛЛЮСТРАЦИЯ
+ * формата отчёта из дизайн-макета (адреса, суммы и расхождения придуманы для
+ * примера), а не реальные сделки — макет не содержал заполненных данных для
+ * этой таблицы (только шаблон `{{ r.addr }}` и т.д.), реальных проверок под
+ * ней нет. `checks`/`err`/`gain` здесь НЕ обязаны биться с
+ * `ACCURACY_PLACEHOLDER` арифметически — это витринные примеры, а не выборка,
+ * из которой посчитана средняя ошибка.
+ *
+ * Как и весь файл: до снятия noindex каждая строка обязана быть либо заменена
+ * реальной сверенной сделкой, либо явно помечена как иллюстрация в самой
+ * вёрстке (см. подпись под таблицей в AccuracyV3.tsx).
+ */
+export interface ProofRow {
+ /** Адрес/район объекта. */
+ readonly addr: string;
+ /** Тип, площадь, этаж. */
+ readonly meta: string;
+ /** Прогноз «Меры». */
+ readonly said: string;
+ /** Фактическая цена сделки. */
+ readonly fact: string;
+ /** Расхождение прогноза и факта, %. */
+ readonly err: string;
+ /** Превышение над средней ценой по району, ₽. */
+ readonly gain: string;
+ /** То же превышение, %. */
+ readonly gainPct: string;
+}
+
+export const PROOF_ROWS_PLACEHOLDER: readonly ProofRow[] = [
+ {
+ addr: "Втузгородок, ул. Ясная, 8",
+ meta: "1-к, 34 м², 6/16 этаж",
+ said: "3 640 000 ₽",
+ fact: "3 705 000 ₽",
+ err: "1,8%",
+ gain: "+165 000 ₽",
+ gainPct: "+4,7%",
+ },
+ {
+ addr: "Юго-Западный, ул. Юмашева, 15",
+ meta: "2-к, 52 м², 3/9 этаж",
+ said: "5 210 000 ₽",
+ fact: "5 180 000 ₽",
+ err: "0,6%",
+ gain: "+96 000 ₽",
+ gainPct: "+1,9%",
+ },
+ {
+ addr: "Ботанический, ул. Академика Бардина, 23",
+ meta: "3-к, 71 м², 8/16 этаж",
+ said: "7 480 000 ₽",
+ fact: "7 640 000 ₽",
+ err: "2,1%",
+ gain: "+310 000 ₽",
+ gainPct: "+4,2%",
+ },
+ {
+ addr: "Уралмаш, ул. Кировградская, 42",
+ meta: "2-к, 46 м², 5/5 этаж",
+ said: "4 120 000 ₽",
+ fact: "4 065 000 ₽",
+ err: "1,3%",
+ gain: "+58 000 ₽",
+ gainPct: "+1,4%",
+ },
+ {
+ addr: "Центр, ул. Малышева, 51",
+ meta: "1-к, 38 м², 11/25 этаж",
+ said: "6 350 000 ₽",
+ fact: "6 490 000 ₽",
+ err: "2,2%",
+ gain: "+215 000 ₽",
+ gainPct: "+3,4%",
+ },
+] as const;
diff --git a/tradein-mvp/frontend/src/app/mera-public/oferta/page.tsx b/tradein-mvp/frontend/src/app/mera-public/oferta/page.tsx
index b5908fe3..84fdf581 100644
--- a/tradein-mvp/frontend/src/app/mera-public/oferta/page.tsx
+++ b/tradein-mvp/frontend/src/app/mera-public/oferta/page.tsx
@@ -13,6 +13,9 @@ import {
} from "../content";
import styles from "../landing.module.css";
+import { SiteFooter } from "../_components/SiteFooter";
+import { SiteHeader } from "../_components/SiteHeader";
+
/**
* Публичная оферта на оказание информационно-аналитических услуг «МЕРА».
*
@@ -55,7 +58,9 @@ export default function MeraPublicOfferPage() {
const entity = LEGAL_ENTITY;
return (
-