mapdash/frontend/src/styles.css
aaverbitskiy 5ba5a77f97 UI-доработки: выравнивание тогглов, датапикер с «Применить», степперы 80px, поле поиска, автозакрытие балуна карты
- Тогглы: Бренд+Компания в одну строку, блок выровнен ровно по полосе контролов (верх = верх полей, низ = низ «Поиска»), равномерные зазоры
- Датапикер: выбор диапазона больше не применяется автоматически по второму клику — только кнопкой «Применить» (+ «Сбросить» для незавершённого выбора)
- Степперы «Оформления»: ровно 80px по ширине
- Поле поиска в дропдаунах: равномерная рамка 1px по всем сторонам вместо разноцветных краёв
- Балун на карте: клик по пустой карте закрывает балун (guard от самозакрытия по «своему» клику, клик по другому кластеру открывает новый)
- Полоса фильтров: поля сужены на 10px, «Период» до 333px

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 20:39:12 +00:00

864 lines
45 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.

/* Open Props — design-token foundation (colours, shadows, easing, scales).
Our semantic tokens below are backed by its ramps, so the rest of the CSS
(which references var(--accent) etc.) is untouched. */
@import "open-props/style";
:root {
/* appearance (overridden at runtime by appearance.ts) */
--row-h: 30px;
--bar-h: 18px;
--bar-font: 11px;
--label-font: 12px;
--label-w: 340px;
/* ---- design tokens (light), backed by Open Props ---- */
--bg: #ffffff;
--surface: var(--gray-0);
--surface-2: var(--gray-1);
--border: var(--gray-3);
--border-strong: var(--gray-4);
--text: var(--gray-9);
--text-muted: var(--gray-6);
--font-sans: "IBM Plex Sans", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
--accent: #4f46e5;
--accent-weak: rgba(79, 70, 229, .12);
--accent-ring: rgba(79, 70, 229, .22);
--radius: 8px;
--radius-sm: 6px;
--shadow-sm: var(--shadow-1);
--shadow-md: var(--shadow-3);
--transition: .13s var(--ease-3);
/* timeline / map surfaces (built on the tokens above) */
--grid-line: var(--border);
--row-even: var(--gray-1);
--row-odd: #ffffff;
--row-hover: #fff3cd;
--month-hover: rgba(79, 70, 229, .08);
--tip-bg: var(--gray-9);
--tip-text: #ffffff;
--tip-sub: var(--gray-4);
--today: #ef5350;
}
/* Dark theme — single source of truth is data-theme on <html>, set by the
no-flash inline script in index.html (defaults to prefers-color-scheme). */
:root[data-theme="dark"] {
--bg: #0f1420;
--surface: #161c2b;
--surface-2: #1e2637;
--border: #273043;
--border-strong: #33405a;
--text: #e6e9ef;
--text-muted: #93a0b5;
--accent: #5b8def;
--accent-weak: rgba(91, 141, 239, .16);
--accent-ring: rgba(91, 141, 239, .30);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
--shadow-md: 0 6px 18px rgba(0, 0, 0, .5);
--row-even: #141a27;
--row-odd: #0f1420;
--row-hover: #33361f;
--month-hover: rgba(91, 141, 239, .12);
--tip-bg: #0b0f18;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; overflow: hidden; }
body {
display: flex;
flex-direction: column;
font-family: var(--font-sans);
background: var(--bg);
color: var(--text);
}
header {
flex: 0 0 auto;
padding: 12px 18px;
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow-sm);
display: flex;
gap: 18px;
align-items: stretch;
/* Responsive: the fields group compresses (and wraps its own fields) first;
if the window is still too narrow, whole blocks drop to the next row
instead of overlapping. */
flex-wrap: wrap;
align-content: flex-start;
background: var(--bg);
/* position:relative makes the z-index actually take effect, so the header
(and its open dropdown panels) forms a stacking context above the timeline
— otherwise the column-resizer line bleeds over the filter dropdown lists. */
position: relative;
z-index: 40;
}
/* Header groups. Flat now (no boxes) — the header itself is the panel. */
.hgroup {
display: flex;
align-items: center;
gap: var(--size-3);
padding: var(--size-1) 0;
flex-shrink: 1;
min-width: 0;
}
/* The fields group is the one that gives way first when space runs out. */
.hgroup-fields {
flex-shrink: 4; min-width: 0;
display: grid;
grid-template-columns: repeat(4, 170px);
gap: 12px 14px;
align-items: start;
align-content: start;
}
/* Compact grid: 4 dropdowns on row 1; the two dates + search fall to row 2. */
.hgroup-fields .field { min-width: 0; }
.hgroup-fields .dropdown,
.hgroup-fields .dropdown-btn,
.hgroup-fields input[type=text],
.hgroup-fields input[type=date] { min-width: 0; width: 100%; }
@media (max-width: 760px) { .hgroup-fields { grid-template-columns: repeat(2, minmax(120px, 1fr)); } }
/* Fields group: top-align so the date input sits UNDER Город/Бренд. */
.hgroup-fields { align-items: flex-start; }
/* ---- date-range field + calendar popup ---- */
.field-dates { grid-column: span 2; }
.field-dates .dr-field { max-width: 333px; } /* the "Период" field is 40px narrower than its 2-col slot */
.dr-native { display: none; }
.daterange { position: relative; }
.dr-field {
display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box;
height: 36px; padding: 0 10px; cursor: pointer; font-size: 13px; color: var(--text-muted);
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);
transition: border-color var(--transition);
}
.dr-field:hover { border-color: var(--border-strong); }
.dr-field.has-value { color: var(--text); }
.dr-ico { width: 16px; height: 16px; flex: 0 0 auto; color: var(--accent); }
.dr-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.dr-clear { border: none; background: none; color: var(--text-muted); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; flex: 0 0 auto; }
.dr-clear:hover { color: var(--text); }
.dr-cal {
position: absolute; top: calc(100% + 4px); left: 0; z-index: 40; width: 252px;
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow-md); padding: 10px; font-variant-numeric: tabular-nums;
}
.dr-cal[hidden] { display: none; }
.dr-cal-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.dr-cal-title { font-size: 13px; font-weight: 600; color: var(--text); }
.dr-nav { border: none; background: none; color: var(--text-muted); cursor: pointer; font-size: 18px; line-height: 1; width: 26px; height: 26px; border-radius: var(--radius-sm); }
.dr-nav:hover { background: var(--surface); color: var(--text); }
.dr-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.dr-wd { text-align: center; font-size: 11px; color: var(--text-muted); padding-bottom: 4px; }
.dr-day { position: relative; border: none; background: none; padding: 2px 0; cursor: pointer; display: flex; align-items: center; justify-content: center; height: 30px; }
.dr-day::before { content: ""; position: absolute; inset: 0; z-index: 0; }
.dr-day.rng::before { background: #eef0fe; }
.dr-day.s::before { background: linear-gradient(to right, transparent 50%, #eef0fe 50%); }
.dr-day.e::before { background: linear-gradient(to left, transparent 50%, #eef0fe 50%); }
.dr-day.s.e::before { background: transparent; }
.dr-num { position: relative; z-index: 1; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; font-size: 12px; color: var(--text); }
.dr-day:hover .dr-num { background: var(--surface-2); }
.dr-day.s .dr-num, .dr-day.e .dr-num { background: var(--accent); color: #fff; font-weight: 600; }
.dr-day.s:hover .dr-num, .dr-day.e:hover .dr-num { background: var(--accent); }
.dr-foot { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
.dr-reset { border: none; background: none; color: var(--text-muted); cursor: pointer; font-size: 13px; padding: 7px 10px; border-radius: var(--radius-sm); }
.dr-reset:hover { background: var(--surface); color: var(--text); }
.dr-apply { border: none; background: var(--accent); color: #fff; cursor: pointer; font-size: 13px; font-weight: 600; padding: 7px 16px; border-radius: var(--radius-sm); transition: background var(--transition); }
.dr-apply:hover { background: var(--accent-strong, #4338ca); }
.dr-apply:disabled { opacity: .45; cursor: default; }
.fcol { display: flex; flex-direction: column; gap: 8px; }
input[type=date],
input[type=text] {
height: 36px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 10px;
font-size: 13px;
min-width: 0;
width: 100%;
background: var(--bg);
color: var(--text);
transition: border-color var(--transition), box-shadow var(--transition);
}
input[type=date]:hover,
input[type=text]:hover { border-color: var(--border-strong); }
input[type=date]:focus,
input[type=text]:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-ring);
}
input::placeholder { color: var(--text-muted); }
/* Search field: magnifier icon inside the input. */
#search {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 10px center;
background-size: 15px;
padding-left: 32px;
}
:root[data-theme="dark"] input[type=date]::-webkit-calendar-picker-indicator {
filter: invert(1) opacity(.65);
}
/* Compact controls block: scale (label + short slider + round Оформление) on
top, the three checkboxes stacked below. */
.hgroup-controls { flex-direction: column; align-items: flex-start; gap: 8px; }
.zoom-row { display: flex; align-items: center; gap: 8px; }
/* Toggles: aligned to the filter *controls* (not the labels) — Бренд + Компания
share the top row, then Коллизии and Все поверхности, evenly distributed
between the top of the first control box and the bottom of Поиск. */
.checks { display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; flex: 1; align-self: stretch; padding: 19px 0 0; }
.checks .checkbox-field { align-self: flex-start; }
.checks-row { display: flex; flex-direction: row; align-items: center; gap: 18px; }
/* Let the brand block fill the header height so the counters can sit at the
bottom, level with the date fields / mode switch. */
.hgroup-brand { align-items: stretch; }
.brand { display: flex; flex-direction: column; justify-content: flex-start; line-height: 1.2; }
.brand-name { font-size: 23px; font-weight: 700; white-space: nowrap; }
.brand-sub { font-size: 15px; font-weight: 400; color: var(--text-muted); white-space: nowrap; }
.field { display: flex; flex-direction: column; gap: var(--size-1); }
.field label {
font-size: 11px; color: var(--text-muted);
text-transform: uppercase; letter-spacing: .05em; font-weight: 500;
}
/* Counters — compact inset inside the brand block. */
.brand #status { margin-top: auto; }
/* Scale group: stack the "Оформление" button under the zoom slider. */
.checkbox-field { flex-direction: row; align-items: center; gap: 6px; align-self: center; }
.checkbox-field label {
display: flex; align-items: center; gap: 6px;
font-size: 13px; color: var(--text); text-transform: none; letter-spacing: normal;
font-weight: 400; cursor: pointer; white-space: nowrap;
}
/* Toggle switch: the checkbox keeps its semantics/JS, only the look changes.
Scoped to .checkbox-field so the dropdown option checkboxes stay normal. */
.checkbox-field input[type=checkbox] {
appearance: none; -webkit-appearance: none; margin: 0; flex: 0 0 auto;
width: 34px; height: 20px; border-radius: 20px; background: #cbd0d8;
position: relative; cursor: pointer; transition: background var(--transition);
}
.checkbox-field input[type=checkbox]::after {
content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
transition: left var(--transition);
}
.checkbox-field input[type=checkbox]:checked { background: var(--accent); }
.checkbox-field input[type=checkbox]:checked::after { left: 16px; }
.checkbox-field input[type=checkbox]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.zoom-field { gap: 4px; }
.zoom-field label { white-space: nowrap; }
.zoom-field .zoom-row { position: relative; width: 120px; padding-bottom: 24px; display: block; }
/* The scale control floats over the top-right of the timeline, just below the
time axis (JS sets `top` = axis bottom + 5px; this is only a fallback). */
#chart-wrap #zoom-field {
position: absolute; top: 52px; right: 16px; z-index: 20;
background: rgba(255, 255, 255, .92); border-radius: 8px;
padding: 3px 12px 4px; box-shadow: var(--shadow-md);
}
.zoom-field input[type=range] {
-webkit-appearance: none; appearance: none; width: 100%; height: 6px; margin: 0;
border-radius: 4px; cursor: pointer; outline: none;
background: linear-gradient(to right, var(--accent) var(--zoom-fill, 0%), var(--border-strong) var(--zoom-fill, 0%));
}
.zoom-field input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%;
background: #fff; border: 2px solid var(--accent); box-shadow: 0 1px 3px rgba(0, 0, 0, .3); cursor: pointer;
}
.zoom-field input[type=range]::-moz-range-thumb {
width: 16px; height: 16px; border-radius: 50%; background: #fff;
border: 2px solid var(--accent); box-shadow: 0 1px 3px rgba(0, 0, 0, .3); cursor: pointer;
}
.zoom-field input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Value bubble that rides below the slider thumb, pointing up (JS sets `left`). */
.zoom-bubble {
position: absolute; bottom: 0; top: auto; left: 0; transform: translateX(-50%);
background: var(--accent); color: #fff; font-size: 11px; font-weight: 600;
padding: 2px 8px; border-radius: 6px; white-space: nowrap; pointer-events: none;
font-variant-numeric: tabular-nums;
}
.zoom-bubble::after {
content: ""; position: absolute; top: -4px; bottom: auto; left: 50%; transform: translateX(-50%);
border: 4px solid transparent; border-bottom-color: var(--accent); border-top: 0;
}
/* checkbox dropdown filter */
.dropdown { position: relative; }
.dropdown-btn {
height: 36px;
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);
color: var(--text); padding: 0 12px; font-size: 13px; min-width: 180px; text-align: left;
cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 8px;
transition: border-color var(--transition), box-shadow var(--transition);
}
.dropdown-btn:hover { border-color: var(--border-strong); }
.dropdown.open .dropdown-btn { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); }
.dropdown-btn .caret { font-size: 10px; color: var(--text-muted); }
.dropdown-panel {
display: none; position: absolute; top: calc(100% + 4px); left: 0;
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow-md); padding: 6px 0;
min-width: 220px; max-height: 280px; overflow-y: auto; z-index: 30;
}
.dropdown.open .dropdown-panel { display: block; }
.dropdown-panel label {
display: flex; align-items: center; gap: var(--size-2); padding: var(--size-2) var(--size-3);
font-size: 13px; cursor: pointer; white-space: nowrap; color: var(--text);
/* Option labels are <label>s inside .field, so they'd inherit the uppercase /
weight / letter-spacing of `.field label`. Reset to clean body text. */
font-weight: 400; text-transform: none; letter-spacing: normal;
}
.dropdown-panel label:hover { background: var(--surface); }
.dropdown-panel input[type=checkbox] { accent-color: var(--accent); }
.dropdown-panel .dp-actions {
display: flex; gap: 10px; padding: 4px 12px 8px;
border-bottom: 1px solid var(--border); margin-bottom: 4px;
}
.dropdown-panel .dp-actions a { font-size: 11px; color: var(--accent); cursor: pointer; text-decoration: none; font-weight: 600; }
.dropdown-panel .dp-actions a:hover { text-decoration: underline; }
/* In-dropdown search that filters the option list — a clean, evenly-bordered box. */
.dropdown-panel .dp-search {
display: block; width: calc(100% - 16px); box-sizing: border-box; margin: 3px 8px 6px;
padding: 7px 10px; font-size: 13px; color: var(--text); background: var(--surface);
border: 1px solid var(--border); border-radius: var(--radius-sm); outline: none;
}
.dropdown-panel .dp-search::placeholder { color: var(--text-muted); }
.dropdown-panel .dp-search:focus { border-color: var(--border-strong); background: var(--bg); }
/* Оформление button + context menu */
/* Pinned to the header's bottom-right corner. */
.decor-wrap { position: absolute; right: 18px; bottom: 12px; }
/* Round "+" icon button; label shown as a hover tooltip (data-tip). */
.decor-btn {
position: relative;
width: 36px; height: 36px; border-radius: 50%; padding: 0;
border: 1px solid var(--border); background: var(--bg); color: var(--text-muted);
font-size: 23px; line-height: 1; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: border-color var(--transition), background var(--transition), color var(--transition);
}
.decor-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--surface); }
.decor-btn::after {
content: attr(data-tip);
position: absolute; top: calc(100% + 7px); right: 0;
background: var(--tip-bg); color: var(--tip-text); font-size: 11px; white-space: nowrap;
padding: 4px 8px; border-radius: var(--radius-sm); box-shadow: var(--shadow-md);
opacity: 0; pointer-events: none; transition: opacity var(--transition); z-index: 60;
}
.decor-btn:hover::after { opacity: 1; }
.decor-menu {
/* Fixed to the viewport: JS sets `top` (20px below the header/chips) and
`max-height` (ending ≥30px above the window bottom) on open. */
display: none; position: fixed; right: 58px;
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow-md); padding: var(--size-3); z-index: 50;
width: max-content; max-width: min(420px, 92vw); overflow-y: auto;
}
.decor-menu.open { display: block; }
.decor-section { margin-bottom: 22px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
.decor-section:last-of-type { border-bottom: none; margin-bottom: 8px; }
.decor-section-title {
font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: var(--text-muted); margin: 6px 0 8px;
}
.decor-row {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 4px 0; font-size: 13px;
}
.decor-row > span { color: var(--text); }
.decor-row input[type=number] {
width: 84px; padding: 5px 6px; border: 1px solid var(--border); border-radius: var(--radius-sm);
font-size: 13px; background: var(--bg); color: var(--text);
}
/* Stepper: /+ buttons around an editable number field. */
.decor-stepper { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.decor-stepper .ds-btn {
display: flex; align-items: center; justify-content: center; padding: 0;
width: 20px; height: 28px; border: none; background: var(--surface);
color: var(--accent); font-size: 15px; line-height: 1; cursor: pointer;
transition: background var(--transition);
}
.decor-stepper .ds-btn:hover { background: var(--surface-2); }
.decor-stepper input.ds-input {
width: 38px; height: 28px; text-align: center; padding: 0 2px;
border: none; border-left: 1px solid var(--border); border-right: 1px solid var(--border);
border-radius: 0; font-size: 13px; background: var(--bg); color: var(--text);
font-variant-numeric: tabular-nums; -moz-appearance: textfield;
}
.decor-stepper .ds-input::-webkit-outer-spin-button,
.decor-stepper .ds-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.decor-stepper .ds-input:focus { outline: none; }
.decor-row input[type=color] {
width: 42px; height: 28px; padding: 0; border: 1px solid var(--border);
border-radius: var(--radius-sm); background: var(--bg); cursor: pointer;
}
.decor-color-wrap { display: inline-flex; align-items: center; gap: 8px; }
.decor-color-reset {
display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px; padding: 0; border: none; background: none;
color: var(--text-muted); cursor: pointer; border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
}
.decor-color-reset:hover { color: var(--accent); background: var(--surface-2); }
.decor-reset {
margin-top: 6px; width: 100%; padding: 8px; font-size: 12px;
border: 1px solid var(--border); border-radius: var(--radius-sm);
background: var(--surface); color: var(--text); cursor: pointer;
transition: background var(--transition);
}
.decor-reset:hover { background: var(--surface-2); }
/* ---- view modes: segmented control ---- */
.mode-switch {
display: inline-flex; flex-direction: column; gap: 5px;
background: var(--surface-2); border: none; position: relative;
border-radius: var(--radius); padding: 6px;
height: 100%; /* fill the header row so the bottom lines up with the date fields */
width: 160px; /* fixed so the bold active label doesn't resize the block */
}
/* Sliding active-mode indicator (JS sets top/height to the active button). */
.mode-ind {
position: absolute; left: 6px; right: 6px; top: 6px; height: 0; z-index: 0;
background: var(--bg); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm);
transition: top .2s var(--ease-3), height .2s var(--ease-3); pointer-events: none;
}
.mode-btn {
position: relative; z-index: 1;
border: none; background: transparent; color: var(--text-muted); cursor: pointer;
padding: 6px 12px; font-size: 13px; white-space: nowrap; text-align: left; width: 100%;
border-radius: var(--radius-sm); transition: color var(--transition);
flex: 1; display: flex; align-items: center; gap: 8px; /* icon + label */
}
.mode-ico { width: 16px; height: 16px; flex: 0 0 auto; }
.mode-btn:hover { color: var(--text); }
.mode-btn.active { color: var(--accent); font-weight: 600; }
/* ---- toast notifications (bottom-centre, transient) ---- */
.toasts {
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); z-index: 200;
display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none;
}
.toast {
display: flex; align-items: center; gap: 9px;
background: var(--tip-bg); color: var(--tip-text); font-size: 13px;
padding: 9px 15px; border-radius: 9px; box-shadow: var(--shadow-md);
opacity: 0; transform: translateY(8px);
transition: opacity .22s var(--ease-3), transform .22s var(--ease-3);
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast-ico { color: #37b24d; display: inline-flex; }
/* Theme toggle */
.theme-toggle {
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);
color: var(--text); width: 34px; height: 34px; font-size: 16px; line-height: 1; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center;
transition: border-color var(--transition), background var(--transition);
}
.theme-toggle:hover { border-color: var(--border-strong); background: var(--surface); }
/* Anti-flash: until the role is resolved (Keycloak check-sso), hide every
manager-only control so anonymous visitors never see them flash on load.
applyRoleUi() drops .role-pending and then sets each element's final state. */
html.role-pending .manager-only { display: none !important; }
/* Login / logout button — pinned to the header's top-right corner */
.auth-wrap { position: absolute; right: 18px; top: 12px; z-index: 3; }
/* Login / logout button (mirrors the toggle styling, sized to its text label) */
.auth-btn {
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg);
color: var(--text); height: 34px; padding: 0 14px; font-size: 13px; font-weight: 600;
line-height: 1; cursor: pointer; display: inline-flex; align-items: center;
transition: border-color var(--transition), background var(--transition);
}
.auth-btn:hover { border-color: var(--border-strong); background: var(--surface); }
/* Anonymous "Войти": filled accent for visibility; hover inverts to white with
an accent outline. */
.auth-btn--login { background: var(--accent); border-color: var(--accent); color: #fff; }
.auth-btn--login:hover { background: var(--bg); border-color: var(--accent); color: var(--accent); }
/* Dark theme temporarily hidden — toggle stays in the DOM, just not shown.
Re-enable by removing this rule + restoring the head script in index.html. */
.theme-toggle { display: none; }
/* Top-align the mode switch with the field labels / brand instead of centring it
vertically, so it lines up with the rest of the header row. */
.hgroup-modes { gap: 10px; align-items: stretch; }
#view { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; --split-left: 50%; background: var(--bg); }
#pane-timeline { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
#pane-map { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
#map { width: 100%; height: 100%; }
/* timeline-only */
#view.mode-timeline #pane-timeline { flex: 1 1 auto; }
#view.mode-timeline #pane-map,
#view.mode-timeline #split-resizer { display: none; }
/* map-only */
#view.mode-map #pane-map { flex: 1 1 auto; }
#view.mode-map #pane-timeline,
#view.mode-map #split-resizer { display: none; }
/* split */
#view.mode-split #pane-timeline { flex: 0 0 var(--split-left); }
#view.mode-split #pane-map { flex: 1 1 auto; }
#view.mode-split #split-resizer { display: block; }
#split-resizer {
flex: 0 0 6px; background: var(--border); cursor: col-resize; position: relative;
transition: background var(--transition);
}
#split-resizer:hover { background: var(--accent); }
/* Donut cluster icon (built in map.ts) — centre the 60px svg on the geo point. */
.mapdash-cluster { width: 60px; height: 60px; margin: -30px 0 0 -30px; line-height: 0; cursor: pointer; }
/* ---- stat tiles (header counter + map legend) ---- */
.stat-tile {
display: inline-flex; flex-direction: column; align-items: flex-start; gap: 1px;
background: var(--surface); border-radius: 9px; padding: 5px 11px;
font-variant-numeric: tabular-nums; border: none;
}
.st-l { font-size: 11px; color: var(--text-muted); display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.st-v { font-size: 18px; font-weight: 600; line-height: 1.15; color: var(--text); }
.stat-accent { background: #eef0fe; }
.stat-accent .st-v { color: var(--accent); }
.st-v.st-free { color: #2f9e44; }
.st-v.st-busy { color: #d64545; }
#map-legend {
position: absolute; left: 10px; bottom: 44px; z-index: 5;
display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px;
}
#map-legend .stat-tile { box-shadow: var(--shadow-md); }
#map-legend .lg-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
#map-legend .lg-free { background: #37b24d; }
#map-legend .lg-busy { background: #e24b4a; }
#map-legend .lg-total { background: var(--border-strong); }
/* Actionable "no coordinates" tile — dashed accent, clickable. */
.st-nc { cursor: pointer; background: #fbf6e9; border: 1px dashed #d7c08a; padding: 4px 10px; text-align: left;
transition: border-color var(--transition); }
.st-nc .lg-dot { background: #e8a317; }
.st-nc .st-v { color: #b07d16; }
.st-nc:hover { border-color: #b07d16; }
/* Panel listing surfaces missing coordinates. */
#map-nocoords-panel {
position: absolute; left: 10px; bottom: 150px; z-index: 6;
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow-md); padding: 8px 10px; font-size: 12px; color: var(--text);
max-width: 300px; max-height: 300px; overflow-y: auto;
}
#map-nocoords-panel .nc-head {
font-weight: 600; color: var(--text-muted); margin-bottom: 6px;
text-transform: uppercase; font-size: 11px; letter-spacing: .04em;
}
#map-nocoords-panel .nc-list { display: flex; flex-wrap: wrap; gap: 4px 6px; }
#map-nocoords-panel .nc-code {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
padding: 2px 6px; font-size: 12px; white-space: nowrap;
}
/* Muted map tiles so green/red markers stand out (ground pane only, not markers). */
[class*="-ground-pane"] { filter: saturate(0.72) brightness(1.02); }
/* Balloon card (click) — matches the timeline tooltip structure. */
.bl { font-size: 13px; color: var(--text); line-height: 1.4; min-width: 190px; }
.bl-sub { color: var(--text-muted); }
.bl-status { display: flex; align-items: center; gap: 7px; font-weight: 600; margin: 6px 0; }
.bl-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.bl-sep { height: 1px; background: var(--border); margin: 8px 0; }
.bl-label { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin-bottom: 4px; }
.bl-booking { font-size: 12px; margin: 3px 0; }
.bl-dates { color: var(--text-muted); }
.bl-link { color: var(--accent); text-decoration: none; font-weight: 600; }
.bl-link:hover { text-decoration: underline; }
/* Balloon photo gallery: main image + thumbnail strip; opens the lightbox. */
.bl-photos { margin: 2px 0 9px; }
.bl-photo { position: relative; height: 168px; border-radius: 8px; overflow: hidden; background: var(--surface-2); cursor: zoom-in; }
.bl-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bl-count { position: absolute; top: 8px; right: 8px; background: rgba(17,20,26,.72); color: #fff; font-size: 11px; padding: 2px 8px; border-radius: 20px; }
.bl-exp { position: absolute; bottom: 8px; right: 8px; background: rgba(17,20,26,.72); color: #fff; width: 26px; height: 26px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
.bl-thumbs { display: flex; gap: 6px; margin-top: 6px; }
.bl-thumb { width: 62px; height: 42px; padding: 0; border-radius: 6px; overflow: hidden; cursor: pointer; border: 2px solid transparent; background: var(--surface-2); }
.bl-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bl-thumb-act { border-color: var(--accent); }
.bl-more { width: 62px; height: 42px; border-radius: 6px; border: none; background: var(--surface-2); color: var(--text-muted); font-size: 13px; cursor: pointer; }
/* Fullscreen photo lightbox above the map. */
.lb-overlay { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.85); display: none; align-items: center; justify-content: center; }
.lb-overlay.open { display: flex; }
.lb-img { max-width: 92vw; max-height: 88vh; object-fit: contain; border-radius: 4px; }
.lb-close, .lb-prev, .lb-next { position: absolute; background: rgba(255,255,255,.12); color: #fff; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
.lb-close:hover, .lb-prev:hover, .lb-next:hover { background: rgba(255,255,255,.24); }
.lb-close { top: 18px; right: 20px; width: 40px; height: 40px; font-size: 20px; }
.lb-prev, .lb-next { top: 50%; transform: translateY(-50%); width: 46px; height: 66px; font-size: 30px; }
.lb-prev { left: 18px; }
.lb-next { right: 18px; }
.lb-count { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #fff; font-size: 13px; background: rgba(0,0,0,.4); padding: 3px 12px; border-radius: 20px; }
/* Custom surface panel (replaces the Yandex balloon): fits content, draggable
by the header, resizable from the bottom-right corner. */
.mpanel {
position: fixed; z-index: 60; width: 320px; height: auto;
min-width: 240px; min-height: 130px; max-width: 92vw; max-height: 84vh;
overflow: auto; resize: both;
background: var(--bg); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow-md);
font-size: 13px; color: var(--text);
animation: pop-in .13s var(--ease-3);
}
.mpanel-head {
position: sticky; top: 0; z-index: 1; background: var(--bg);
display: flex; align-items: center; justify-content: space-between; gap: 10px;
padding: 9px 12px; border-bottom: 1px solid var(--border); cursor: move;
border-radius: var(--radius) var(--radius) 0 0;
}
.mpanel-title { font-weight: 700; font-size: 15px; }
.mpanel-close { border: none; background: none; color: var(--text-muted); font-size: 16px; line-height: 1; cursor: pointer; padding: 0 2px; }
.mpanel-close:hover { color: var(--text); }
.mpanel-tabs { display: flex; flex-wrap: wrap; gap: 6px; padding: 9px 12px 0; }
.mpanel-tab { border: 1px solid var(--border); background: var(--surface); color: var(--accent); border-radius: var(--radius-sm); padding: 3px 9px; font-size: 12px; cursor: pointer; }
.mpanel-tab-act { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }
.mpanel-body { padding: 10px 12px 14px; }
#map-empty.empty-state { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 5; }
/* Custom hover tooltip for map markers and clusters. */
.map-tip {
position: fixed; z-index: 1000; pointer-events: none;
background: var(--tip-bg); color: var(--tip-text); font-size: 12px; line-height: 1.35;
padding: var(--size-2); border-radius: var(--radius-sm); max-width: 340px;
max-height: calc(100vh - 24px); overflow: hidden;
box-shadow: var(--shadow-md);
}
.map-tip .tip-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 6px; vertical-align: baseline; }
.map-tip .tip-free { background: #59a831; }
.map-tip .tip-busy { background: #e35b45; }
.map-tip .tip-row { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.map-tip .tip-head { font-weight: 700; margin-bottom: 3px; }
.map-tip .tip-sub { color: var(--tip-sub); }
.map-tip .tip-brand { color: #f2a08f; }
.map-tip .tip-more { color: var(--tip-sub); margin-top: 3px; }
#chart-wrap { flex: 1 1 auto; min-height: 0; position: relative; overflow: hidden; }
#timeline { border: none; }
/* vis-timeline bars: colour comes from each item's inline style (per status /
collision). Height and vertical centring come from --bar-h + symmetric vis
margin (see appearance.ts marginVertical); flex centres the text too. */
/* The vis item spans the FULL row height and is transparent; the visible bar
is an inner div (.bar-inner) of --bar-h height, flex-centred. This makes
vertical centring independent of vis-timeline's own item positioning. */
.vis-item.vis-range {
height: var(--row-h);
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0;
display: flex;
align-items: center;
cursor: pointer;
}
.vis-item.vis-range .vis-item-overflow { display: flex; align-items: center; height: 100%; width: 100%; overflow: hidden; }
.vis-item.vis-range .vis-item-content { padding: 0; width: 100%; }
.bar-inner {
position: relative;
height: var(--bar-h);
width: 100%;
box-sizing: border-box;
border-radius: 4px;
display: flex;
align-items: center;
padding: 0 6px;
color: #FFFFFF;
font-size: var(--bar-font);
line-height: 1;
white-space: nowrap;
overflow: hidden;
}
.vis-item.vis-range:hover .bar-inner { filter: brightness(0.92); }
/* Label wider than the bar: JS adds .bar-of (label past the bar's right edge) or
.bar-of-left (past the left edge, when the right side would hit the next bar),
as dark text on the track instead of clipping inside the bar. */
.vis-item.vis-range.bar-of .vis-item-overflow,
.vis-item.vis-range.bar-of-left .vis-item-overflow { overflow: visible; }
.vis-item.vis-range.bar-of .bar-inner,
.vis-item.vis-range.bar-of-left .bar-inner { overflow: visible; }
.vis-item.vis-range.bar-of .bar-text,
.vis-item.vis-range.bar-of-left .bar-text {
position: absolute; top: 50%; transform: translateY(-50%);
color: var(--text); white-space: nowrap; pointer-events: none;
}
.vis-item.vis-range.bar-of .bar-text { left: 100%; margin-left: 5px; }
.vis-item.vis-range.bar-of-left .bar-text { right: 100%; margin-right: 5px; text-align: right; }
.vis-foreground .vis-group { min-height: var(--row-h); }
.vis-labelset .vis-label { font-size: var(--label-font); display: flex; align-items: center; min-height: var(--row-h); }
/* Zebra striping */
.vis-label.row-even, .vis-group.row-even { background-color: var(--row-even); }
.vis-label.row-odd, .vis-group.row-odd { background-color: var(--row-odd); }
/* Row hover */
.vis-label.row-hover, .vis-group.row-hover { background-color: var(--row-hover) !important; }
/* Fixed/resizable label column */
.vis-label .vis-inner {
display: inline-block;
min-width: var(--label-w);
max-width: var(--label-w);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle;
}
/* vis-timeline chrome — themed so it reads in dark mode too. */
.vis-timeline { border-color: var(--border); }
.vis-panel.vis-center, .vis-panel.vis-left, .vis-panel.vis-right,
.vis-panel.vis-top, .vis-panel.vis-bottom { border-color: var(--border); }
.vis-time-axis .vis-text { color: var(--text-muted); }
.vis-time-axis .vis-grid.vis-minor { border-color: var(--grid-line); }
.vis-time-axis .vis-grid.vis-major { border-color: var(--border-strong); }
.vis-labelset .vis-label,
.vis-foreground .vis-group { border-color: var(--grid-line); }
.vis-current-time { background-color: var(--today); }
/* Column resize handle */
/* Matches #split-resizer: a 6px solid bar that turns accent on hover/drag, so
both dividers look identical. */
#col-resizer {
position: absolute; top: 0; bottom: 0; width: 6px; margin-left: -3px;
background: var(--border); cursor: col-resize; z-index: 25; display: none;
transition: background var(--transition);
}
#col-resizer:hover, #col-resizer.dragging { background: var(--accent); }
/* Hovered-month column highlight */
.vis-item.vis-background.month-hover-bg { background-color: var(--month-hover); }
/* Rich hover tooltip for timeline bars (structured HTML, built in timeline.ts).
Shares the dark look of the map tooltip (.map-tip) for a consistent feel. */
#tooltip { position: fixed; display: none; pointer-events: none; z-index: 100; max-width: 340px; }
.tt {
background: var(--tip-bg); color: var(--tip-text);
border-left: 3px solid var(--tt-accent, var(--accent));
border-radius: var(--radius-sm); box-shadow: var(--shadow-md);
padding: 9px 12px; font-size: 12px; line-height: 1.4; min-width: 170px;
}
.tt-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.tt-title { font-weight: 700; font-size: 13px; }
.tt-id { color: var(--tip-sub); font-size: 11px; white-space: nowrap; }
.tt-sub { color: var(--tip-sub); margin-top: 1px; }
.tt-sep { height: 1px; background: rgba(255, 255, 255, .14); margin: 7px 0; }
.tt-row { display: flex; align-items: center; gap: 7px; margin: 3px 0; }
.tt-ico { width: 15px; text-align: center; opacity: .85; flex: 0 0 auto; }
.tt-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; margin: 0 3px; }
.tt-warn { margin-top: 8px; padding-top: 7px; border-top: 1px solid rgba(255, 255, 255, .14); }
.tt-warn-head { color: #ffcc66; font-weight: 600; margin-bottom: 3px; }
.tt-warn-row { font-size: 11.5px; color: #f0d9b0; margin: 2px 0; }
.tt-warn-dates { color: var(--tip-sub); }
.tt-hint { margin-top: 9px; color: var(--tip-sub); font-size: 11px; }
/* ---- empty states (timeline + map) ---- */
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: var(--size-2); text-align: center; color: var(--text-muted); padding: var(--size-5);
}
#empty.empty-state { position: absolute; inset: 0; }
.empty-ico { width: 40px; height: 40px; color: var(--border-strong); }
.empty-title { font-size: 15px; font-weight: 600; color: var(--text); }
.empty-sub { font-size: 13px; color: var(--text-muted); }
.empty-reset {
margin-top: var(--size-1); border: 1px solid var(--border); background: var(--bg); color: var(--accent);
font-size: 13px; font-weight: 500; padding: 7px 14px; border-radius: var(--radius-sm); cursor: pointer;
transition: border-color var(--transition), background var(--transition);
}
.empty-reset:hover { border-color: var(--accent); background: var(--accent-weak); }
/* ---- loading bar (delayed, indeterminate) ---- */
#load-bar {
position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 200; overflow: hidden;
opacity: 0; transition: opacity .2s var(--ease-3); pointer-events: none;
}
#load-bar.active { opacity: 1; }
#load-bar::before {
content: ""; position: absolute; top: 0; bottom: 0; width: 35%;
background: var(--accent); border-radius: 0 3px 3px 0; animation: loadbar 1.1s var(--ease-3) infinite;
}
@keyframes loadbar { 0% { left: -35%; } 100% { left: 100%; } }
/* ---- active-filter chips ---- */
.chips {
flex: 0 0 auto; display: flex; flex-wrap: wrap; align-items: center; gap: var(--size-2);
padding: var(--size-2) var(--size-3); border-bottom: 1px solid var(--border); background: var(--bg); z-index: 15;
}
.chip {
display: inline-flex; align-items: center; gap: 5px;
background: var(--accent-weak); color: var(--accent);
font-size: 12px; font-weight: 500; padding: 3px 4px 3px 10px; border-radius: 999px; white-space: nowrap;
}
.chip-x {
border: none; background: transparent; color: inherit; cursor: pointer; line-height: 1; font-size: 15px;
width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
transition: background var(--transition);
}
.chip-x:hover { background: rgba(79, 70, 229, .18); }
.chips-clear {
border: none; background: transparent; color: var(--text-muted); cursor: pointer;
font-size: 12px; font-weight: 500; padding: 3px 8px; border-radius: var(--radius-sm); margin-left: 2px;
transition: color var(--transition), background var(--transition);
}
.chips-clear:hover { color: var(--text); background: var(--surface-2); }
.chips-share {
border: none; background: transparent; color: var(--accent); cursor: pointer;
font-size: 12px; font-weight: 500; padding: 3px 8px; border-radius: var(--radius-sm);
transition: background var(--transition);
}
.chips-share:hover { background: var(--accent-weak); }
.chips-divider { color: var(--border-strong); font-weight: 400; user-select: none; }
/* ---- polish: focus rings, animations, scrollbars, tabular figures ---- */
button:focus-visible, input[type=text]:focus-visible, input[type=date]:focus-visible,
.dropdown-btn:focus-visible, .mode-btn:focus-visible, a:focus-visible {
outline: none; box-shadow: 0 0 0 3px var(--accent-ring);
}
input[type=checkbox]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.dropdown.open .dropdown-panel, .decor-menu.open { animation: pop-in .13s var(--ease-3); }
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.mode-btn:active, .decor-btn:active, .empty-reset:active, .chips-clear:active, .lg-nocoords:active { transform: scale(.97); }
.dropdown-panel, #map-nocoords-panel, .decor-menu { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.dropdown-panel::-webkit-scrollbar, #map-nocoords-panel::-webkit-scrollbar, .decor-menu::-webkit-scrollbar { width: 9px; }
.dropdown-panel::-webkit-scrollbar-thumb, #map-nocoords-panel::-webkit-scrollbar-thumb, .decor-menu::-webkit-scrollbar-thumb {
background: var(--border-strong); border-radius: 999px; border: 2px solid var(--bg);
}
.vis-time-axis .vis-text, #tooltip, .map-tip, .bl, input[type=date] { font-variant-numeric: tabular-nums; }
/* ---- board detail panel (slide-in from the right) ---- */
.bp-backdrop { position: fixed; inset: 0; background: rgba(15, 20, 32, .35); z-index: 300; }
.board-panel {
position: fixed; top: 0; right: 0; bottom: 0; width: 380px; max-width: 92vw; z-index: 301;
background: var(--bg); border-left: 1px solid var(--border); box-shadow: var(--shadow-md);
display: flex; flex-direction: column; animation: slide-in .18s var(--ease-3);
}
.board-panel[hidden], .bp-backdrop[hidden] { display: none; }
@keyframes slide-in { from { transform: translateX(24px); opacity: .5; } to { transform: none; opacity: 1; } }
.bp-head {
display: flex; align-items: center; justify-content: space-between; gap: var(--size-2);
padding: var(--size-3); border-bottom: 1px solid var(--border);
}
.bp-title { font-size: 16px; font-weight: 600; word-break: break-word; }
.bp-close {
border: none; background: transparent; color: var(--text-muted); font-size: 17px; cursor: pointer;
width: 30px; height: 30px; border-radius: var(--radius-sm); flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center; transition: background var(--transition), color var(--transition);
}
.bp-close:hover { background: var(--surface-2); color: var(--text); }
.bp-body { padding: var(--size-3); overflow-y: auto; flex: 1 1 auto; }
.bp-meta { font-size: 13px; color: var(--text-muted); margin-bottom: 2px; }
.bp-label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); font-weight: 600; margin: var(--size-3) 0 var(--size-2); }
.bp-empty { color: var(--text-muted); font-size: 13px; }
.bp-booking { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--size-2) var(--size-3); margin-bottom: var(--size-2); }
.bp-b-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--size-2); }
.bp-b-title { font-size: 14px; font-weight: 500; }
.bp-b-link { font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 500; white-space: nowrap; }
.bp-b-link:hover { text-decoration: underline; }
.bp-b-sub { font-size: 12px; color: var(--text-muted); margin-top: 1px; }
.bp-b-dates { font-size: 13px; margin-top: 4px; font-variant-numeric: tabular-nums; }
.bp-b-row { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-muted); margin-top: 4px; }
.bp-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }