fix(mera/b2c): на мобильном липкий хром съедал 42% экрана
Жалоба «много всплывашек, экрана не видно» — это не всплывашки, а постоянный
хром: на 375×812 липкая шапка (235 px) и фиксированная нижняя панель (109 px)
вместе занимали 42% экрана и отъедали их при любой прокрутке. Замер тем же
способом (getBoundingClientRect по элементам с position fixed/sticky): было
366 px = 45,1% высоты экрана, стало 119,5 px = 14,7%. На 1280 — 135,5 px до и
после, без изменений.
Шапка на узком экране сведена к одной строке (лого + «Для бизнеса»):
- навигация по секциям скрыта — четыре пункта переносились в две строки, а
на мобильном к секциям всё равно доезжают прокруткой; «Статьи» и «МЕРА для
бизнеса» остаются ссылками в подвале, «Продажа под ключ» и на десктопе
неактивная заглушка;
- CTA скрыт — он вёл туда же, куда нижняя панель, и два одинаковых призыва
на одном экране это и есть жалоба; панель осталась, шапка уступила.
Селектор города (CityPicker) удалён совсем, а не спрятан: он не владел своим
выбором — состояние жило внутри компонента и никуда не отправлялось, — тогда
как гео-гейт расчёта стоит на городе из формы (FreeCheckCard, #2576). Два
места, задающих одно значение, из которых работает одно, — дефект сам по себе;
то же правило уже записано в InnerHeader. Граница покрытия, ради которой
выпадашку открывали, стоит текстом в герое.
Нижняя панель на узком экране — только кнопка: её строка переносилась на две-
три и делала панель втрое выше кнопки. То же обещание остаётся словами героя.
Шаг медиазапроса — 1000 px, а не общий для файла 720: на 720 шапка макета всё
ещё переносится в две строки (замерено: 109 px хрома), то есть возвращать её
там значило бы вернуть тот же дефект планшету.
This commit is contained in:
parent
1f3d8d0b15
commit
5ed4a4ec25
5 changed files with 100 additions and 305 deletions
|
|
@ -1,140 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CityPicker — выбор города в шапке лэндинга v3 (макет ~ строки 47-73).
|
|
||||||
*
|
|
||||||
* Презентационный клиентский остров: сегодня публичный расчёт выключен
|
|
||||||
* (`PUBLIC_ESTIMATE_ENABLED = false` в content.ts), поэтому выбранный здесь
|
|
||||||
* город никуда не отправляется — состояние живёт только в этом компоненте и
|
|
||||||
* показывает посетителю, что сервис знает больше одного города области.
|
|
||||||
* Когда форма расчёта откроется, сюда нужно будет добавить проброс выбора
|
|
||||||
* наружу (проп/контекст) — в этом заходе scope ограничен шапкой/подвалом/
|
|
||||||
* sticky-баром, самой формы здесь нет.
|
|
||||||
*
|
|
||||||
* Список городов и деление на «полное покрытие» / «данных меньше» берутся из
|
|
||||||
* `OBLAST_CITIES` / `PRIMARY_CITY` (content.ts, тот же реестр, что честно
|
|
||||||
* объясняет разницу в покрытии в Hero.tsx). Макет показывал числовые «веса»
|
|
||||||
* по городам (тысячи объявлений) — мы их не повторяем: таких цифр в коде нет,
|
|
||||||
* а гадать нельзя (правило честности в content.ts).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useId, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { DEFAULT_CITY, OBLAST_CITIES } from "@/lib/city-registry";
|
|
||||||
|
|
||||||
import { PRIMARY_CITY, REGION_NAME } from "../../content";
|
|
||||||
import styles from "../../landing-v3.module.css";
|
|
||||||
|
|
||||||
export function CityPicker() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const [city, setCity] = useState<string>(DEFAULT_CITY.label);
|
|
||||||
const wrapRef = useRef<HTMLDivElement>(null);
|
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const panelId = useId();
|
|
||||||
|
|
||||||
// Клик вне панели — закрыть. Тот же паттерн, что components/auth/UserMenu.tsx.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
function handleClick(e: MouseEvent) {
|
|
||||||
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleClick);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClick);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
// Escape — закрыть и вернуть фокус на кнопку.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
function handleKey(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
setOpen(false);
|
|
||||||
buttonRef.current?.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("keydown", handleKey);
|
|
||||||
return () => document.removeEventListener("keydown", handleKey);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const normalizedQuery = query.trim().toLowerCase();
|
|
||||||
const filtered = normalizedQuery
|
|
||||||
? OBLAST_CITIES.filter((c) =>
|
|
||||||
c.label.toLowerCase().includes(normalizedQuery),
|
|
||||||
)
|
|
||||||
: OBLAST_CITIES;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.hdrCityWrap} ref={wrapRef}>
|
|
||||||
<button
|
|
||||||
ref={buttonRef}
|
|
||||||
type="button"
|
|
||||||
className={styles.hdrCityBtn}
|
|
||||||
onClick={() => setOpen((v) => !v)}
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded={open}
|
|
||||||
aria-controls={panelId}
|
|
||||||
>
|
|
||||||
<span className={styles.hdrCityDot} aria-hidden="true" />
|
|
||||||
<span>{city}</span>
|
|
||||||
<span className={styles.hdrCityCaret} aria-hidden="true">
|
|
||||||
{open ? "▲" : "▼"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open ? (
|
|
||||||
<div
|
|
||||||
id={panelId}
|
|
||||||
className={styles.hdrCityPanel}
|
|
||||||
aria-label="Выбор города"
|
|
||||||
>
|
|
||||||
<div className={styles.hdrCitySearchRow}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={styles.hdrCityInput}
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
placeholder="Поиск города"
|
|
||||||
aria-label="Поиск города"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.hdrCityList}>
|
|
||||||
{filtered.length > 0 ? (
|
|
||||||
filtered.map((c) => (
|
|
||||||
<button
|
|
||||||
key={c.id}
|
|
||||||
type="button"
|
|
||||||
className={styles.hdrCityItem}
|
|
||||||
data-active={c.label === city ? "true" : undefined}
|
|
||||||
onClick={() => {
|
|
||||||
setCity(c.label);
|
|
||||||
setOpen(false);
|
|
||||||
setQuery("");
|
|
||||||
buttonRef.current?.focus();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className={styles.hdrCityItemName}>{c.label}</span>
|
|
||||||
<span className={styles.hdrCityItemNote}>
|
|
||||||
{c.label === PRIMARY_CITY
|
|
||||||
? "Полное покрытие"
|
|
||||||
: "Данных меньше"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className={styles.hdrCityEmpty}>
|
|
||||||
Города нет в списке. Мы работаем там, где хватает данных по
|
|
||||||
сделкам — напишите нам, и добавим.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={styles.hdrCityFooter}>
|
|
||||||
ДАННЫЕ ПО {OBLAST_CITIES.length} ГОРОДАМ:{" "}
|
|
||||||
{REGION_NAME.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -28,10 +28,11 @@
|
||||||
* Правило из шапки `lib/city-registry.ts`: город считается известным, только
|
* Правило из шапки `lib/city-registry.ts`: город считается известным, только
|
||||||
* если человек выбрал его сам ИЛИ `detectCityInText` нашёл его в тексте
|
* если человек выбрал его сам ИЛИ `detectCityInText` нашёл его в тексте
|
||||||
* адреса; молча подставлять Екатеринбург нельзя. Ответ про город вне покрытия
|
* адреса; молча подставлять Екатеринбург нельзя. Ответ про город вне покрытия
|
||||||
* — тот же и дословно тот же, что в v1. Выпадашка города в шапке
|
* — тот же и дословно тот же, что в v1. Выпадашки города в шапке больше нет
|
||||||
* (`CityPicker`) сюда НЕ пробрасывается: она презентационная и своим выбором
|
* вовсе (`CityPicker` удалён 29.08.2026, см. JSDoc HeaderV3): она была
|
||||||
* не владеет (см. её JSDoc), а гейт обязан читать то, что человек указал
|
* презентационной и своим выбором не владела, то есть значение задавалось в
|
||||||
* рядом с адресом.
|
* двух местах, а на расчёт влияло только это. Гейт обязан читать то, что
|
||||||
|
* человек указал рядом с адресом, — и теперь это единственное такое место.
|
||||||
*
|
*
|
||||||
* СОСТОЯНИЕ ТЕПЕРЬ ДОЕЗЖАЕТ ДО ЧЕРНОВИКА. Поле было обязательным, а его
|
* СОСТОЯНИЕ ТЕПЕРЬ ДОЕЗЖАЕТ ДО ЧЕРНОВИКА. Поле было обязательным, а его
|
||||||
* значение никуда не клалось — человек выбирал состояние квартиры, и мы его
|
* значение никуда не клалось — человек выбирал состояние квартиры, и мы его
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,34 @@
|
||||||
/**
|
/**
|
||||||
* HeaderV3 — шапка лэндинга v3 (макет ~ строки 32-78): sticky, blur, лого +
|
* HeaderV3 — шапка лэндинга v3 (макет ~ строки 32-78): sticky, blur, лого +
|
||||||
* навигация + селектор города + CTA.
|
* навигация + CTA.
|
||||||
*
|
*
|
||||||
* Логотип НЕ ссылка: на публичном домене корень — это сам лэндинг, то есть
|
* Логотип НЕ ссылка: на публичном домене корень — это сам лэндинг, то есть
|
||||||
* ссылка вела бы на текущую же страницу.
|
* ссылка вела бы на текущую же страницу.
|
||||||
*
|
*
|
||||||
|
* СЕЛЕКТОРА ГОРОДА ЗДЕСЬ БОЛЬШЕ НЕТ (29.08.2026). `CityPicker` жил в этой
|
||||||
|
* шапке с тех пор, когда карточка проверки города не спрашивала вовсе, и
|
||||||
|
* своим выбором он НЕ владел: состояние оставалось внутри компонента и никуда
|
||||||
|
* не отправлялось (см. его прежний JSDoc). Город у формы появился —
|
||||||
|
* `FreeCheckCard` спрашивает его сама и на нём стоит гео-гейт расчёта (#2576),
|
||||||
|
* — и с этого момента шапка стала вторым местом, задающим одно значение, но
|
||||||
|
* ни на что не влияющим: человек выбирал в шапке Нижний Тагил и всё равно
|
||||||
|
* заполнял город в форме. Правило то же, по которому селектора нет во
|
||||||
|
* внутренней шапке (`_components/InnerHeader.tsx`). Граница покрытия, ради
|
||||||
|
* которой выпадашку когда-то читали, теперь стоит текстом в герое (плашка
|
||||||
|
* «полное покрытие / данных меньше», HeroV3), то есть видна без клика.
|
||||||
|
*
|
||||||
|
* НА УЗКОМ ЭКРАНЕ ШАПКА — ОДНА СТРОКА: лого и «Для бизнеса». Навигация по
|
||||||
|
* секциям и CTA скрыты в CSS (`landing-v3.module.css`, блок шапки) и
|
||||||
|
* возвращаются с той ширины, на которой шапка макета замерена как
|
||||||
|
* однострочная, — а не с общего для файла шага в 720px, где она всё ещё
|
||||||
|
* переносится. На 375px переносилось всё, и липкая шапка занимала 29%
|
||||||
|
* экрана — эти проценты отъедались при любой прокрутке. Куда делись пункты: «Точность»
|
||||||
|
* и «Проверьте себя» — секции этой же страницы, до них доезжают прокруткой
|
||||||
|
* (ради чего на мобильном и скроллят); «Статьи» и «МЕРА для бизнеса» стоят
|
||||||
|
* ссылками в подвале (`FooterV3`); «Продажа под ключ» и на десктопе не
|
||||||
|
* ссылка, а заглушка. CTA не потерян: снизу висит `StickyCtaV3` — на экране
|
||||||
|
* остаётся ОДИН призыв вместо двух одинаковых.
|
||||||
|
*
|
||||||
* НЕАКТИВНЫЕ ПУНКТЫ (29.08.2026). Осталась одна «Продажа под ключ» —
|
* НЕАКТИВНЫЕ ПУНКТЫ (29.08.2026). Осталась одна «Продажа под ключ» —
|
||||||
* партнёрской формы нет ни в коде, ни у владельца, и пункт остаётся видимым,
|
* партнёрской формы нет ни в коде, ни у владельца, и пункт остаётся видимым,
|
||||||
* но некликабельным. «Проверьте себя» и «Статьи» раньше стояли рядом с ней по
|
* но некликабельным. «Проверьте себя» и «Статьи» раньше стояли рядом с ней по
|
||||||
|
|
@ -21,16 +45,14 @@
|
||||||
* «Для бизнеса» ведёт на АБСОЛЮТНЫЙ адрес B2B-контура: относительный «/» на
|
* «Для бизнеса» ведёт на АБСОЛЮТНЫЙ адрес B2B-контура: относительный «/» на
|
||||||
* публичном домене вёл на сам лэндинг, то есть пункт не работал вовсе.
|
* публичном домене вёл на сам лэндинг, то есть пункт не работал вовсе.
|
||||||
*
|
*
|
||||||
* Единственный клиентский остров — `CityPicker` (открытие/закрытие, поиск).
|
* Шапка целиком серверная: клиентских островов в ней не осталось (последним
|
||||||
* Сама шапка серверная.
|
* был `CityPicker`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { B2B_URL } from "../../content";
|
import { B2B_URL } from "../../content";
|
||||||
import styles from "../../landing-v3.module.css";
|
import styles from "../../landing-v3.module.css";
|
||||||
import { PublicLink } from "../PublicLink";
|
import { PublicLink } from "../PublicLink";
|
||||||
|
|
||||||
import { CityPicker } from "./CityPicker";
|
|
||||||
|
|
||||||
export function HeaderV3() {
|
export function HeaderV3() {
|
||||||
return (
|
return (
|
||||||
<header className={styles.hdrRoot}>
|
<header className={styles.hdrRoot}>
|
||||||
|
|
@ -65,7 +87,6 @@ export function HeaderV3() {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className={styles.hdrRight}>
|
<div className={styles.hdrRight}>
|
||||||
<CityPicker />
|
|
||||||
<a href={B2B_URL} className={styles.hdrBizLink}>
|
<a href={B2B_URL} className={styles.hdrBizLink}>
|
||||||
Для бизнеса
|
Для бизнеса
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,16 @@
|
||||||
* того, куда именно в дерево страницы его вставят (ожидаемое место — сразу
|
* того, куда именно в дерево страницы его вставят (ожидаемое место — сразу
|
||||||
* после подвала, как Header/Footer в текущем `layout.tsx`).
|
* после подвала, как Header/Footer в текущем `layout.tsx`).
|
||||||
*
|
*
|
||||||
|
* НА УЗКОМ ЭКРАНЕ ПАНЕЛЬ — ТОЛЬКО КНОПКА (29.08.2026). Строка «Узнайте цену
|
||||||
|
* своей квартиры…» на 375px переносилась и панель занимала 13% экрана, а
|
||||||
|
* вместе с шапкой хром съедал 42% при любой прокрутке. Текст скрыт в CSS
|
||||||
|
* (`.barText`), а не удалён: с той же ширины, что возвращает шапку макета
|
||||||
|
* (1000px — там строка и кнопка встают рядом), панель снова целиком. То же
|
||||||
|
* обещание на самой странице остаётся словами героя («Бесплатно: сколько
|
||||||
|
* похожих квартир продаётся рядом и как давно они висят»), то есть уходит
|
||||||
|
* повтор, а не мысль. Призыв на мобильном теперь ОДИН: `HeaderV3` свою
|
||||||
|
* кнопку на этой ширине не показывает.
|
||||||
|
*
|
||||||
* НА КНОПКЕ НЕТ СРОКА. Здесь стояло «Проверить за 30 секунд» — замера времени
|
* НА КНОПКЕ НЕТ СРОКА. Здесь стояло «Проверить за 30 секунд» — замера времени
|
||||||
* заполнения формы нет и не было (та же причина, по которой «30 СЕКУНД» ушло
|
* заполнения формы нет и не было (та же причина, по которой «30 СЕКУНД» ушло
|
||||||
* из бейджа первого шага, см. `landing-facts.ts`). Панель висит на экране
|
* из бейджа первого шага, см. `landing-facts.ts`). Панель висит на экране
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,15 @@
|
||||||
*
|
*
|
||||||
* ------------------------------------------------------------------------
|
* ------------------------------------------------------------------------
|
||||||
* ЭТОТ БЛОК (шапка / подвал / sticky-CTA) — префиксы hdr / ftr / bar.
|
* ЭТОТ БЛОК (шапка / подвал / sticky-CTA) — префиксы hdr / ftr / bar.
|
||||||
* Секция «Шапка» = HeaderV3.tsx + CityPicker.tsx.
|
* Секция «Шапка» = HeaderV3.tsx (селектор города из неё убран 29.08.2026:
|
||||||
|
* город спрашивает сама форма проверки, FreeCheckCard).
|
||||||
* Секция «Подвал» = FooterV3.tsx.
|
* Секция «Подвал» = FooterV3.tsx.
|
||||||
* Секция «Sticky CTA» = StickyCtaV3.tsx.
|
* Секция «Sticky CTA» = StickyCtaV3.tsx.
|
||||||
* ------------------------------------------------------------------------
|
* ------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
* Шапка (HeaderV3 + CityPicker)
|
* Шапка (HeaderV3)
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
.hdrRoot {
|
.hdrRoot {
|
||||||
|
|
@ -40,11 +41,15 @@
|
||||||
border-bottom: 1px solid var(--b2c-line);
|
border-bottom: 1px solid var(--b2c-line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* База — узкий экран: в шапке остаются только лого и «Для бизнеса», они
|
||||||
|
помещаются в одну строку, поэтому и высота узкая (`flex-wrap` тут страховка,
|
||||||
|
а не режим работы). Навигация и CTA возвращаются в @media-блоке в конце
|
||||||
|
секции. */
|
||||||
.hdrInner {
|
.hdrInner {
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding: 10px clamp(20px, 5vw, 56px);
|
padding: 8px clamp(20px, 5vw, 56px);
|
||||||
min-height: 64px;
|
min-height: 52px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px 26px;
|
gap: 14px 26px;
|
||||||
|
|
@ -101,7 +106,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.hdrNav {
|
.hdrNav {
|
||||||
display: flex;
|
/* База — узкий экран, где навигации по секциям НЕТ: четыре пункта
|
||||||
|
переносились в две строки и вместе с остальным делали липкую шапку в 29%
|
||||||
|
экрана. Куда делись пункты — в шапке HeaderV3.tsx. */
|
||||||
|
display: none;
|
||||||
gap: 10px 22px;
|
gap: 10px 22px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 14.5px;
|
font-size: 14.5px;
|
||||||
|
|
@ -119,8 +127,9 @@
|
||||||
color: var(--b2c-accent-text);
|
color: var(--b2c-accent-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* «Статьи» — раздела нет в коде (ни маршрута, ни контента): пункт нарочно
|
/* «Продажа под ключ» — раздела нет в коде (ни маршрута, ни контента): пункт
|
||||||
неинтерактивен (aria-disabled, без href), а не ссылка в 404. */
|
нарочно неинтерактивен (aria-disabled, без href), а не ссылка в 404.
|
||||||
|
«Статьи» стояли здесь же, пока не появился `articles/`. */
|
||||||
.hdrNavDisabled {
|
.hdrNavDisabled {
|
||||||
color: var(--b2c-muted);
|
color: var(--b2c-muted);
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
|
|
@ -164,7 +173,11 @@
|
||||||
color: var(--b2c-on-accent);
|
color: var(--b2c-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* На узком экране призыв в шапке скрыт: он вёл туда же, куда липкая нижняя
|
||||||
|
панель (`.barRoot`), и два одинаковых призыва на одном экране — это ровно то
|
||||||
|
«много всплывашек», на которое жалуются. Панель остаётся, шапка отдаёт. */
|
||||||
.hdrCta {
|
.hdrCta {
|
||||||
|
display: none;
|
||||||
padding: 11px 22px;
|
padding: 11px 22px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
@ -174,146 +187,24 @@
|
||||||
filter: brightness(0.92);
|
filter: brightness(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- Селектор города (CityPicker) ---------------------------------------- */
|
/* Шапка макета целиком — с той ширины, на которой она РЕАЛЬНО влезает в одну
|
||||||
|
строку (замерено: 1000px — лого, четыре пункта, «Для бизнеса» и кнопка;
|
||||||
|
на 720px то же самое переносится в две строки и даёт 109px липкого хрома,
|
||||||
|
то есть ровно тот дефект, от которого узкий экран и лечили). Отсюда шаг
|
||||||
|
1000, а не общий для файла 720. */
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.hdrInner {
|
||||||
|
padding-block: 10px;
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
.hdrCityWrap {
|
.hdrNav {
|
||||||
position: relative;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hdrCityBtn {
|
.hdrCta {
|
||||||
display: flex;
|
display: inline-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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
@ -472,16 +363,10 @@
|
||||||
/* Компенсация fixed-позиционирования `.barRoot` (тот вырезан из потока
|
/* Компенсация fixed-позиционирования `.barRoot` (тот вырезан из потока
|
||||||
документа): без спейсера панель легла бы поверх последних пикселей
|
документа): без спейсера панель легла бы поверх последних пикселей
|
||||||
страницы при докрутке до конца (реквизиты в подвале, последний вопрос
|
страницы при докрутке до конца (реквизиты в подвале, последний вопрос
|
||||||
FAQ). Высота — на мобильном тексте+кнопка чаще переносятся на две строки
|
FAQ). Высота одна на все ширины: панель везде в одну строку — на узком
|
||||||
(flex-wrap), поэтому база выше; на 720px+ строка одна. */
|
экране за счёт того, что текст скрыт (см. `.barText`), а не переносится. */
|
||||||
.barSpacer {
|
.barSpacer {
|
||||||
height: 96px;
|
height: 68px;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 720px) {
|
|
||||||
.barSpacer {
|
|
||||||
height: 68px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.barRoot {
|
.barRoot {
|
||||||
|
|
@ -495,14 +380,20 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px 24px;
|
gap: 16px 24px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 12px clamp(16px, 4vw, 32px);
|
padding: 10px clamp(16px, 4vw, 32px);
|
||||||
/* Фоллбэк на непрозрачный surfaceDark для браузеров без color-mix(). */
|
/* Фоллбэк на непрозрачный surfaceDark для браузеров без color-mix(). */
|
||||||
background: var(--b2c-surface-dark);
|
background: var(--b2c-surface-dark);
|
||||||
background: color-mix(in srgb, var(--b2c-surface-dark) 97%, transparent);
|
background: color-mix(in srgb, var(--b2c-surface-dark) 97%, transparent);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* На узком экране панель — только кнопка. Строка «Узнайте цену своей
|
||||||
|
квартиры…» переносилась на две-три строки и делала постоянную панель втрое
|
||||||
|
выше кнопки; то же самое сказано словами в герое (заголовок, лид и перк
|
||||||
|
«Бесплатно: сколько похожих квартир продаётся рядом»), то есть со страницы
|
||||||
|
мысль не пропадает — пропадает её третий повтор поверх контента. */
|
||||||
.barText {
|
.barText {
|
||||||
|
display: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
@ -518,6 +409,18 @@
|
||||||
font-size: 14.5px;
|
font-size: 14.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Панель макета целиком — с той же ширины, что и шапка (1000px): ниже неё
|
||||||
|
текст и кнопка тоже переносились, и панель становилась вдвое выше. */
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.barRoot {
|
||||||
|
padding-block: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barText {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
* HERO (hero*) — первый экран: герой-текст + карточка «Бесплатная проверка».
|
* HERO (hero*) — первый экран: герой-текст + карточка «Бесплатная проверка».
|
||||||
* Секция «Герой» = HeroV3.tsx + FreeCheckCard.tsx + FreeResultV3.tsx.
|
* Секция «Герой» = HeroV3.tsx + FreeCheckCard.tsx + FreeResultV3.tsx.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue