diff --git a/frontend/src/app/analytics/page.tsx b/frontend/src/app/analytics/page.tsx
index 1df9aa92..bd76d6f6 100644
--- a/frontend/src/app/analytics/page.tsx
+++ b/frontend/src/app/analytics/page.tsx
@@ -118,6 +118,7 @@ export default function SverdlMarketPage() {
+
+ {/* Header */}
+
+
+ ← GenDesign
+
+
+ Источники данных и атрибуция
+
+
+ GenDesign объединяет открытые и лицензированные источники
+ пространственных и рыночных данных. Ниже — перечень источников,
+ режим их использования и требуемая атрибуция. Картографическая
+ подложка предоставлена участниками OpenStreetMap по лицензии ODbL.
+
+ Перечень носит информационный характер и отражает текущий состав
+ используемых источников. По вопросам условий использования и
+ лицензирования данных — свяжитесь с командой GenDesign.
+
+
+
+ );
+}
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 13046cd8..53bbea46 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -88,3 +88,114 @@ body {
a {
color: inherit;
}
+
+/*
+ * ── Responsive (issue #66) ───────────────────────────────────────────────────
+ * 3 контекста (НЕ Tailwind sm/md/lg): phone <768, tablet 768–1280, desktop >1280.
+ * Утилиты ниже — единственный источник правды для брейкпоинтов. Компоненты
+ * подключаются через className; инлайн-стили остаются desktop-видом по умолчанию.
+ */
+
+/* Никакого горизонтального скролла на узких экранах (acceptance #66). */
+html,
+body {
+ max-width: 100%;
+ overflow-x: hidden;
+}
+
+/* Sidebar-grid (напр. "240px 1fr" / "minmax(300px,360px) 1fr"): на планшете
+ * и ниже схлопывается в одну колонку, сайдбар уезжает над контентом. */
+@media (max-width: 1024px) {
+ .gd-grid-sidebar {
+ grid-template-columns: 1fr !important;
+ }
+ /* sticky-сайдбар внутри схлопнутого грида не должен «прилипать». */
+ .gd-grid-sidebar > .gd-sidebar {
+ position: static !important;
+ top: auto !important;
+ height: auto !important;
+ }
+}
+
+/* Карта/контент рядом с фиксированным сайдбаром: на планшете в колонку. */
+@media (max-width: 1024px) {
+ .gd-split {
+ flex-direction: column !important;
+ overflow: visible !important;
+ }
+ .gd-split > .gd-split-aside {
+ width: 100% !important;
+ flex-shrink: 1 !important;
+ /* в desktop у сайдбара не было левого паддинга (он стоял справа от карты);
+ * в колонке возвращаем симметрию. */
+ padding-left: 16px !important;
+ padding-top: 0 !important;
+ }
+}
+
+/* Nav-tabs: на телефоне — горизонтальный скролл со снапом, НЕ hamburger. */
+@media (max-width: 767px) {
+ .gd-nav-scroll {
+ overflow-x: auto;
+ flex-wrap: nowrap;
+ scroll-snap-type: x proximity;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .gd-nav-scroll::-webkit-scrollbar {
+ display: none;
+ }
+ .gd-nav-scroll > * {
+ flex: 0 0 auto;
+ scroll-snap-align: start;
+ }
+}
+
+/* Таблицы с фиксированной шириной колонок — оборачивать в .gd-table-scroll. */
+.gd-table-scroll {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* KPI-grid: на телефоне одна колонка во всю ширину (ui-conventions). */
+@media (max-width: 767px) {
+ .gd-kpi-grid {
+ grid-template-columns: 1fr !important;
+ }
+}
+
+/* Карта Leaflet на узких экранах — гарантированная высота, чтобы не
+ * схлопнулась в 0 когда flex-родитель встаёт в колонку. */
+@media (max-width: 1024px) {
+ .gd-map-shell {
+ min-height: 420px;
+ }
+}
+
+/* Section-nav сайдбара анализа: на планшете/телефоне (когда грид схлопнут)
+ * вертикальный список превращается в горизонтальную скролл-полоску со снапом —
+ * чтобы не съедать пол-экрана высоты над контентом (issue #66). */
+@media (max-width: 1024px) {
+ .gd-section-nav {
+ flex-direction: row !important;
+ overflow-x: auto;
+ padding: 8px 4px !important;
+ scroll-snap-type: x proximity;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .gd-section-nav::-webkit-scrollbar {
+ display: none;
+ }
+ .gd-section-nav > * {
+ display: flex;
+ flex: 0 0 auto;
+ scroll-snap-align: start;
+ }
+ .gd-section-nav button {
+ width: auto !important;
+ white-space: nowrap;
+ padding-left: 12px !important;
+ padding-right: 12px !important;
+ }
+}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 066457b1..a2f43082 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -699,6 +699,12 @@ export default function HomePage() {
>
ПКК Росреестра
+
+ Источники данных
+ GenDesign {new Date().getFullYear()}
diff --git a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx
index 68ca08ea..f80f6349 100644
--- a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx
+++ b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx
@@ -134,8 +134,10 @@ export function AnalysisPageContent({ cad }: Props) {
/>
- {/* Page layout: sidebar (240px) + main scroll */}
+ {/* Page layout: sidebar (240px) + main scroll. На планшете/телефоне
+ .gd-grid-sidebar схлопывает грид в одну колонку (issue #66). */}
{/* ── Sidebar nav ───────────────────────────────────────────────── */}
- {/* Main layout */}
+ {/* Main layout. На планшете/телефоне .gd-split встаёт в колонку
+ (карта над сайдбаром), .gd-map-shell держит высоту карты (issue #66). */}
{/* Map area — position: relative so ParcelDrawer can anchor absolutely here */}
@@ -30,12 +31,13 @@ export function AnalyticsNav() {
style={{
padding: "10px 14px",
borderBottom: active
- ? "2px solid #1d4ed8"
+ ? "2px solid var(--accent)"
: "2px solid transparent",
- color: active ? "#1d4ed8" : "#374151",
+ color: active ? "var(--accent)" : "var(--fg-secondary)",
fontWeight: active ? 600 : 500,
textDecoration: "none",
fontSize: 14,
+ whiteSpace: "nowrap",
}}
>
{tab.label}
diff --git a/frontend/src/components/analytics/ChartShell.tsx b/frontend/src/components/analytics/ChartShell.tsx
index b20d5fc3..bd4be921 100644
--- a/frontend/src/components/analytics/ChartShell.tsx
+++ b/frontend/src/components/analytics/ChartShell.tsx
@@ -1,7 +1,9 @@
"use client";
import dynamic from "next/dynamic";
+import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
+import type { EChartsInstance } from "echarts-for-react";
const ReactECharts = dynamic(() => import("echarts-for-react"), { ssr: false });
@@ -20,13 +22,37 @@ export function ChartShell({
loading,
notMerge,
}: Props) {
+ const wrapRef = useRef(null);
+ const instanceRef = useRef(null);
+
+ // ECharts canvas only reflows on window.resize by default. On mobile/tablet
+ // the chart container width changes via CSS (sidebar grid collapse, flex
+ // column) WITHOUT a window resize, so the canvas keeps its desktop width and
+ // overflows. A ResizeObserver on the wrapper resizes the instance whenever its
+ // box changes — covers every chart that goes through ChartShell (issue #66).
+ // The instance is captured via onChartReady (a plain prop) rather than a ref,
+ // because next/dynamic does not forward refs to the wrapped component.
+ useEffect(() => {
+ const el = wrapRef.current;
+ if (el == null || typeof ResizeObserver === "undefined") return;
+
+ const ro = new ResizeObserver(() => {
+ instanceRef.current?.resize();
+ });
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, []);
+
return (
-