gendesign/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css
Light1YT ba05a4cb62
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 49s
CI / openapi-codegen-check (pull_request) Successful in 1m48s
feat(ptica): rich dark cockpit map + ПТИЦА as the default analysis route
Fix two regressions vs the prototype:
1. PticaMapInner: render real data layers over the dark base by REUSING the
   existing analysis-map components — POI by category, competitors/pipeline
   (MarketLayers), connection points + colored polylines (ConnectionPointsLayer),
   ЗОУИТ (ZouitLayer), custom POI add/edit/delete (CustomPoiLayer + mutation hooks).
   Add Спутник/Схема base toggle (Esri default + --map-filter-sat); legend rows
   toggle layers; «Точки подключения» driven by real CP data. Isochrones left
   «скоро» (ORS is on-demand, not in /analyze).
2. /site-finder/analysis/[cad] now renders the ПТИЦА cockpit — every parcel entry
   opens ПТИЦА, not the old design. [cad]/ptica redirects to the parent (single
   cockpit copy). Old AnalysisPageContent kept in repo; legacy UI at /legacy/site-finder.
3. Surface 3D massing: new «Инсоляция · 3D-масса» scan card + 3D legend row + 3D
   map tool all open the insolation/future3d drawers.

SSR-safe (Leaflet only behind dynamic ssr:false); real POI from score_breakdown;
no redirect loop. tsc/lint/prettier/build green. code-reviewer APPROVE.
2026-06-21 00:04:00 +05:00

2169 lines
41 KiB
CSS

/*
* ПТИЦА 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:
"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%;
}
.pticaRoot 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;
position: sticky;
top: 0;
height: 100vh;
}
.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);
}
.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;
}
/* 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;
}
.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;
}
/* canvas (scroll area) */
.canvas {
padding: 14px 18px 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ===================== 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) {
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);
}
.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);
}
.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(4, 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);
}
.kvrow .v {
font-weight: 600;
color: var(--text);
text-align: right;
}
.kvrowPlaceholder .v {
color: var(--text-soft);
font-weight: 500;
}
.scanCard .detailBtn {
margin-top: auto;
align-self: flex-start;
}
/* 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;
}
.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);
}
/* 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);
}
.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);
}
.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;
}
.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(2, 1fr);
}
.forecastGrid,
.reportGrid {
grid-template-columns: 1fr;
}
}
@media (max-width: 820px) {
.shell {
grid-template-columns: 1fr;
}
.rail {
display: none;
}
.hero,
.scanGrid {
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);
}