diff --git a/tradein-mvp/frontend/src/app/mera-public/__tests__/sticky-cta-hero.test.tsx b/tradein-mvp/frontend/src/app/mera-public/__tests__/sticky-cta-hero.test.tsx
new file mode 100644
index 00000000..e14c8459
--- /dev/null
+++ b/tradein-mvp/frontend/src/app/mera-public/__tests__/sticky-cta-hero.test.tsx
@@ -0,0 +1,92 @@
+/**
+ * Липкая панель не должна лежать поверх первого экрана.
+ *
+ * Владелец увидел на проде, что полоса накрывает абзац «Мы называем реальную
+ * цену вашей квартиры». `.barSpacer` это не лечит и не может: он добавляет
+ * высоту в КОНЕЦ документа, а `.barRoot` — `fixed`, то есть накрывает нижние
+ * ~67px вьюпорта при любой прокрутке, включая самый верх страницы.
+ *
+ * Проверяется ПОВЕДЕНИЕ, а не наличие класса в файле: наблюдателю скармливается
+ * пересечение и проверяется, что панель убралась, затем обратное.
+ */
+import { render, screen } from "@testing-library/react";
+import { act } from "react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { StickyCtaV3 } from "../_components/v3/StickyCtaV3";
+
+let срабатывание: ((entries: { isIntersecting: boolean }[]) => void) | null = null;
+
+beforeEach(() => {
+ срабатывание = null;
+ vi.stubGlobal(
+ "IntersectionObserver",
+ class {
+ constructor(cb: (entries: { isIntersecting: boolean }[]) => void) {
+ срабатывание = cb;
+ }
+ observe() {}
+ disconnect() {}
+ },
+ );
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+/**
+ * Корень панели ищем ПО КОНТЕЙНЕРУ, а не через getByRole: когда панель убрана,
+ * на ней стоит aria-hidden, и роль «ссылка» из дерева доступности пропадает —
+ * это и есть желаемое поведение, но локатор по роли на нём слепнет.
+ */
+function корень(c: HTMLElement): HTMLElement {
+ const link = c.querySelector('a[href*="estimate"]');
+ if (!link?.parentElement) throw new Error("панель не отрендерилась");
+ return link.parentElement;
+}
+
+const скрыта = (c: HTMLElement) => /barHidden/u.test(корень(c).className);
+
+describe("липкая панель против первого экрана", () => {
+ it("без секции героя панель остаётся видимой, а не пропадает", () => {
+ // Наблюдать нечего — поведение обязано остаться сегодняшним.
+ const { container } = render();
+ expect(скрыта(container)).toBe(false);
+ });
+
+ it("герой в кадре — панель убрана и недоступна с клавиатуры", () => {
+ const hero = document.createElement("section");
+ hero.id = "check";
+ document.body.appendChild(hero);
+ const { container } = render();
+
+ expect(скрыта(container), "до срабатывания панель видима").toBe(false);
+
+ act(() => срабатывание!([{ isIntersecting: true }]));
+ expect(скрыта(container)).toBe(true);
+ expect(корень(container).getAttribute("aria-hidden")).toBe("true");
+
+ // Убранная панель обязана уйти и из дерева доступности — иначе Tab
+ // уводит в кнопку, которой на экране нет.
+ expect(screen.queryByRole("link", { name: /Проверить квартиру/u })).toBeNull();
+
+ hero.remove();
+ });
+
+ it("герой ушёл — панель возвращается", () => {
+ const hero = document.createElement("section");
+ hero.id = "check";
+ document.body.appendChild(hero);
+ const { container } = render();
+
+ act(() => срабатывание!([{ isIntersecting: true }]));
+ expect(скрыта(container)).toBe(true);
+
+ act(() => срабатывание!([{ isIntersecting: false }]));
+ expect(скрыта(container), "панель не вернулась — призыв потерян навсегда").toBe(false);
+ expect(screen.getByRole("link", { name: /Проверить квартиру/u })).toBeTruthy();
+
+ hero.remove();
+ });
+});
diff --git a/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx b/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx
index 21f1be7d..87e8dc4e 100644
--- a/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx
+++ b/tradein-mvp/frontend/src/app/mera-public/_components/v3/StickyCtaV3.tsx
@@ -1,3 +1,5 @@
+"use client";
+
/**
* StickyCtaV3 — липкая нижняя CTA-панель (макет ~ строки 518-521).
*
@@ -32,17 +34,64 @@
* чаще всего. В тексте панели по той же причине нет и «срока продажи»:
* платный отчёт отдаёт медиану экспозиции похожих объявлений, а не прогноз
* срока (`_estimate_days_on_market`, см. HeroV3).
+ *
+ * ПАНЕЛЬ ПРЯЧЕТСЯ, ПОКА ГЕРОЙ В КАДРЕ (31.08.2026). Владелец увидел на первом
+ * экране, что полоса лежит поверх абзаца «Мы называем реальную цену вашей
+ * квартиры». `.barSpacer` это НЕ лечит и не может: он добавляет высоту в
+ * КОНЕЦ документа, а `.barRoot` — `fixed`, то есть накрывает нижние ~67px
+ * вьюпорта при ЛЮБОЙ прокрутке, включая `scrollTop = 0`.
+ *
+ * Пока герой виден, панель не нужна: прямо в нём стоит та же форма проверки,
+ * и панель дублирует призыв, закрывая текст. Уходит герой — панель появляется.
+ *
+ * ПО УМОЛЧАНИЮ ПАНЕЛЬ ВИДИМА, и это важно. Скрытие включает только клиентский
+ * наблюдатель; если скрипт не выполнился (ошибка, старый браузер, отключённый
+ * JS), поведение остаётся ровно сегодняшним — полоса на месте. Обратный
+ * порядок (по умолчанию скрыта, показывает скрипт) в тех же условиях убрал бы
+ * призыв со страницы совсем, то есть чинил бы вид ценой работы.
+ *
+ * Наблюдатель смотрит на `#check` — секцию героя (`HeroV3`). Если её на
+ * странице нет (компонент вставили в другую композицию), наблюдать нечего и
+ * панель просто остаётся видимой: снова сегодняшнее поведение, а не пустой
+ * экран.
*/
+import { useEffect, useState } from "react";
+
import styles from "../../landing-v3.module.css";
import { PublicLink } from "../PublicLink";
+/** Секция героя: пока она в кадре, панель молчит. */
+const HERO_ID = "check";
+
export function StickyCtaV3() {
+ const [heroVisible, setHeroVisible] = useState(false);
+
+ useEffect(() => {
+ const hero = document.getElementById(HERO_ID);
+ if (!hero || typeof IntersectionObserver === "undefined") return;
+
+ const io = new IntersectionObserver(
+ ([entry]) => setHeroVisible(entry.isIntersecting),
+ // Панель высотой ~67px снизу: считаем героя «в кадре», пока он заходит
+ // хотя бы на эту полосу — иначе она успевала бы мигнуть на его хвосте.
+ { rootMargin: "0px 0px -68px 0px" },
+ );
+ io.observe(hero);
+ return () => io.disconnect();
+ }, []);
+
return (
<>
-
+
Узнайте цену своей квартиры и что происходит с похожими рядом
diff --git a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css
index ee875cf8..2e291e96 100644
--- a/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css
+++ b/tradein-mvp/frontend/src/app/mera-public/landing-v3.module.css
@@ -365,6 +365,24 @@
страницы при докрутке до конца (реквизиты в подвале, последний вопрос
FAQ). Высота одна на все ширины: панель везде в одну строку — на узком
экране за счёт того, что текст скрыт (см. `.barText`), а не переносится. */
+/* Панель убрана, пока герой в кадре (см. докстринг StickyCtaV3).
+ Убирается ТРАНСФОРМОМ, а не display:none: transform идёт на композиторе и
+ не вызывает перекладку. Страница и так дорога в отрисовке на мобильном
+ (1824 мс в Style & Layout), и лечение перекрытия не должно стоить ещё
+ одного пересчёта макета при каждом пересечении границы героя. */
+.barHidden {
+ transform: translateY(100%);
+ opacity: 0;
+ pointer-events: none;
+ transition: transform 180ms ease, opacity 180ms ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .barHidden {
+ transition: none;
+ }
+}
+
.barSpacer {
height: 68px;
}