gendesign/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css
Light1YT dcccebf2c2
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m17s
CI / openapi-codegen-check (pull_request) Successful in 2m11s
CI / backend-tests (pull_request) Successful in 10m19s
fix(forecasting): honest scenarios collapse when β rate-sensitivity gate fails (audit #1871 P1)
Когда demand_normalization honesty-gate отбивает β rate-sensitivity
(`sensitivity.confidence=='low'`, ЕКБ-регрессия не проходит n≥30/R²≥0.1/slope<0),
все 393 прод-отчёта получают coefficient=1.0 для всех трёх сценариев
conservative/base/aggressive. По дизайну backend честно деградирует, но фронт
рисует 3 разноцветные карточки/линии одинаковых чисел без caveat.

Backend (scenarios.py, report.py, report_assembler.py, orchestrator.py):
- compute_scenarios теперь возвращает (list, collapsed: bool, reason: str|None)
- _detect_collapsed(): math.isclose(rel_tol=1e-9, abs_tol=1e-6) сравнивает
  projected_demand_units И deficit_index на всех горизонтах между
  conservative и aggressive — расхождение в любой метрике на любом h → False
- _COLLAPSE_REASON_LOW_BETA — единственный источник истины каноничного
  русского предложения
- ReportScenarios получает поля scenarios_collapsed + scenarios_collapse_reason
- demand_normalization.py НЕ ТРОНУТ — поведение корректно по контракту

Backend экспортёры (report_pdf.py, excel.py):
- PDF: при collapse — тёмная плашка-параграф вместо таблицы трёх столбцов
- Excel: ОДНА строка «base» + caveat-ячейка вместо трёх идентичных столбцов

Frontend (forecast.ts, ScenariosBlock, ScenarioCards, ScenarioCompareTable,
ForecastChart, Section6Forecast, ptica.module.css):
- Тип ReportScenarios расширен двумя optional-полями
- ScenariosBlock (light): headline-bar + одна grid-карточка base + caveat
- ScenarioCards (ptica dark): sub-component ScenarioBaseCard, одна карточка
  + collapse-note, ScenarioCompareTable return null
- ForecastChart: effectiveScenarios filter — одна линия viz-1 «Базовый»
  вместо трёх перекрывающихся
- Section6Forecast: caveat выше графика, читается ТОЛЬКО из
  scenarios_collapse_reason (источник истины — backend constant)

Тесты: +18 новых (TestMetricsEqual×6, TestDetectCollapsed×7,
TestComputeScenariosCollapseDetection×4 + 1 проверка graceful + corner).
102 forecasting passed, 88 cross-module passed.

Refs #1871
2026-06-22 19:06:22 +05:00

2827 lines
58 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* ПТИЦА cockpit — dark "operator terminal" styles (INCREMENT 1).
*
* Ported from the prototype tokens.css + styles.css. Every design token is
* scoped under `.pticaRoot[data-theme="dark"]` (the wrapper carries BOTH the
* class and the attribute) so these dark vars NEVER leak onto the existing
* light analysis pages. Cockpit class styles reference tokens via var(--…).
*/
/* ===================== SCOPED TOKENS (dark) ===================== */
.pticaRoot[data-theme="dark"] {
--font-ui:
var(--font-inter), "Inter", "Manrope", -apple-system, "Segoe UI",
system-ui, sans-serif;
--font-mono:
var(--font-plex-mono), "IBM Plex Mono", "Roboto Mono", ui-monospace,
monospace;
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--line: 1px;
--line-strong: 1.5px;
/* accents */
--accent-blue: #2f6f9f;
--accent-cyan: #7fd0ee;
--accent-cyan-strong: #8ed3ff;
--accent-green: #38c172;
--accent-yellow: #e0a93b;
--accent-red: #d2655b;
--accent-purple: #9b80d6;
--accent-orange: #e46c35;
/* resource (connection-point) accents — ported from prototype tokens.css */
--res-electric: #e6b23c;
--res-water: #4d9bd7;
--res-sewer: #9b80d6;
--res-heat: #e46c35;
--res-gas: #d2655b;
--res-poi: #3fa77d;
--gauge-track: 9px;
/* dark surface palette */
--bg: #060f16;
--bg-2: #08131c;
--grid-line: rgba(120, 165, 190, 0.06);
--surface: rgba(11, 26, 36, 0.72);
--surface-2: rgba(9, 22, 31, 0.62);
--surface-strong: rgba(12, 30, 42, 0.94);
--surface-muted: rgba(26, 52, 67, 0.55);
--surface-inset: rgba(4, 12, 18, 0.6);
--text: #dcebf2;
--text-strong: #f2fafe;
--text-muted: #8ba6b3;
--text-soft: #5f7886;
--border: rgba(150, 192, 214, 0.18);
--border-strong: rgba(150, 192, 214, 0.4);
--border-faint: rgba(150, 192, 214, 0.1);
--map-bg: #0a1820;
--map-filter: saturate(0.7) contrast(1.12) brightness(0.7);
--map-filter-sat: brightness(0.62) saturate(0.8) contrast(1.05);
--glow: rgba(110, 200, 240, 0.28);
--glow-strong: rgba(110, 200, 240, 0.55);
--shadow: 0 18px 70px rgba(0, 0, 0, 0.5);
--gauge-good: var(--accent-green);
/* root paint */
position: relative;
min-height: 100vh;
font-family: var(--font-ui);
color: var(--text);
font-size: 13px;
line-height: 1.4;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
background:
linear-gradient(90deg, transparent 31px, var(--grid-line) 32px),
linear-gradient(0deg, transparent 31px, var(--grid-line) 32px),
radial-gradient(circle at 78% -10%, var(--glow), transparent 45%), var(--bg);
background-size:
32px 32px,
32px 32px,
100% 100%,
100% 100%;
}
/* `:where(button)` keeps specificity at (0,1,0) so per-component button classes
(.navItem 9px, .tab 11px, .detailBtn 9px, .ptab …) win their font-size. With a
bare `.pticaRoot button` (0,1,1) the `font: inherit` reset beat those classes
and forced every cockpit button back to the root 13px — overflowing the 96px
rail and oversizing tabs/buttons. Matches the prototype's bare `button {}`. */
.pticaRoot :where(button) {
font: inherit;
cursor: pointer;
color: inherit;
}
.pticaRoot svg {
display: block;
}
.mono {
font-family: var(--font-mono);
}
/* ===================== APP SHELL ===================== */
.shell {
display: grid;
grid-template-columns: 96px 1fr;
min-height: 100vh;
}
/* sidebar rail */
.rail {
border-right: var(--line) solid var(--border);
background: var(--surface-2);
backdrop-filter: blur(8px);
display: flex;
flex-direction: column;
padding: 16px 10px 12px;
gap: 14px;
/* Prototype .sidebar is a plain flex grid-child that stretches to the FULL
page height (footer pinned via .navFoot margin-top:auto). The old
`position:sticky; top:0; height:100vh` capped the rail strip at one viewport
so on tall pages it looked «обрезан» partway down. Drop it — the rail now
stretches to the full .shell grid height like the prototype. */
}
.brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding-bottom: 6px;
}
.brandMark {
width: 40px;
height: 40px;
color: var(--accent-cyan);
filter: drop-shadow(0 0 10px var(--glow));
}
.nav {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
}
.navItem {
position: relative;
display: grid;
place-items: center;
gap: 6px;
min-height: 60px;
padding: 8px 4px;
border: var(--line) solid transparent;
border-radius: var(--radius-sm);
color: var(--text-soft);
background: transparent;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.05em;
text-align: center;
transition:
color 0.15s,
background 0.15s;
}
.navItem svg {
width: 22px;
height: 22px;
}
.navItem:hover {
color: var(--text-muted);
background: var(--surface);
}
/* Active rail item — cyan accent + inset glow + left accent bar (prototype
.nav-item.active / .nav-item.active::before). */
.navItemActive,
.navItem.navItemActive {
color: var(--accent-cyan);
background: var(--surface);
border-color: var(--border);
box-shadow: inset 0 0 22px var(--glow);
}
.navItemActive::before {
content: "";
position: absolute;
left: -10px;
top: 14px;
bottom: 14px;
width: 2px;
background: var(--accent-cyan);
box-shadow: 0 0 10px var(--accent-cyan);
}
.navFoot {
margin-top: auto;
display: grid;
gap: 8px;
justify-items: center;
color: var(--text-soft);
}
.navFoot .mono {
font-size: 8px;
text-align: center;
line-height: 1.5;
}
/* Rail nav-foot help-dot (?) button (prototype .help-dot). */
.helpDot {
width: 24px;
height: 24px;
border-radius: 50%;
border: var(--line) solid var(--border-strong);
display: grid;
place-items: center;
font-size: 11px;
background: transparent;
color: var(--text-soft);
cursor: pointer;
}
/* main column */
.main {
display: flex;
flex-direction: column;
min-width: 0;
}
/* topbar */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 24px;
padding: 12px 20px;
border-bottom: var(--line) solid var(--border);
background: var(--surface-2);
backdrop-filter: blur(8px);
}
.wordmark {
display: flex;
align-items: center;
gap: 12px;
/* Wordmark is the «На главную» link — visibly clickable, inherits cockpit
colors (no default link blue/underline). */
cursor: pointer;
text-decoration: none;
color: inherit;
border-radius: var(--radius-xs);
}
/* Visible focus ring on the home link — matches cockpit pattern
(.drawerClose / .gaugeButton :focus-visible). */
.wordmark:focus-visible {
outline: var(--line-strong) solid var(--accent-cyan);
outline-offset: 3px;
}
.bird {
width: 26px;
height: 26px;
color: var(--accent-cyan);
}
.wordmarkTitle {
font-weight: 700;
letter-spacing: 0.42em;
font-size: 18px;
color: var(--text-strong);
}
.wordmarkSub {
color: var(--text-soft);
font-size: 8.5px;
line-height: 1.25;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.tabs {
display: flex;
justify-content: center;
gap: 0;
}
.tab {
padding: 8px 22px;
background: transparent;
border: none;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 11px;
border-bottom: 2px solid transparent;
}
.tab + .tab {
border-left: var(--line) solid var(--border);
}
.tab:hover {
color: var(--text);
}
.tabActive {
color: var(--text-strong);
border-bottom-color: var(--accent-cyan);
}
.sysbar {
display: flex;
align-items: center;
gap: 16px;
color: var(--text-muted);
}
.clock {
font-family: var(--font-mono);
font-size: 12px;
text-align: right;
line-height: 1.3;
}
.clock b {
color: var(--text);
font-weight: 600;
}
/* Topbar sysbar icon-buttons (prototype .icon-btn). */
.iconBtn {
width: 30px;
height: 30px;
border: var(--line) solid transparent;
border-radius: var(--radius-xs);
background: transparent;
display: grid;
place-items: center;
color: var(--text-muted);
}
.iconBtn:hover {
color: var(--text);
background: var(--surface);
border-color: var(--border);
}
.sysbar .iconBtn + .iconBtn {
margin-left: -2px;
}
.iconBtn svg {
width: 16px;
height: 16px;
}
/* canvas (scroll area) */
.canvas {
padding: 14px 18px 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Bottom disclaimer footer bar (prototype .app-footer). */
.appFooter {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 10px 20px 14px;
border-top: var(--line) solid var(--border);
color: var(--text-soft);
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-family: var(--font-mono);
}
/* ===================== PANEL / CARD primitives ===================== */
.panel,
.card,
.mapCard {
position: relative;
background: var(--surface);
border: var(--line) solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow);
backdrop-filter: blur(6px);
}
.panel,
.card {
padding: 14px 15px;
}
.cardHead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.cardTitle {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--text-strong);
margin: 0;
}
.cardTitleSub {
font-size: 9px;
font-weight: 500;
letter-spacing: 0.08em;
color: var(--text-soft);
}
.detailBtn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 11px;
border: var(--line) solid var(--border-strong);
background: transparent;
color: var(--text-muted);
border-radius: var(--radius-xs);
text-transform: uppercase;
font-size: 9px;
letter-spacing: 0.08em;
white-space: nowrap;
}
.detailBtn::after {
content: "→";
font-size: 11px;
}
.detailBtn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
/* «Подробнее» pinned to the bottom-left of a flex-column card (scan / score). */
.detailBtnScan {
margin-top: auto;
align-self: flex-start;
}
/* ===================== HERO GRID ===================== */
.hero {
display: grid;
grid-template-columns: 1.7fr 1.02fr 0.62fr;
gap: 12px;
}
/* map card */
.mapCard {
min-height: 358px;
overflow: hidden;
background: var(--map-bg);
}
.mapMount {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
}
.mapMount :global(.leaflet-container) {
position: absolute;
inset: 0;
/* Contain Leaflet's panes (tile 200 / overlay 400 / marker 600 / popup 700)
in their OWN stacking context below the cockpit overlay (.mapOverlay /
.mapTools at z-index 4) so the legend is not buried under the tiles. */
z-index: 1;
width: 100%;
height: 100%;
background: transparent;
font-family: var(--font-ui);
}
.mapMount :global(.leaflet-tile-pane) {
filter: var(--map-filter);
}
/* satellite base → its own (darker) filter so markers stay readable */
.mapMount.baseSat :global(.leaflet-tile-pane) {
filter: var(--map-filter-sat);
}
/* dark, glassy Leaflet popups (match the cockpit chrome) */
.mapMount :global(.leaflet-popup-content-wrapper) {
background: var(--surface-strong);
color: var(--text);
border: var(--line) solid var(--border);
border-radius: var(--radius-sm);
}
.mapMount :global(.leaflet-popup-tip) {
background: var(--surface-strong);
}
.mapMount :global(.leaflet-popup-content) {
margin: 9px 11px;
font-family: var(--font-ui);
}
.mapMount :global(.leaflet-container a.leaflet-popup-close-button) {
color: var(--text-muted);
}
/* data-driven divIcon pins (resource + POI markers) */
.mapMount :global(.map-pin) {
width: 22px;
height: 22px;
border-radius: 50%;
display: grid;
place-items: center;
color: #08131c;
border: 1.5px solid rgba(255, 255, 255, 0.85);
box-shadow:
0 0 0 3px rgba(0, 0, 0, 0.25),
0 0 14px currentColor;
}
.mapMount :global(.map-pin svg) {
width: 13px;
height: 13px;
}
.mapMount :global(.map-pin.poi-dot) {
width: 14px;
height: 14px;
border-width: 1.5px;
}
.mapMount :global(.parcel-glow) {
filter: drop-shadow(0 0 6px var(--glow-strong));
}
.mapMount :global(.conn-glow) {
filter: drop-shadow(0 0 5px currentColor);
}
.mapMount :global(.leaflet-control-attribution) {
background: rgba(8, 19, 28, 0.55);
color: var(--text-soft);
font-size: 8px;
padding: 1px 6px;
opacity: 0.6;
backdrop-filter: blur(4px);
}
.mapMount :global(.leaflet-control-attribution a) {
color: var(--text-muted);
}
/* Dark-styled zoom +/- control (prototype map controls). */
.mapMount :global(.leaflet-control-zoom) {
border: var(--line) solid var(--border-strong);
border-radius: var(--radius-xs);
overflow: hidden;
box-shadow: 0 0 12px var(--glow);
}
.mapMount :global(.leaflet-control-zoom a) {
background: var(--surface-strong);
color: var(--text);
border-bottom: var(--line) solid var(--border);
width: 28px;
height: 28px;
line-height: 28px;
}
.mapMount :global(.leaflet-control-zoom a:hover) {
background: var(--surface-muted);
color: var(--accent-cyan);
}
.mapMount :global(.leaflet-control-zoom a.leaflet-disabled) {
background: var(--surface-strong);
color: var(--text-soft);
}
/* Metric scale bar. */
.mapMount :global(.leaflet-control-scale-line) {
background: rgba(8, 19, 28, 0.55);
color: var(--text-soft);
border: var(--line) solid var(--border);
border-top: none;
font-size: 9px;
padding: 1px 6px;
backdrop-filter: blur(4px);
}
.mapLoading {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--text-soft);
font-size: 12px;
background: var(--map-bg);
}
/* passport KV-grid */
.kvGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--line);
background: var(--border-faint);
border: var(--line) solid var(--border-faint);
border-radius: var(--radius-sm);
overflow: hidden;
}
.kv {
background: var(--surface-2);
padding: 9px 11px;
}
.kvK {
display: block;
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-soft);
margin-bottom: 3px;
}
.kvV {
font-size: 12.5px;
font-weight: 600;
color: var(--text);
}
.kvV.mono {
font-family: var(--font-mono);
font-size: 12px;
}
.kvWide {
grid-column: span 2;
}
.kvPlaceholder {
color: var(--text-soft);
font-weight: 500;
}
.kvCaption {
display: block;
margin-top: 2px;
font-size: 8px;
text-transform: none;
letter-spacing: 0.02em;
color: var(--text-soft);
}
/* score panel (hero col 3) */
.scorePanel {
display: flex;
flex-direction: column;
gap: 10px;
}
.gaugePanel {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
/* compact, centred title above a radial gauge. */
.cardTitleGauge {
font-size: 9.5px;
text-align: center;
}
.scoreStack {
display: flex;
flex-direction: column;
gap: 8px;
}
.statLine {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
}
.statLine .k {
color: var(--text-soft);
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 9px;
}
.statLine .v {
font-weight: 600;
}
.investScore {
display: flex;
align-items: baseline;
gap: 6px;
}
.investBig {
font-size: 26px;
font-weight: 700;
color: var(--text-strong);
font-family: var(--font-mono);
}
.investSmall {
color: var(--text-soft);
font-size: 11px;
}
.investPlaceholder {
color: var(--text-soft);
}
/* Hero stat-line semantic value colors (prototype .v.good / .v.warn). */
.vGood {
color: var(--accent-green);
}
.vWarn {
color: var(--accent-yellow);
}
/* Hero invest-score sparkline (prototype .sparkline). */
.sparkline {
display: flex;
align-items: flex-end;
gap: 3px;
height: 26px;
}
.sparkline span {
flex: 1;
background: linear-gradient(var(--accent-cyan), var(--accent-blue));
border-radius: 1px;
min-height: 3px;
opacity: 0.85;
}
.badge {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
border: var(--line) solid var(--border-strong);
color: var(--text-soft);
}
.fieldCaption {
font-size: 8px;
color: var(--text-soft);
text-transform: none;
letter-spacing: 0.02em;
}
/* ===================== GAUGE ===================== */
.gauge {
position: relative;
width: 122px;
height: 122px;
margin: 2px auto;
}
.gauge svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.gaugeTrack {
fill: none;
stroke: var(--surface-muted);
}
.gaugeProg {
fill: none;
stroke-linecap: round;
filter: drop-shadow(0 0 6px var(--glow));
transition: stroke-dashoffset 0.9s ease;
}
.gaugeGood {
stroke: var(--accent-green);
}
.gaugeWarn {
stroke: var(--accent-yellow);
}
.gaugeBad {
stroke: var(--accent-red);
}
.gaugeNeutral {
stroke: var(--accent-cyan);
}
.gaugeCenter {
position: absolute;
inset: 0;
display: grid;
place-content: center;
text-align: center;
}
.gaugeNum {
font-size: 30px;
font-weight: 700;
line-height: 1;
color: var(--text-strong);
font-family: var(--font-mono);
}
.gaugeNumGood {
color: var(--accent-green);
}
.gaugeNumWarn {
color: var(--accent-yellow);
}
.gaugeNumBad {
color: var(--accent-red);
}
.gaugeLab {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-muted);
margin-top: 5px;
}
.gaugeFootnote {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
}
/* ===================== SCAN GRID ===================== */
.sectionLabel {
display: flex;
align-items: center;
gap: 10px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--text-soft);
margin: 2px 2px -2px;
}
.sectionLabel::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.sectionLabel b {
color: var(--accent-cyan);
font-weight: 600;
letter-spacing: 0.12em;
}
.scanGrid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
}
.scanCard {
padding: 12px 12px 11px;
min-height: 156px;
display: flex;
flex-direction: column;
}
.scanCard .cardTitle {
margin-bottom: 10px;
font-size: 10px;
}
.kvrow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 11px;
padding: 3px 0;
}
.kvrow .k {
color: var(--text-muted);
flex-shrink: 0;
}
.kvrow .v {
font-weight: 600;
color: var(--text);
text-align: right;
/* Long values (e.g. «Стагнация — рынок остыл») wrap gracefully instead of
overflowing the card. */
min-width: 0;
overflow-wrap: anywhere;
}
.kvrowPlaceholder .v {
color: var(--text-soft);
font-weight: 500;
}
.scanCard .detailBtn {
margin-top: auto;
align-self: flex-start;
}
/* Scan status-rows (prototype Инженерия / Экология .status-row): dot + label
+ state. Scoped under .scanCard so they never collide with the unrelated
DRAWER .statusRow variant (drDot / drLabel / drState). */
.scanCard .statusRow {
display: flex;
align-items: center;
gap: 7px;
font-size: 11px;
padding: 3px 0;
}
.scanCard .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 6px currentColor;
color: var(--accent-green);
flex: none;
}
.scanCard .dotWarn {
background: var(--accent-yellow);
color: var(--accent-yellow);
}
.scanCard .dotBad {
background: var(--accent-red);
color: var(--accent-red);
}
.scanCard .label {
flex: 1;
color: var(--text-muted);
}
.scanCard .state {
font-size: 10px;
}
.scanCard .stateOk {
color: var(--accent-green);
}
.scanCard .stateWarn {
color: var(--accent-yellow);
}
/* Risks card centres its gauge; title stays left-aligned like the other cards. */
.scanCardCentered {
align-items: center;
}
.scanCardCentered .cardTitle {
align-self: flex-start;
}
.emptyState {
display: grid;
place-items: center;
flex: 1;
text-align: center;
color: var(--text-soft);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ===================== PLACEHOLDER PANEL ===================== */
.placeholderPanel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
text-align: center;
min-height: 240px;
border-style: dashed;
}
.placeholderPanel .soon {
font-size: 22px;
letter-spacing: 0.12em;
color: var(--text-soft);
font-weight: 700;
text-transform: uppercase;
}
.placeholderPanel p {
font-size: 10px;
color: var(--text-soft);
margin: 0;
max-width: 280px;
}
/* ===================== STATE SCREENS ===================== */
.stateScreen {
display: grid;
place-items: center;
min-height: 60vh;
padding: 40px 24px;
}
.stateBox {
text-align: center;
color: var(--text-muted);
font-size: 14px;
}
.stateError {
border: var(--line) solid var(--accent-red);
background: rgba(210, 101, 91, 0.08);
color: var(--accent-red);
border-radius: var(--radius-md);
padding: 16px 20px;
}
.stateLink {
display: inline-block;
margin-top: 16px;
color: var(--accent-cyan);
text-decoration: none;
font-size: 13px;
}
/* ===================== SCENARIOS (§22 Прогноз) ===================== */
.scenariosRoot {
display: flex;
flex-direction: column;
gap: 12px;
}
/* view header (title + horizon selector) */
.viewHead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.viewHead h2 {
margin: 0;
font-size: 15px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--text-strong);
}
.viewSub {
font-size: 10px;
color: var(--text-soft);
letter-spacing: 0.08em;
margin-left: 8px;
text-transform: none;
}
.horizonSel {
display: flex;
align-items: center;
gap: 8px;
}
.horizonLab {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-soft);
}
.seg {
display: inline-flex;
border: var(--line) solid var(--border);
border-radius: var(--radius-xs);
overflow: hidden;
}
.seg button {
padding: 6px 14px;
background: transparent;
border: none;
color: var(--text-muted);
font-size: 11px;
border-right: var(--line) solid var(--border);
transition:
color 0.15s,
background 0.15s;
}
.seg button:last-child {
border-right: none;
}
.seg button:hover {
color: var(--text);
}
.segActive,
.seg button.segActive {
background: var(--accent-blue);
color: #fff;
}
/* 3 scenario cards */
.scenarioGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
/* #1871 P1 — when backend collapses 3 scenarios into one (failed β
rate-sensitivity gate → demand_normalization coefficient=1.0), the
ScenarioCards grid renders ONE «Базовый» card spanning the full row
plus this caveat note. Token names follow .pticaRoot[data-theme="dark"]
scope (no light --fg-on-dark-muted / --border-soft / --bg-card-alt). */
.scenarioCollapsedNote {
grid-column: 1 / -1;
color: var(--text-soft);
font-size: 11px;
line-height: 1.45;
padding: 10px 14px;
border: var(--line) solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-2);
}
.scenarioCard {
border-top: 2px solid var(--accent-cyan);
}
.scAccentBase {
border-top-color: var(--accent-blue);
}
.scAccentAggr {
border-top-color: var(--accent-green);
}
.scAccentCons {
border-top-color: var(--accent-yellow);
}
.scTag {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-soft);
}
.scDeficit {
font-size: 30px;
font-weight: 700;
font-family: var(--font-mono);
margin: 4px 0;
line-height: 1.05;
}
.deficitPos {
color: var(--accent-green);
}
.deficitNeg {
color: var(--accent-red);
}
.deficitFlat {
color: var(--text);
}
.scKvRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 11px;
padding: 3px 0;
}
/* sc-kvrow that sits below a table/segment block — adds top separation. */
.scKvRowSpaced {
margin-top: 8px;
}
.scK {
color: var(--text-muted);
}
.scV {
font-weight: 600;
color: var(--text);
text-align: right;
}
.scNote {
margin-top: 6px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
}
/* forecast grid (chart + table; confidence + product) */
.forecastGrid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 10px;
}
/* inline legend */
.legendInline {
display: flex;
gap: 14px;
flex-wrap: wrap;
font-size: 10px;
color: var(--text-muted);
}
.legendInline span {
display: inline-flex;
align-items: center;
gap: 6px;
}
.legendInline i {
width: 14px;
height: 3px;
border-radius: 2px;
}
.legendDemand {
background: var(--accent-cyan);
}
.legendSupply {
background: var(--accent-yellow);
}
/* inline SVG line chart */
.chart {
width: 100%;
height: 180px;
}
.chart svg {
width: 100%;
height: 100%;
overflow: visible;
}
.chartGrid {
stroke: var(--border-faint);
stroke-width: 1;
}
.chartDemand {
fill: none;
stroke: var(--accent-cyan);
stroke-width: 2;
}
.chartSupply {
fill: none;
stroke: var(--accent-yellow);
stroke-width: 2;
stroke-dasharray: 4 4;
}
.chartLbl {
fill: var(--text-soft);
font-size: 9px;
font-family: var(--font-mono);
}
/* data table */
.dtable {
width: 100%;
border-collapse: collapse;
font-size: 11px;
}
.dtable th,
.dtable td {
text-align: left;
padding: 8px 9px;
border-bottom: var(--line) solid var(--border-faint);
}
.dtable th {
color: var(--text-soft);
text-transform: uppercase;
font-size: 8.5px;
letter-spacing: 0.06em;
font-weight: 500;
}
.thNum {
text-align: right;
}
.tdNum {
text-align: right;
font-family: var(--font-mono);
}
.dtable tbody tr:hover td {
background: var(--surface-2);
}
.numPos {
color: var(--accent-green);
}
.numNeg {
color: var(--accent-red);
}
.rowTarget td {
background: rgba(47, 111, 159, 0.16);
}
.targetTag {
margin-left: 8px;
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent-cyan);
}
/* confidence rows + pills */
.confRow {
display: flex;
align-items: center;
gap: 10px;
margin: 6px 0;
font-size: 11px;
}
.confName {
flex: 1;
color: var(--text-muted);
}
.confPill {
padding: 2px 9px;
border-radius: 999px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
border: var(--line) solid currentColor;
}
.confPillHigh {
color: var(--accent-green);
}
.confPillMedium {
color: var(--accent-yellow);
}
.confPillLow {
color: var(--accent-red);
}
/* tag (recommended class) */
.tag {
display: inline-block;
padding: 1px 7px;
border-radius: var(--radius-xs);
font-size: 9px;
background: var(--surface-muted);
color: var(--text);
}
/* Recommended Product class-tabs (prototype .product-tabs / .ptab / .active) */
.productTabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.ptab {
padding: 4px 12px;
border: var(--line) solid var(--border);
background: transparent;
color: var(--text-muted);
border-radius: var(--radius-xs);
font-size: 10px;
cursor: pointer;
}
.ptab:hover {
color: var(--text);
border-color: var(--border-strong);
}
.ptabActive,
.ptabActive:hover {
background: var(--accent-blue);
color: var(--text);
border-color: var(--accent-blue);
box-shadow: 0 0 14px var(--glow);
}
/* horizontal bars (квартирография) */
.hbars {
display: grid;
gap: 7px;
}
.hbar {
display: grid;
grid-template-columns: 110px 1fr 56px;
gap: 9px;
align-items: center;
font-size: 11px;
}
.hbarName {
color: var(--text-muted);
/* Keep one row per format — long bucket labels («5-80+ м²») must not wrap to
2 lines and break the name / track / value column alignment across rows. */
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hbarTrack {
height: 5px;
background: var(--surface-muted);
border-radius: 3px;
overflow: hidden;
}
.hbarFill {
height: 100%;
border-radius: 3px;
}
.hbarFillGood {
background: var(--accent-green);
}
.hbarFillBad {
background: var(--accent-red);
}
.hbarFillNeutral {
background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan));
}
.hbarValue {
text-align: right;
font-family: var(--font-mono);
font-weight: 600;
}
/* hints + empty panels */
.panelHint {
margin: 10px 0 0;
font-size: 10px;
line-height: 1.5;
color: var(--text-soft);
}
/* Recommended Product — honest advisory verdict (degenerate-mix case: every
формат «избегать», deficit_index 1.0 → затоварка рынка). Tokens resolve
under .pticaRoot[data-theme="dark"]. */
.verdictPanel {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
}
.signalBadge {
display: inline-flex;
align-self: flex-start;
padding: 2px 9px;
border-radius: 999px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.signalBadgeBad {
border: var(--line) solid var(--accent-red);
background: rgba(210, 101, 91, 0.08);
color: var(--accent-red);
}
.verdictReason {
font-size: 11px;
line-height: 1.4;
color: var(--text);
}
.verdictCaveat {
font-size: 10px;
line-height: 1.5;
color: var(--text-soft);
}
.emptyPanel {
display: grid;
place-items: center;
min-height: 96px;
text-align: center;
color: var(--text-soft);
font-size: 11px;
}
/* pending / polling skeleton (calm, NOT an error) */
.forecastPending {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 260px;
text-align: center;
}
.pendingPulse {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--border-strong);
border-top-color: var(--accent-cyan);
animation: pticaSpin 0.9s linear infinite;
}
@keyframes pticaSpin {
to {
transform: rotate(360deg);
}
}
.pendingTitle {
font-size: 16px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
.pendingHint {
margin: 0;
font-size: 10px;
color: var(--text-soft);
max-width: 320px;
line-height: 1.5;
}
/* ===================== REPORTS (Отчёты) ===================== */
.reportsRoot,
.compareRoot {
display: flex;
flex-direction: column;
gap: 12px;
}
.reportGrid {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 12px;
}
/* export-format cards */
.exportGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.exportCard {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
text-align: left;
padding: 14px;
border: var(--line) solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-2);
color: inherit;
transition:
border-color 0.15s,
box-shadow 0.15s,
opacity 0.15s;
}
.exportCard:hover:not(:disabled):not(.exportCardDisabled) {
border-color: var(--accent-cyan);
box-shadow: 0 0 14px var(--glow);
}
.exportCard:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.exportCardDisabled {
opacity: 0.5;
cursor: not-allowed;
}
.fmt {
width: 40px;
height: 40px;
flex: none;
display: grid;
place-items: center;
border-radius: var(--radius-xs);
font-family: var(--font-mono);
font-weight: 700;
font-size: 11px;
color: #fff;
}
.fmtPdf {
background: var(--accent-red);
}
.fmtCsv {
background: var(--accent-green);
}
.fmtDocx {
background: var(--accent-blue);
}
.fmtPptx {
background: var(--accent-orange);
}
.fmtMuted {
background: var(--surface-muted);
color: var(--text-soft);
}
.exportMeta {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
flex: 1;
}
.exportTitle {
display: flex;
align-items: center;
gap: 7px;
font-weight: 600;
font-size: 12px;
color: var(--text);
}
.exportDesc {
font-size: 10px;
color: var(--text-soft);
}
.exportTag {
padding: 1px 6px;
border-radius: 999px;
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent-yellow);
border: var(--line) solid currentColor;
}
.exportTagSoon {
padding: 1px 6px;
border-radius: 999px;
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
border: var(--line) solid var(--border);
}
.exportIco {
flex: none;
color: var(--text-soft);
}
.reportError {
margin: 12px 0 0;
font-size: 11px;
color: var(--accent-red);
}
/* report-section checklist */
.tocList {
display: grid;
gap: 2px;
}
.tocItem {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: var(--line) dashed var(--border-faint);
font-size: 11px;
color: var(--text-muted);
}
.tocIndex {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-soft);
flex: none;
}
.tocLabel {
flex: 1;
color: var(--text);
}
.tocEyebrow {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-soft);
}
.sourceLine {
display: flex;
justify-content: space-between;
gap: 10px;
margin-top: 14px;
font-size: 9px;
color: var(--text-soft);
font-family: var(--font-mono);
}
/* ===================== COMPARE (Сравнение) ===================== */
.compareBar {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.compareInput {
flex: 0 1 320px;
padding: 9px 12px;
background: var(--surface-2);
border: var(--line) solid var(--border);
border-radius: var(--radius-xs);
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
outline: none;
}
.compareInput:focus {
border-color: var(--accent-cyan);
}
.compareInput:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.compareAddBtn {
padding: 9px 16px;
border: var(--line) solid var(--accent-blue);
background: transparent;
color: var(--text);
border-radius: var(--radius-xs);
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.08em;
white-space: nowrap;
transition:
background 0.15s,
opacity 0.15s;
}
.compareAddBtn:hover:not(:disabled) {
background: var(--accent-blue);
color: #fff;
}
.compareAddBtn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.chipRow {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-left: 4px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: var(--line) solid var(--border);
border-radius: 999px;
font-size: 10px;
}
.chip .mono {
font-size: 10px;
}
.chipPinned {
border-color: var(--accent-cyan);
}
.chipTag {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent-cyan);
}
.chipRemove {
display: grid;
place-items: center;
border: none;
background: none;
padding: 0;
color: var(--text-soft);
}
.chipRemove:hover {
color: var(--text);
}
.compareInputError {
margin: 10px 0 0;
font-size: 11px;
color: var(--accent-red);
}
.compareHint {
margin: 0;
padding: 8px 2px;
font-size: 11px;
line-height: 1.5;
color: var(--text-soft);
}
.compareTableWrap {
overflow-x: auto;
}
.compareTable {
width: 100%;
border-collapse: collapse;
font-size: 11px;
min-width: 560px;
}
.compareTable th,
.compareTable td {
padding: 9px 11px;
text-align: right;
border-bottom: var(--line) solid var(--border-faint);
font-family: var(--font-mono);
}
.compareTable th:first-child,
.compareTable td:first-child {
text-align: left;
color: var(--text-muted);
font-family: var(--font-ui);
position: sticky;
left: 0;
background: var(--surface);
}
.compareTable thead th {
color: var(--text-strong);
font-family: var(--font-ui);
font-size: 11px;
vertical-align: top;
}
.compareTable thead th .mono {
font-size: 11px;
word-break: break-all;
}
.compareDistrict {
display: block;
margin-top: 3px;
font-family: var(--font-ui);
font-weight: 400;
font-size: 9px;
color: var(--text-soft);
}
.compareBest {
color: var(--accent-green);
font-weight: 600;
}
.compareVerdictRow td {
background: var(--surface-2);
}
.verdictBuy {
color: var(--accent-green);
font-weight: 600;
}
.verdictWatch {
color: var(--accent-yellow);
font-weight: 600;
}
.verdictSkip {
color: var(--accent-red);
font-weight: 600;
}
.cellMuted {
color: var(--text-soft);
}
.cellError {
color: var(--accent-red);
font-size: 10px;
}
/* ===================== DETAIL DRAWER (PR#4) ===================== */
.drawerScrim {
position: fixed;
inset: 0;
background: rgba(2, 8, 12, 0.62);
backdrop-filter: blur(2px);
opacity: 0;
visibility: hidden;
transition:
opacity 0.18s ease,
visibility 0.18s ease;
z-index: 60;
}
.drawerScrimOpen {
opacity: 1;
visibility: visible;
}
.drawer {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 480px;
max-width: 100vw;
background: var(--surface-strong);
border-left: var(--line) solid var(--border-strong);
box-shadow: var(--shadow);
transform: translateX(100%);
transition: transform 0.18s ease;
z-index: 61;
display: flex;
flex-direction: column;
}
.drawerOpen {
transform: translateX(0);
}
.drawerHead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-bottom: var(--line) solid var(--border);
flex-shrink: 0;
}
.drawerTitle {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--text-strong);
}
.drawerSub {
margin-top: 3px;
font-size: 10px;
color: var(--text-soft);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.drawerClose {
flex-shrink: 0;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border: var(--line) solid var(--border);
border-radius: var(--radius-xs);
background: transparent;
color: var(--text-muted);
}
.drawerClose:hover {
color: var(--text-strong);
border-color: var(--border-strong);
}
.drawerClose:focus-visible {
outline: var(--line-strong) solid var(--accent-cyan);
outline-offset: 1px;
}
.drawerBody {
flex: 1;
overflow-y: auto;
padding: 16px 18px 32px;
}
.drawerSection {
margin-bottom: 18px;
}
.drawerSection:last-child {
margin-bottom: 0;
}
.drawerH4 {
margin: 0 0 9px;
font-size: 9.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-soft);
}
.summaryBox {
padding: 11px 12px;
background: var(--surface-2);
border: var(--line) solid var(--border-faint);
border-radius: var(--radius-sm);
font-size: 11.5px;
line-height: 1.55;
color: var(--text);
}
.statusRow {
display: flex;
align-items: center;
gap: 9px;
padding: 4px 0;
font-size: 11px;
}
.drDot {
flex-shrink: 0;
width: 7px;
height: 7px;
border-radius: 50%;
}
.drDotOk {
background: var(--accent-green);
}
.drDotWarn {
background: var(--accent-yellow);
}
.drDotBad {
background: var(--accent-red);
}
.drDotMuted {
background: var(--text-soft);
}
.drLabel {
flex: 1;
color: var(--text-muted);
}
.drState {
text-align: right;
font-weight: 600;
}
.drStateOk {
color: var(--accent-green);
}
.drStateWarn {
color: var(--accent-yellow);
}
.drStateBad {
color: var(--accent-red);
}
.drStateMuted {
color: var(--text-soft);
font-weight: 500;
}
.drawerActions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.drawerGaugeRow {
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.drawerNote {
margin: 0 0 8px;
font-size: 11px;
line-height: 1.5;
color: var(--text-muted);
}
.drawerLinks {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.drawerLink {
font-size: 10px;
color: var(--accent-cyan);
text-decoration: none;
}
.drawerLink:hover {
text-decoration: underline;
}
/* ===================== 3D MASSING SANDBOX (Three.js) ===================== */
.massingWrap {
display: flex;
flex-direction: column;
gap: 12px;
}
.massingViewport {
position: relative;
width: 100%;
height: 380px;
border: var(--line) solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-inset);
overflow: hidden;
}
.massingViewport canvas {
display: block;
}
/* Compact always-on preview canvas in the «Инсоляционная матрица» lower-grid
card — autorotating mini massing, hands-off (canvas pointer-events:none so it
never steals page scroll/clicks). Tokens resolve under .pticaRoot[data-theme="dark"]. */
.massingPreviewViewport {
position: relative;
width: 100%;
height: 150px;
margin: 2px 0 10px;
border: var(--line) solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-inset);
overflow: hidden;
}
.massingPreviewViewport canvas {
display: block;
pointer-events: none;
}
.massingSkeleton,
.massingFallback {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px;
text-align: center;
color: var(--text-soft);
font-size: 11px;
}
.massingFallbackTitle {
font-size: 12px;
font-weight: 600;
color: var(--accent-yellow);
}
.massingFallback p {
margin: 0;
max-width: 280px;
line-height: 1.5;
}
.massingControls {
display: flex;
flex-direction: column;
gap: 12px;
}
.massingSliders {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
align-items: end;
}
.massingSlider {
display: flex;
flex-direction: column;
gap: 6px;
}
.massingSliderLabel {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
}
.massingSliderValue {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0;
text-transform: none;
color: var(--accent-cyan);
}
/* dark range inputs */
.massingControls input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
background: var(--border);
outline: none;
cursor: pointer;
}
.massingControls input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--accent-cyan);
border: var(--line) solid var(--bg);
cursor: pointer;
}
.massingControls input[type="range"]::-moz-range-thumb {
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--accent-cyan);
border: var(--line) solid var(--bg);
cursor: pointer;
}
.massingControls input[type="range"]:focus-visible {
box-shadow: 0 0 0 2px var(--glow-strong);
}
.massingToggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--text-muted);
cursor: pointer;
user-select: none;
}
.massingToggle input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: var(--accent-cyan);
cursor: pointer;
}
.massingMetrics {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
padding: 10px 12px;
background: var(--surface-2);
border: var(--line) solid var(--border-faint);
border-radius: var(--radius-sm);
}
.massingMetric {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.massingMetricLabel {
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
}
.massingMetricValue {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-strong);
white-space: nowrap;
}
.massingMetricOk {
color: var(--accent-green);
}
.massingMetricOver {
color: var(--accent-yellow);
}
.massingCaption {
font-size: 9px;
line-height: 1.5;
color: var(--text-soft);
font-family: var(--font-mono);
}
/* drawer sub-tabs (.dtabs / .dtab-panel) */
.dtabs {
display: flex;
gap: 4px;
margin-bottom: 11px;
border-bottom: var(--line) solid var(--border-faint);
}
.dtabs button {
padding: 6px 10px;
background: transparent;
border: none;
border-bottom: var(--line-strong) solid transparent;
color: var(--text-soft);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.dtabs button:hover {
color: var(--text-muted);
}
/* specific enough to beat `.dtabs button` without !important. */
.dtabs button.dtabActive {
color: var(--text-strong);
border-bottom-color: var(--accent-cyan);
}
.dtabPanel {
display: grid;
gap: 2px;
}
/* gauge-as-button (hero Buildability) */
.gaugeButton {
display: block;
width: 100%;
background: transparent;
border: none;
padding: 0;
cursor: pointer;
}
.gaugeButton:focus-visible {
outline: var(--line-strong) solid var(--accent-cyan);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* ===================== RESPONSIVE ===================== */
@media (max-width: 1500px) {
.hero {
grid-template-columns: 1.5fr 1fr;
}
.scorePanel {
grid-column: span 2;
flex-direction: row;
justify-content: space-around;
flex-wrap: wrap;
}
}
@media (max-width: 1200px) {
.scanGrid {
grid-template-columns: repeat(3, 1fr);
}
.lowerGrid,
.bottomGrid {
grid-template-columns: 1fr 1fr;
}
.forecastGrid,
.reportGrid {
grid-template-columns: 1fr;
}
}
@media (max-width: 820px) {
.shell {
grid-template-columns: 1fr;
}
.rail {
display: none;
}
.hero,
.scanGrid,
.lowerGrid,
.bottomGrid {
grid-template-columns: 1fr;
}
.scorePanel {
flex-direction: column;
}
.tabs {
display: none;
}
.scenarioGrid,
.forecastGrid,
.exportGrid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.drawer {
width: 100vw;
}
.massingSliders {
grid-template-columns: 1fr;
}
.massingMetrics {
grid-template-columns: repeat(2, 1fr);
}
.massingViewport {
height: 320px;
}
}
/* ===================== MAP OVERLAY (legend + layer toggles + CP list) ===== */
.mapOverlay {
position: absolute;
left: 14px;
top: 14px;
width: 224px;
max-height: calc(100% - 28px);
overflow-y: auto;
padding: 12px 13px;
background: var(--surface-inset);
border: var(--line) solid var(--border);
border-radius: var(--radius-sm);
backdrop-filter: blur(10px);
z-index: 4;
}
.mapOverlay h4 {
margin: 0 0 10px;
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text);
display: flex;
align-items: center;
gap: 6px;
}
.mapOverlay h4 .live {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-green);
box-shadow: 0 0 8px var(--accent-green);
}
/* base-layer segmented control (Спутник | Схема) */
.baseToggle {
display: inline-flex;
margin: 0 0 10px;
border: var(--line) solid var(--border);
border-radius: var(--radius-xs);
overflow: hidden;
background: var(--surface-inset);
}
.baseToggle button {
padding: 4px 9px;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-soft);
background: transparent;
border: none;
}
.baseToggle button + button {
border-left: var(--line) solid var(--border);
}
.baseToggle button:hover {
color: var(--text);
}
.baseToggle button.baseToggleActive {
color: #08131c;
background: var(--accent-cyan);
}
/* layer-toggle list */
.layerList {
display: grid;
gap: 6px;
font-size: 10px;
color: var(--text-muted);
}
.layerRow {
display: grid;
grid-template-columns: 14px 1fr auto;
gap: 7px;
align-items: center;
background: none;
border: none;
padding: 0;
text-align: left;
color: inherit;
font: inherit;
width: 100%;
}
.layerRow:not(.layerRowSoon) {
cursor: pointer;
}
.layerRow .swatch {
width: 9px;
height: 9px;
border-radius: 2px;
}
.layerRow .swatch3d {
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
}
.layerRow .cnt {
color: var(--text-soft);
font-size: 9px;
}
.layerRowOff {
opacity: 0.4;
}
.layerRowOff .swatch {
background: var(--text-soft) !important;
}
.layerRowSoon {
opacity: 0.55;
cursor: default;
}
.layerRowSoon .swatch {
background: var(--text-soft) !important;
}
.layerRow3d {
cursor: pointer;
}
.openTag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 7px;
border: var(--line) solid var(--accent-cyan);
border-radius: 999px;
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent-cyan);
}
.openTag::before {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent-cyan);
box-shadow: 0 0 6px var(--accent-cyan);
}
/* isochrone sub-legend (5/10/15 мин) — only visible when layer is on */
.isoSublegend {
display: none;
gap: 8px;
margin: 4px 0 2px;
font-size: 8.5px;
color: var(--text-soft);
}
.isoSublegendOn {
display: flex;
}
.isoSublegend i {
display: inline-flex;
align-items: center;
gap: 4px;
font-style: normal;
}
.isoSublegend i::before {
content: "";
width: 8px;
height: 8px;
border-radius: 2px;
background: var(--accent-cyan);
}
.isoSublegend i.b10::before {
opacity: 0.6;
}
.isoSublegend i.b15::before {
opacity: 0.32;
}
.overlayDivider {
margin: 10px 0 8px;
border: none;
border-top: var(--line) solid var(--border);
}
.overlaySub {
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-soft);
margin-bottom: 7px;
}
.resRow {
display: grid;
grid-template-columns: 16px 1fr auto;
gap: 7px;
align-items: center;
font-size: 10px;
margin-bottom: 5px;
}
.resRow .dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.resRow b {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text);
}
/* map tool buttons (zoom / fit / add-POI) */
.mapTools {
position: absolute;
right: 14px;
top: 14px;
display: grid;
gap: 7px;
z-index: 4;
}
.mapTools .tool {
width: 34px;
height: 34px;
border: var(--line) solid var(--border);
background: var(--surface-strong);
border-radius: var(--radius-xs);
display: grid;
place-items: center;
color: var(--text);
}
.mapTools .tool:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.mapTools .tool svg {
width: 15px;
height: 15px;
}
.mapTools .toolActive {
border-color: var(--accent-yellow);
color: var(--accent-yellow);
box-shadow: 0 0 10px rgba(224, 169, 59, 0.45);
}
.mapHint {
position: absolute;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
z-index: 4;
padding: 4px 12px;
font-size: 9.5px;
letter-spacing: 0.04em;
color: var(--text);
background: var(--surface-strong);
border: var(--line) solid var(--border);
border-radius: 999px;
backdrop-filter: blur(6px);
}
/* Static compass (prototype .map-compass) — bottom-right "N" arrow above the
Leaflet panes; non-interactive so it never blocks panning. */
.mapCompass {
position: absolute;
right: 18px;
bottom: 16px;
width: 46px;
height: 46px;
color: var(--text-muted);
z-index: 4;
pointer-events: none;
}
/* ===================== LOWER GRID (cockpit) =====================
Ported from prototype .lower-grid: ОКС · Development Potential ·
Recommended Product · Инсоляционная матрица. */
.lowerGrid {
display: grid;
grid-template-columns: 1.05fr 1.05fr 1.15fr 0.95fr;
gap: 10px;
/* Equal-height row (прот.: «выровняй тут все»). Each card is a flex column
(rule below) so a stretched card lays out intentionally: the footer action
pins to the bottom (margin-top:auto) and sparse empty-states (.emptyState /
.placeholder flex:1) centre their content instead of leaving an awkward gap. */
align-items: stretch;
}
/* Make every lower/bottom-grid card a flex column so stretching is intentional:
the footer «Подробнее» button drops to the bottom and the .emptyState /
.placeholder body grows to fill (centred) rather than the card being a tall
empty box. */
.lowerGrid > .card,
.bottomGrid > .card {
display: flex;
flex-direction: column;
}
/* Footer actions (DIRECT children of the card — the «Подробнее» button or the
Инсоляция .placeholder block) pinned to the bottom of a stretched flex-column
card. Header buttons (inside .cardHead) are NOT direct children, so they stay
put. */
.lowerGrid > .card > .detailBtn,
.bottomGrid > .card > .detailBtn,
.lowerGrid > .card > .placeholder {
margin-top: auto;
align-self: flex-start;
}
/* The Инсоляция .placeholder is itself a centred column — its OWN button keeps
normal spacing inside it (the placeholder block is what gets pinned). */
.lowerGrid > .card > .placeholder {
align-self: stretch;
}
.lowerGrid > .card > .placeholder .detailBtn {
margin-top: 8px;
align-self: center;
}
/* Sparse bodies grow + centre when their card is stretched (ОКС «Нет данных»). */
.lowerGrid > .card > .emptyState,
.bottomGrid > .card > .emptyState {
flex: 1;
}
/* Lower-grid квартирография bars match the prototype .bar-row (narrow label /
value columns, 2px track radius) — SCOPED so the shared base .hbar used by
the drawer (110px 1fr 46px, 3px radius) is NOT crushed. */
.lowerGrid .hbars {
gap: 8px;
}
.lowerGrid .hbar {
grid-template-columns: 46px 1fr 38px;
}
.lowerGrid .hbarTrack {
border-radius: 2px;
}
.lowerGrid .hbarFill {
border-radius: 2px;
}
/* shared detail-btn that needs top separation inside flow cards */
.detailBtnSpaced {
margin-top: 10px;
align-self: flex-start;
}
/* ОКС — isometric building visual + empty-state */
.isoBox {
height: 84px;
display: grid;
place-items: center;
margin: 2px 0 10px;
}
.isoBox svg {
width: 100%;
height: 100%;
}
.isoFaceLeft {
fill: rgba(210, 101, 91, 0.22);
stroke: var(--accent-red);
stroke-width: 1;
}
.isoFaceTop {
fill: rgba(210, 101, 91, 0.13);
stroke: var(--accent-red);
stroke-width: 1;
}
.isoFaceRight {
fill: rgba(210, 101, 91, 0.3);
stroke: var(--accent-red);
stroke-width: 1;
}
/* Development Potential — metric tiles */
.potentialGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 9px;
margin-bottom: 10px;
}
/* grid-item wrapper (label + value stack); min-width:0 stops long values
blowing out the grid track. */
.metric {
min-width: 0;
}
.metricLabel {
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
margin-bottom: 2px;
}
.metricValue {
font-size: 17px;
font-weight: 600;
color: var(--text-strong);
}
.metricValueLg {
font-size: 22px;
}
.metricValuePlaceholder {
color: var(--text-soft);
font-weight: 500;
}
.metricUnit {
font-size: 10px;
color: var(--text-muted);
margin-left: 3px;
}
/* Recommended Product — mix-bar head (квартирография / доля) */
.mixHead {
display: flex;
justify-content: space-between;
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
margin-bottom: 1px;
}
/* Инсоляционная матрица — live preview massing (.massingPreviewViewport above)
+ ПРЕВЬЮ placeholder. The former faint SVG iso-cube (.isoBoxPreview /
@keyframes pticaIsoSpin) was replaced by the real autorotating 3D model. */
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
}
.placeholderSoon {
font-size: 22px;
letter-spacing: 0.12em;
color: var(--text-soft);
font-weight: 600;
}
.placeholder p {
font-size: 10px;
color: var(--text-soft);
margin: 0;
max-width: 180px;
}
/* ===================== BOTTOM GRID (cockpit) =====================
Ported from prototype .bottom-grid: Investment Clearance · Buy Signal ·
Legal Status · Site Verdict. */
.bottomGrid {
display: grid;
grid-template-columns: 1.5fr 0.62fr 0.86fr 1.2fr;
gap: 10px;
/* Equal-height row (как .lowerGrid · «выровняй тут все»). Cards are flex
columns (rule above): footer actions pin to the bottom, sparse bodies
(.emptyState flex:1) centre, and Buy Signal re-centres its gauge — so a
stretched card lays out intentionally instead of leaving an awkward gap. */
align-items: stretch;
}
/* Investment Clearance — bignum row */
.bignumRow {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.bignum {
min-width: 0;
}
.bignumLabel {
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
margin-bottom: 4px;
}
.bignumValue {
font-size: 30px;
font-weight: 500;
letter-spacing: 0.01em;
color: var(--text-strong);
line-height: 1;
}
.bignumValuePlaceholder {
color: var(--text-soft);
}
.bignumUnit {
display: block;
font-size: 9px;
color: var(--text-muted);
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.bignumCaption {
margin: 12px 0 0;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-soft);
}
/* Buy Signal — centered gauge. Flex-column centering (not grid) so it composes
with the `.bottomGrid > .card` flex-column stretch rule; the gauge body sits
centred in the equal-height row. */
.bottomGrid > .card.buySignalCard {
align-items: center;
justify-content: center;
}
.buySignalTitle {
align-self: flex-start;
}
/* Legal Status — Нет/значение grid */
.legalGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--line);
background: var(--border-faint);
border: var(--line) solid var(--border-faint);
border-radius: var(--radius-sm);
overflow: hidden;
}
.legalCell {
background: var(--surface-2);
padding: 9px 11px;
}
.legalKey {
display: block;
font-size: 8.5px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-soft);
margin-bottom: 3px;
}
.legalValue {
font-size: 12.5px;
font-weight: 600;
color: var(--text);
}
.legalValuePlaceholder {
color: var(--text-soft);
font-weight: 500;
}
/* Site Verdict — headline plaque + checklist */
.verdictCard {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
text-align: center;
/* Prototype .verdict-card plaque gradient — scoped dark cockpit (authoritative
here over the global flat-token rule). */
background: linear-gradient(135deg, var(--surface), var(--surface-2));
}
.verdictSplit {
display: grid;
grid-template-columns: auto 1fr;
gap: 18px;
align-items: center;
}
.verdictLead {
text-align: center;
}
.verdictLeadLabel {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--text-soft);
margin-bottom: 6px;
}
.verdictWord {
font-size: 30px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
text-shadow: 0 0 22px var(--glow);
}
.verdictWordGood {
color: var(--accent-green);
}
.verdictWordWarn {
color: var(--accent-yellow);
}
.verdictWordBad {
color: var(--accent-red);
}
.checklist {
display: grid;
gap: 8px;
text-align: left;
}
.checkItem {
display: flex;
align-items: flex-start;
gap: 9px;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-muted);
}
.checkIc {
width: 16px;
height: 16px;
flex: none;
}
.checkItemOk .checkIc {
color: var(--accent-green);
border: var(--line-strong) solid currentColor;
border-radius: 50%;
padding: 2px;
}
.checkItemWarn .checkIc {
color: var(--accent-yellow);
/* Prototype removes the icon border ring on warn checklist items. */
border: none;
}
.checkItemBad .checkIc {
color: var(--accent-red);
}