:root {
  --bg: #f6f7fb;
  --sidebar-bg: #ffffff;
  --panel: #ffffff;
  --panel-2: #f2f3f8;
  --panel-float: rgba(255, 255, 255, 0.4);
  --border: #e5e7eb;
  --text: #111827;
  --text-dim: #6b7280;
  --text-faint: #9aa1b1;
  --accent: #6366f1;
  --accent-hover: #4f46e5;
  --accent-soft: #eef0fe;
  --danger: #ef4444;
  --danger-soft: #fee2e2;
  --danger-border: #fca5a5;
  --danger-text: #b91c1c;
  --radius: 10px;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 1px 3px rgba(16, 24, 40, 0.06);
}

/* dark theme: a near-black plum/charcoal palette with a red accent echoing the logo's red */
[data-theme="dark"] {
  --bg: #030203;
  --sidebar-bg: #08060a;
  --panel: #08060a;
  --panel-2: #0e0b11;
  --panel-float: rgba(8, 6, 10, 0.4);
  --border: #1a1620;
  --text: #ece9ef;
  --text-dim: #a39cae;
  --text-faint: #756e82;
  --accent: #d5453f;
  --accent-hover: #b8332e;
  --accent-soft: rgba(213, 69, 63, 0.16);
  --danger: #e0752e;
  --danger-soft: rgba(224, 117, 46, 0.14);
  --danger-border: rgba(224, 117, 46, 0.4);
  --danger-text: #f0af7c;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 2px 10px rgba(0, 0, 0, 0.4);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

a { color: var(--accent); }

svg { display: block; }

/* ---------- auth pages ---------- */

.auth-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background:
    radial-gradient(560px circle at 12% 8%, var(--accent-soft), transparent 60%),
    radial-gradient(480px circle at 88% 94%, var(--accent-soft), transparent 55%),
    var(--bg);
}

.auth-container {
  width: 100%;
  max-width: 380px;
  padding: 36px 34px 30px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 18px;
  box-shadow: 0 20px 50px -12px rgba(16, 24, 40, 0.16), var(--shadow);
}
[data-theme="dark"] .auth-container { box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.55), var(--shadow); }

.auth-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 24px;
}
/* single image already bakes in the wordmark (see logo-full-light/dark.png) — replaced the
   old separate icon + CSS-text pairing, one file instead of two things to keep in sync */
.auth-logo-full { height: 40px; width: auto; }
.main-header .auth-logo-full { height: 48px; }

.auth-container h1 {
  margin: 0 0 4px;
  font-size: 21px;
  font-weight: 700;
  letter-spacing: -0.2px;
}

.auth-subtitle { margin: 0 0 22px; font-size: 13.5px; color: var(--text-faint); }

.field { margin-bottom: 16px; }
.field label {
  display: block;
  margin-bottom: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
}
.field input { margin: 0; }
.field-hint { margin: 6px 0 0; font-size: 12px; color: var(--text-faint); }

.auth-submit { width: 100%; justify-content: center; padding: 10px 16px; font-size: 14.5px; margin-top: 6px; }

.auth-footer { text-align: center; margin: 20px 0 0; font-size: 13.5px; color: var(--text-dim); }
.auth-footer a { font-weight: 600; }

.login-required-modal { width: 380px; }
.login-required-text { margin: 0 0 16px; font-size: 14px; color: var(--text-dim); line-height: 1.5; }

.error-msg {
  background: var(--danger-soft);
  border: 1px solid var(--danger-border);
  color: var(--danger-text);
  padding: 10px 14px;
  border-radius: 8px;
  margin-bottom: 16px;
  font-size: 14px;
}

.success-msg {
  background: rgba(34, 197, 94, 0.12);
  border: 1px solid rgba(34, 197, 94, 0.35);
  color: #16a34a;
  padding: 10px 14px;
  border-radius: 8px;
  margin-bottom: 16px;
  font-size: 14px;
}

.empty-state { color: var(--text-faint); text-align: center; padding: 48px 0; font-size: 14px; }

/* ---------- form controls ---------- */

input[type="text"], input[type="password"], input[type="file"], input[type="search"], input[type="email"] {
  width: 100%;
  padding: 10px 12px;
  margin: 6px 0 16px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
}

input[type="text"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="email"]:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--panel);
}

/* Safari/Chrome draw their own search-field chrome (rounded native pill, decorative icons)
   on top of type="search" regardless of the box styling above unless this is reset */
input[type="search"] { -webkit-appearance: none; appearance: none; }
input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

label { font-size: 13px; color: var(--text-dim); }

button, .btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 8px;
  padding: 9px 16px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s;
  font-family: inherit;
}

button:hover, .btn:hover { background: var(--accent-hover); }

button.secondary, .btn.secondary {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
}
button.secondary:hover, .btn.secondary:hover { background: var(--panel-2); }

/* pressed/toggled state for a standalone toolbar button (not part of a .tabs-segment, which
   already has its own .active rule) — e.g. the "Метки" panel-visibility toggle */
button.secondary.active, .btn.secondary.active {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: var(--accent);
}
button.secondary.active:hover, .btn.secondary.active:hover { background: var(--accent-soft); }

/* compact icon-only variant of a toolbar button — same border/background language as
   .secondary, just square and padding-free so a single glyph sits centered (e.g. reset-zoom) */
button.icon-square-btn, .btn.icon-square-btn {
  width: 36.5px;
  height: 36.5px;
  padding: 0;
  gap: 0;
  justify-content: center;
}

button.danger { background: var(--danger); }
button.danger:hover { background: #dc2626; }

button:disabled, .btn.disabled {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* ---------- app shell ---------- */

.app-shell { display: flex; min-height: 100vh; }

/* compact icon-rail sidebar — big icon on top, label below, instead of the old icon-left/
   text-right row layout, so it takes noticeably less horizontal width away from page content
   (the log viewer's chart especially) while every destination is still one glance to identify */
.sidebar {
  width: 84px; /* wide enough for .sidebar-brand's 72px logo plus the existing 6px side padding */
  flex-shrink: 0;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 14px 6px;
}

.sidebar-brand {
  width: 72px;
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 0 16px;
  text-decoration: none;
  flex-shrink: 0;
}
.sidebar-brand img { width: 100%; height: 100%; object-fit: contain; }

.sidebar-nav { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%; }

.sidebar-nav a, .nav-stub, .feedback-nav-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
  padding: 8px 2px 7px;
  border-radius: 10px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 10px;
  font-weight: 500;
  text-align: center;
}

.sidebar-nav a.active { background: var(--accent-soft); color: var(--accent); }
.sidebar-nav a:hover:not(.active) { background: var(--panel-2); color: var(--text); }
.nav-stub { cursor: default; opacity: 0.5; }
.feedback-nav-btn { background: none; border: none; cursor: pointer; font-family: inherit; }
.feedback-nav-btn:hover { background: var(--panel-2); color: var(--text); }

.nav-icon { width: 21px; height: 21px; flex-shrink: 0; }
.nav-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

.nav-badge {
  font-size: 8px;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--text-faint);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2px;
}

.sidebar-footer { margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border); width: 100%; }

/* compact light/dark switch pinned under the account block — the rail is only ~72px wide, so
   this is icons-either-side-of-a-track rather than the labeled segmented control settings used */
.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 100%;
  margin-bottom: 10px;
  padding: 7px 4px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: none;
  color: var(--text-faint);
  cursor: pointer;
}
.theme-toggle:hover { color: var(--text); border-color: var(--text-faint); }
.theme-toggle-icon { width: 12px; height: 12px; flex-shrink: 0; }
.theme-toggle-track {
  position: relative;
  width: 22px;
  height: 12px;
  border-radius: 999px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.theme-toggle-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  transition: transform 0.15s;
}
[data-theme="dark"] .theme-toggle-thumb { transform: translateX(10px); }

.sidebar-user { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 8px 2px; text-align: center; }

.avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 13px;
  flex-shrink: 0;
  overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }

.avatar-upload-spinner {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(17, 24, 39, 0.55);
}
.avatar-upload-spinner[hidden] { display: none; }
.avatar-upload-spinner .spinner-ring {
  width: 46%;
  height: 46%;
  border-radius: 50%;
  border: 2.5px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  animation: tunelog-spin 0.7s linear infinite;
}
@keyframes tunelog-spin { to { transform: rotate(360deg); } }

.sidebar-user .info { min-width: 0; width: 100%; }
.sidebar-user .username {
  font-size: 11px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-user-links { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: 2px; }

.linklike {
  background: none;
  border: none;
  padding: 0;
  color: var(--text-faint);
  font-size: 10.5px;
  cursor: pointer;
  font-family: inherit;
}
.linklike:hover { color: var(--danger); background: none; }
.linklike.settings-link { font-size: 12.5px; font-weight: 600; color: var(--text-dim); }
.linklike.settings-link:hover { color: var(--accent); }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; position: relative; }

.main-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 28px;
  border-bottom: 1px solid var(--border);
  gap: 16px;
  background: var(--panel);
}

.main-header h1 { font-size: 18px; margin: 0; }

.search-input-wrap { position: relative; flex: 1; max-width: 420px; }
input.search-input {
  width: 100%;
  padding: 9px 14px 9px 36px;
  margin: 0;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--panel-2);
  font-size: 14px;
  color: var(--text);
}
input.search-input:focus { outline: none; border-color: var(--accent); background: var(--panel); }
.search-icon {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--text-faint);
}

.main-body { padding: 24px 28px; flex: 1; }

/* ---------- log table ---------- */

.log-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  box-shadow: var(--shadow);
}

.log-table thead th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-faint);
  font-weight: 700;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--panel-2);
}

.log-table tbody tr { border-bottom: 1px solid var(--border); transition: opacity 0.1s; }
.log-table tbody tr:last-child { border-bottom: none; }
.log-table tbody tr:hover { background: var(--panel-2); }
.log-table tbody tr.row-dimmed { opacity: 0.3; }
.log-table td { padding: 13px 16px; font-size: 14px; vertical-align: middle; }

.log-title { font-weight: 600; color: var(--text); text-decoration: none; }
.log-title:hover { color: var(--accent); }
.log-sub { font-size: 12px; color: var(--text-faint); margin-top: 2px; }
.col-added, .col-params { color: var(--text-dim); white-space: nowrap; }
.col-actions { text-align: right; white-space: nowrap; }

.row-actions { display: inline-flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
.log-table tr:hover .row-actions { opacity: 1; }

.icon-btn {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 7px;
  border: 1px solid transparent;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  padding: 0;
}
.icon-btn:hover { background: var(--panel-2); color: var(--text); }
.icon-btn.danger:hover { background: var(--danger-soft); color: var(--danger); }

/* ---------- upload modal ---------- */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(17, 24, 39, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}
.modal-overlay.open { opacity: 1; pointer-events: auto; }

.modal {
  background: var(--panel);
  border-radius: 14px;
  width: 480px;
  max-width: 90vw;
  box-shadow: 0 20px 60px rgba(17, 24, 39, 0.25);
  padding: 20px;
}

.modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.modal-header h2 { font-size: 16px; margin: 0; display: flex; align-items: center; gap: 8px; }

.modal-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-faint);
  width: 28px;
  height: 28px;
  border-radius: 7px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.modal-close:hover { background: var(--panel-2); }

.dropzone {
  border: 2px dashed var(--border);
  border-radius: 12px;
  padding: 40px 20px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); background: var(--accent-soft); }
.dropzone svg { width: 30px; height: 30px; color: var(--text-faint); margin: 0 auto 10px; }
.dropzone.has-file svg { color: var(--accent); }
.dropzone .dz-title { font-weight: 600; font-size: 14px; color: var(--text); }
.dropzone.has-file .dz-title { color: var(--accent); }
.dropzone .dz-sub { font-size: 12px; color: var(--text-faint); margin-top: 4px; }

.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
.modal input[type="text"] { margin: 6px 0 0; }
.modal label { display: block; margin-top: 14px; }

/* channel-mapping modal (see #compareMapOverlay in log.ejs, openCompareMapModal in log.js) */
.compare-map-hint { font-size: 12.5px; color: var(--text-dim); margin-top: 12px; line-height: 1.5; }
/* set only when auto-opened right after picking a mismatched compare log (see loadCompareData)
   — explains why the modal appeared unprompted, instead of just showing the generic hint that
   assumes the user opened it on purpose via the "Каналы" button */
.compare-map-hint.auto-opened { color: var(--accent-hover); font-weight: 600; }
.compare-map-list {
  margin-top: 10px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 10px;
  /* an odd row count would otherwise leave the last one stretched across both tracks */
  align-items: start;
}
.compare-map-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  min-width: 0;
}
/* flagged when this channel currently has no compare overlay at all (no same-name match and no
   manual choice yet) — the one state that actually needs the user's attention */
.compare-map-row.needs-mapping { border-color: var(--danger-border); background: var(--danger-soft); }
/* flex:1 so the name claims whatever room the (fixed-width) select next to it doesn't need,
   instead of shrinking to make way for it first — real channel names run long enough (e.g.
   "Torque act. clutch (Nm)") that without this they were the first thing to get truncated */
.compare-map-row-name { flex: 1 1 auto; min-width: 0; font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.compare-map-row select {
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 6px 8px;
  font-size: 13px;
  background: var(--panel);
  color: var(--text);
  font-family: inherit;
  flex: 0 0 150px;
  min-width: 0;
}

.formula-hint { font-size: 12px; color: var(--text-faint); margin-top: 8px; line-height: 1.5; }
.formula-hint code {
  background: var(--panel-2);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 11.5px;
  color: var(--text-dim);
}
.formula-channel-list {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 5px;
  margin-top: 10px;
  max-height: 280px;
  overflow-y: auto;
}
.formula-channel-chip {
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--text-dim);
  font-size: 11px;
  cursor: pointer;
  user-select: none;
  transition: opacity 0.1s;
}
.formula-channel-chip:hover { border-color: var(--accent); color: var(--accent-hover); }
/* search dims non-matches instead of removing them — same pattern as the main channel list */
.formula-channel-chip.dimmed { opacity: 0.3; }

.formula-color-input {
  margin-top: 6px;
  width: 44px;
  height: 30px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  cursor: pointer;
}

/* outline-styled danger action (delete) inside a modal — distinct from the solid .danger
   button used for one-shot destructive confirms elsewhere, since this sits alongside plain
   .secondary buttons and shouldn't visually shout louder than the primary submit action */
button.danger-outline, .btn.danger-outline {
  background: var(--panel);
  color: var(--danger-text);
  border: 1px solid var(--danger-border);
}
button.danger-outline:hover, .btn.danger-outline:hover { background: var(--danger-soft); }

/* ---------- log viewer ---------- */

/* dvh, not vh: on iOS Safari 100vh is the LARGE viewport — the height the page would have if
   the address bar were hidden — so the bottom of the chart ends up underneath that bar. dvh
   tracks the bar collapsing and expanding instead. The vh line stays first as the fallback for
   browsers without dvh. .app-shell is pinned to the same height on this page specifically,
   otherwise its own min-height:100vh makes the page scroll by exactly the address bar's height. */
.log-viewer-page .app-shell { min-height: 100vh; min-height: 100dvh; }
.viewer { display: flex; flex-direction: column; height: 100vh; height: 100dvh; position:relative; }

.viewer-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 24px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  gap: 16px;
  flex-shrink: 0;
}

.breadcrumb { font-size: 14px; display: flex; align-items: center; gap: 6px; min-width: 0; }
.breadcrumb a { color: var(--text-dim); text-decoration: none; }
.breadcrumb a:hover { color: var(--accent); }
.breadcrumb .sep { color: var(--text-faint); }
.breadcrumb .current {
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-count-label { color: var(--text-faint); flex-shrink: 0; white-space: nowrap; }

.topbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

.tabs-segment { display: flex; background: var(--panel-2); border-radius: 9px; padding: 3px; gap: 2px; }
.tabs-segment button {
  background: none;
  color: var(--text-dim);
  border: none;
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 7px;
  box-shadow: none;
}
.tabs-segment button.active { background: var(--panel); color: var(--accent); box-shadow: 0 1px 2px rgba(16, 24, 40, 0.08); }
.tabs-segment button:disabled { opacity: 0.4; cursor: not-allowed; }
.tabs-segment button:hover:not(.active):not(:disabled) { color: var(--text); background: none; }

.viewer-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 24px;
  border-bottom: 1px solid var(--border);
  background: var(--panel-2);
  flex-shrink: 0;
  /* anchors #splitXStretchWrap, which needs true dead-center regardless of how much content
     sits on either side of it */
  position: relative;
}

/* Comparison has its own secondary toolbar: the primary graph controls stay stable while the
   overlay's offset and remove actions appear immediately underneath them. */
.compare-toolbar {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 42px;
  padding: 7px 24px;
  border-bottom: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 13px;
}
.compare-toolbar-log { display: inline-flex; align-items: center; gap: 6px; margin-right: 8px; color: var(--text); font-weight: 600; }
.compare-toolbar-line { width: 16px; border-top: 2px dashed var(--text-dim); }
/* wide on purpose: the offset track can now span the whole distance between two logs' time
   columns, so it needs the pixels to stay draggable at that range (see updateCompareBarUI) */
.compare-toolbar input[type="range"] { width: clamp(200px, 32vw, 420px); }
.compare-offset-nudge { display: inline-flex; gap: 2px; }
.compare-offset-nudge button {
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1;
  box-shadow: none;
}
.compare-offset-nudge button:hover { border-color: var(--accent); color: var(--accent); }
.compare-toolbar input[type="number"] { width: 64px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 7px; font-size: 13px; background: var(--panel); color: var(--text); }
.compare-toolbar #compareOffsetResetBtn { padding: 5px 10px; font-size: 12px; }

.ai-review-popover {
  position: fixed;
  z-index: 220;
  width: min(390px, calc(100vw - 24px));
  max-height: min(70vh, 650px);
  overflow-y: auto;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: var(--shadow), 0 14px 36px rgba(16, 24, 40, 0.16);
}
.ai-review-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; color:var(--text); }
.ai-review-head .icon-btn { width:26px; height:26px; padding:0; font-size:20px; line-height:1; }
.ai-review-content { display:flex; flex-direction:column; gap:10px; }
.ai-review-status { color:var(--text); font-size:13px; font-weight:600; }
.ai-review-muted { color:var(--text-faint); font-size:12.5px; line-height:1.45; }
.ai-review-error { color:var(--danger); }
.ai-review-summary { color:var(--text); font-size:13px; line-height:1.5; }
.ai-review-engine { padding:9px 10px; border:1px solid var(--border); border-radius:7px; color:var(--text-dim); font-size:12.5px; line-height:1.45; }
.ai-review-engine strong { display:block; color:var(--text); font-size:12.5px; margin-bottom:3px; }
.ai-review-engine-evidence { margin-top:5px; color:var(--text-faint); font-size:11.5px; }
.ai-review-wot { display:flex; flex-direction:column; gap:5px; padding:9px 10px; border:1px solid var(--border); border-radius:7px; color:var(--text); font-size:12.5px; }
.ai-review-range-toggle { width:fit-content; padding:3px 0; border:0; border-radius:4px; background:none; color:var(--accent); font:inherit; font-size:12px; text-decoration:underline; text-underline-offset:2px; box-shadow:none; }
.ai-review-range-toggle:hover, .ai-review-range-toggle.active { color:var(--accent-hover); background:var(--accent-soft); padding:3px 5px; }
.ai-review-finding { border-left:3px solid var(--accent); border-radius:6px; background:var(--panel-2); padding:9px 10px; color:var(--text-dim); font-size:12.5px; line-height:1.45; cursor:pointer; }
.ai-review-finding:hover, .ai-review-finding.active { outline:1px solid var(--accent); background:var(--accent-soft); }
.ai-review-finding.warning { border-left-color:#f59e0b; }
.ai-review-finding.critical { border-left-color:var(--danger); }
.ai-review-finding strong { display:block; color:var(--text); font-size:13px; margin-bottom:3px; }
.ai-review-range { display:block; color:var(--text-faint); font-size:11.5px; margin-bottom:5px; }

.ai-review-action { align-self:flex-start; margin-top:2px; }

.axis-select { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-dim); }
.axis-select select {
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 6px 10px;
  font-size: 13px;
  background: var(--panel);
  color: var(--text);
  font-family: inherit;
}

.checkbox-label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.checkbox-label input[type="checkbox"] { accent-color: var(--accent); cursor: pointer; }

#xyPickerWrap { gap: 14px; }
.xy-picker-label { display: flex; align-items: center; gap: 6px; }

/* no vertical scroll in Single mode on purpose — mouse wheel over the chart zooms the Y axis
   instead (see attachWheelZoom in log.js/compare.js), so a scrollbar competing for the same
   gesture would be actively confusing. Split mode has no such wheel-zoom (each per-channel box
   is a separate small chart) and stacks an arbitrary number of boxes taller than the viewport,
   so it needs the opposite — .split-scroll (toggled by mode in log.js) re-enables it there. */
.chart-scroll {
  flex: 1;
  overflow: hidden;
  padding: 20px 24px;
  min-height: 0;
  /* Set by centerYStretchControls(): the chart deliberately claims part of the former
     control gutter, while the Y control stays centered in the smaller remaining gap. */
  margin-left: calc(-1 * var(--y-gutter-reduction, 0px));
  /* anchors .chart-row-gap (absolutely positioned, see below) */
  position: relative;
}
.chart-scroll.split-scroll { overflow-y: auto; overflow-x: hidden; }

/* per-axis stretch sliders (see attachYStretch/attachXStretch in log.js) — chart-frame holds
   the vertical Y slider to the left of the chart itself, chart-stretch-x is the horizontal X
   slider below the whole frame. Hidden outside Single mode via inline display (Split's multiple
   boxes don't have one shared Y to stretch). */
/* position:relative anchors .marker-panel/.marker-panel-hover-zone (absolutely positioned,
   see below) to chart-frame's own box specifically — not the whole .viewer — so their height
   comes for free from ordinary flex layout (chart-frame already shrinks correctly when the
   channel-bar below takes more/less room) instead of JS having to measure channel-bar's rect */
.chart-frame { display: flex; flex: 1; min-height: 0; position: relative; }
.chart-frame .chart-scroll { padding-left: 8px; }
.chart-stretch-y {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 28px;
  flex-shrink: 0;
  padding: 24px 0 32px;
  /* margin, not padding — box-sizing:border-box means padding would eat into the fixed 28px
     width and squeeze the slider/button instead of shifting the whole column. Matches
     .chart-scroll's own right padding (24px) so the reset button sits as far from the sidebar
     as the plotted area sits from the window edge, instead of flush against it. */
  margin-left: 24px;
  position: relative;
  transform: translateX(var(--y-control-offset, 0px));
}
/* a plain horizontal range input, rotated -90deg, rather than writing-mode:vertical-lr —
   vertical-oriented range inputs via writing-mode render inconsistently across engines (the
   track/thumb pseudo-elements don't reliably pick up the same thin sizing that works for the
   normal horizontal orientation), so this instead reuses the *exact* horizontal rendering the
   user already confirmed looks right, just turned on its side. Its "width" becomes the visual
   vertical length after rotation, so it's sized in JS (see sizeYStretchSlider in log.js) to
   match the wrap's actual available height rather than a fixed guess. Absolutely positioned so
   its pre-rotation (wide, short) layout box can't push the 28px-wide column wider than it.
*/
.stretch-slider-y {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotate(-90deg);
}
.chart-stretch-x {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 38px 12px 76px; /* left padding lines the slider up under the chart, past the sidebar gap + Y stretch column + axis gutter; right padding = chart-scroll's 24px + chart-box's own 14px margin-right, so the track lines up with the card's true edge, not the wider chart-scroll edge */
  position: relative;
}
/* Docked panel: center the track in the strip between the chart card and the panel. */
.chart-stretch-x:not(.clear-floating-panel) { transform: translateY(-4px); }
/* In floating mode the channel card protrudes into the bottom edge of the viewer. Move the
   slider to a real clear strip above that card — a z-index alone still leaves it visually
   bisected by the card's top border. */
.chart-stretch-x.clear-floating-panel {
  transform: translateY(var(--x-control-offset, -18px));
  /* The expanded floating channel panel must cover this secondary control. */
  z-index: 39;
}
.stretch-slider-x { flex: 1; }
/* Split's toolbar copy (see #splitXStretchWrap) — fixed, compact width instead of flex:1,
   since it sits inline among other toolbar buttons rather than spanning a dedicated row */
.stretch-slider-toolbar { width: 260px; }
/* margin:auto centering was tried first but doesn't land dead-center in practice: this item's
   own auto margin competes with #rightToolbarGroup's margin-left:auto for the SAME free space
   (both are flex items with an auto margin facing each other), so the free space splits between
   the two rather than centering this one in the toolbar as a whole — it visibly drifts left of
   center. Absolute positioning against .viewer-toolbar's own width sidesteps that entirely. */
#splitXStretchWrap { position: absolute; left: 50%; transform: translateX(-50%); }

/* deliberately muted/thin — this is a secondary, occasional-use control that shouldn't compete
   for attention with the chart itself, unlike the accent-colored primary toolbar controls */
.stretch-slider {
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
}
.stretch-slider::-webkit-slider-runnable-track { height: 3px; background: var(--border); border-radius: 2px; }
.stretch-slider::-moz-range-track { height: 3px; background: var(--border); border-radius: 2px; }
.stretch-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 9px;
  height: 9px;
  margin-top: -3px;
  border-radius: 50%;
  background: var(--text-faint);
  border: none;
}
.stretch-slider::-moz-range-thumb { width: 9px; height: 9px; border-radius: 50%; background: var(--text-faint); border: none; }
.stretch-slider:hover::-webkit-slider-thumb, .stretch-slider:focus::-webkit-slider-thumb { background: var(--text-dim); }
.stretch-slider:hover::-moz-range-thumb, .stretch-slider:focus::-moz-range-thumb { background: var(--text-dim); }
.stretch-reset {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text-dim);
  box-shadow: none;
}
.stretch-reset:hover { color: var(--accent-hover); border-color: var(--accent); }

/* click-and-drag panning (see attachDragPan) — grab cursor invites the drag, grabbing cursor
   confirms it's active; applied to uPlot's .u-over overlay, which is what receives the drag.
   touch-action:none hands every touch gesture on the chart to our own JS (attachDragPan's touch
   branch, attachTouchPinchZoom, attachRegionSelect) instead of the browser's native scroll/pinch
   — without it, mobile Safari/Chrome intercept the first move of a drag or pinch for page zoom
   or scroll before our touchmove listeners ever see it. */
.u-over { cursor: grab; touch-action: none; }
.u-over.tunelog-grabbing { cursor: grabbing; }
.u-over.tunelog-select-mode { cursor: crosshair; }

/* rectangle drawn while dragging in "Выделение" cursor mode (see attachRegionSelect) */
.region-select-box {
  position: absolute;
  z-index: 50;
  border: 1px solid var(--accent);
  /* The temporary drag band must not veil the chart itself (especially in the light theme,
     where --accent-soft is nearly white). The graph stays readable until mouseup, matching
     the persistent selected band shown with the context menu. */
  background: rgba(99, 102, 241, 0.04);
  pointer-events: none;
}

/* Once the mouse is released, keep the chosen time slice visible until its context menu is
   dismissed. The two masks darken only the surrounding data, so the selection reads as a
   focused window instead of a second opaque rectangle over the graph. */
.region-selection-overlay {
  position: absolute;
  inset: 0;
  z-index: 50;
  pointer-events: none;
}
.region-selection-mask {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--panel);
  opacity: 0.78;
}
.region-selection-band {
  position: absolute;
  top: 0;
  bottom: 0;
  border-left: 1px solid var(--accent);
  border-right: 1px solid var(--accent);
  background: rgba(99, 102, 241, 0.04);
}

/* context menu shown after releasing a region selection — same popup pattern as .pill-scale-menu,
   but a vertical list of full-width text actions instead of a row of short scale options */
.chart-context-menu {
  position: fixed;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(17, 24, 39, 0.18);
  padding: 5px;
  min-width: 220px;
}
.chart-context-menu-item {
  text-align: left;
  padding: 7px 10px;
  border-radius: 6px;
  border: none;
  background: none;
  color: var(--text);
  font-size: 12.5px;
  font-weight: 500;
  box-shadow: none;
}
.chart-context-menu-item:hover { background: var(--panel-2); color: var(--accent-hover); }
.chart-marker-input { width:250px; padding:7px 9px; border:1px solid var(--border); border-radius:6px; background:var(--panel-2); color:var(--text); font:inherit; font-size:12.5px; }
.chart-marker-actions { display:flex; justify-content:flex-end; gap:6px; margin-top:4px; }
.chart-marker-actions button { padding:5px 9px; font-size:12px; }

/* right-side marker panel: docked (default) shares borders with the chart like a sidebar
   column; unpinning turns it into a floating card that slides mostly off-screen to the right,
   leaving a thin sliver showing — same visual language as the bottom channel-bar's collapse
   behavior (see .channel-bar.floating), just turned 90°. Lives inside .chart-frame (not
   .viewer) specifically so top:0/bottom:0 below anchor to chart-frame's own box — which flex
   layout already sizes correctly around the channel-bar/toolbar/stretch-x siblings — instead of
   needing JS to measure and copy the channel-bar's live rect. That's what keeps this panel's
   height and the channel-bar's width/docked-state fully independent of each other. */
.marker-panel { position:absolute; z-index:36; top:0; bottom:0; right:0; width:230px; padding:12px; border-left:1px solid var(--border); background:var(--panel); overflow:auto; }
.marker-panel[hidden] { display:none; }

.marker-panel-head { display:flex; align-items:center; justify-content:space-between; margin:0 0 10px; color:var(--text); font-size:13px; font-weight:600; }

.marker-panel-pin { display:flex; align-items:center; justify-content:center; flex-shrink:0; width:26px; height:26px; padding:0; border:none; border-radius:7px; background:none; color:var(--text-faint); box-shadow:none; }
.marker-panel-pin:hover { color:var(--text-dim); background:var(--panel-2); }
.marker-panel-pin svg { transition:transform 0.18s ease; }
/* same "pin tilts loose" language as the channel-bar's collapse button: upright = pinned/docked,
   tilted = no longer holding the panel in the layout */
.marker-panel-pin.flipped svg { transform:rotate(-35deg); }

.viewer:has(.marker-panel:not([hidden]):not(.floating)) .chart-scroll { padding-right:254px; }
.viewer:has(.marker-panel:not([hidden]):not(.floating)) .chart-stretch-x { padding-right:268px; } /* +14px = chart-box's own margin-right, keeps the slider track under the card's true edge */

.marker-panel.floating {
  top:16px;
  bottom:16px;
  right:16px;
  width:230px;
  z-index:40;
  border:1px solid var(--border);
  border-radius:14px;
  background:var(--panel-float);
  backdrop-filter:blur(5px);
  -webkit-backdrop-filter:blur(5px);
  box-shadow:0 12px 32px rgba(15,18,28,.4);
  /* resting position leaves a sliver visible at the panel's left edge — same wordless hover hint
     as the channel-bar's peek sliver. It's allowed to be visible here because .chart-box now has
     a matching margin-right (see .chart-box) that keeps the card itself out of this strip — the
     sliver rests in that reserved gap, not on top of the card. */
  transform:translateX(calc(100% - 12px));
  transition:transform 0.16s ease;
}
.marker-panel.floating.peek { transform:translateX(0); }

.marker-panel-hover-zone { display:none; }
/* only ever shown while the panel is in overlay mode (see syncMarkerPanelHoverZone in log.js),
   so it always uses the floating inset (top/bottom:16px), matching .marker-panel.floating above */
.marker-panel-hover-zone.active { display:block; position:absolute; top:16px; bottom:16px; right:0; width:40px; z-index:39; }

/* desktop defaults for mobile-only controls — MUST stay before the @media block below: equal
   specificity means source order decides, and these need to lose to the mobile overrides once
   that query actually matches, not win by simply coming later in the file */
.mobile-channels-trigger { display: none; }
.channel-bar-mobile-title { display: none; }
.channel-bar-mobile-close { display: none; }
.mobile-back-btn { display: none; }
.mobile-readout { display: none; }
.readout-mode-btn { display: none; } /* the readout it switches between is mobile-only */

/* below this width there's no room to dock a 230px sidebar next to the chart without crushing
   it — force the panel to always behave like .floating (overlay, no reserved chart-scroll
   padding), regardless of the user's pin state saved from a wider screen.
   max-width alone misses phones in LANDSCAPE — a 375×812 portrait phone reads as ~812×375
   rotated, which is well past 760px wide — so max-height:500px is included too (matches
   MOBILE_QUERY in log.js; real desktop/laptop windows essentially never get that short). */
@media (max-width: 760px), (max-height: 500px) {
  /* !important + explicit base value (not 0): the desktop docked rule above resolves to higher
     specificity than any :has() selector naming just [hidden] — :not(.floating) in its argument
     adds an extra class-level specificity point that source order can't out-rank on its own.
     Restoring each element's own normal padding-right (not zeroing it) so this only cancels the
     254px reservation, not the base breathing room every chart already has. */
  .viewer:has(.marker-panel:not([hidden])) .chart-scroll { padding-right:24px !important; }
  .viewer:has(.marker-panel:not([hidden])) .chart-stretch-x { padding-right:38px !important; } /* +14px = chart-box's own margin-right, see desktop rule */
  .marker-panel {
    top:16px;
    bottom:16px;
    right:16px;
    width:min(230px, calc(100vw - 32px));
    border-left:1px solid var(--border);
    border-radius:14px;
    background:var(--panel-float);
    backdrop-filter:blur(5px);
    -webkit-backdrop-filter:blur(5px);
    box-shadow:0 12px 32px rgba(15,18,28,.4);
    transform:translateX(calc(100% - 12px)); /* sliver rests in .chart-box's margin-right gap, see desktop rule */
    transition:transform 0.16s ease;
  }
  .marker-panel.peek { transform:translateX(0); }
  /* a paired row's two tiles would otherwise squeeze to an unreadable width on a phone-size
     screen — stack them like separate rows instead, same as if they'd never been paired.
     align-items:stretch too: the desktop rule is flex-start (deliberately NOT stretch, so two
     side-by-side tiles of different natural heights don't get force-matched) — but once the
     axis flips to column that same flex-start aligns each box to the LEFT of the row's cross
     (now horizontal) axis instead of stretching it full-width, leaving every box shrunk to its
     own content width with a blank gap beside it. */
  /* scoped with .log-viewer-page (not bare .chart-row) on purpose: the later, unconditional
     .chart-row rule further down this file (display:flex; align-items:flex-start) has equal
     specificity to a bare selector here, and — being unconditional — would win the cascade by
     source order alone even while this media query matches; the extra class beats it outright
     regardless of where either rule sits in the file. */
  .log-viewer-page .chart-row { flex-direction: column; align-items: stretch; }

  /* --- log viewer: phone layout —————————————————————————————————————————————
     goal is "the chart takes the screen": drop the icon rail (the breadcrumb's own "Логи"
     link already gets you back), shrink every toolbar down to its controls, hide button text
     next to icons (title="" still carries it for anyone who long-presses/inspects), and turn
     the channel picker into an on-demand slide-up sheet instead of a permanently-docked strip
     eating vertical space out of the one thing this page is actually for. */
  .log-viewer-page .sidebar { display: none; }

  /* the whole phone chrome is one short row: back arrow, log title, channel sheet trigger.
     Everything else that used to sit up here (mode switch, visibility, share, AI, markers,
     reset-zoom, pan/select) is gone on touch — each was costing a full row of the one thing
     this page exists to show, and the gestures (drag to pan, pinch to zoom) already cover
     what the removed controls did. */
  /* the bar sits at the BOTTOM on a phone — that's where the thumb already is, and it puts the
     channels button within reach instead of at the far top corner. .viewer is a flex column, so
     ordering moves it without touching the markup (which desktop still reads top-down). */
  /* order 3 leaves 2 free for the docked readout to slot between the chart and this bar */
  .log-viewer-page .viewer-topbar { order: 3; border-bottom: 0; border-top: 1px solid var(--border); }
  .log-viewer-page .chart-frame { order: 1; }
  .log-viewer-page .viewer-topbar {
    flex-wrap: nowrap;
    gap: 8px;
    /* viewport-fit=cover (see log.ejs) lets the page run edge-to-edge under the notch and the
       home indicator — that's what hands the chart the whole screen — but it also means every
       inset has to be paid back by hand. All of these resolve to 0 on a device without one.
       Now that the bar is at the bottom it's the thing sitting over the home indicator, so the
       inset moves here from .chart-scroll; the chart in turn picks up the notch inset at its top. */
    padding: 6px calc(8px + env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) calc(8px + env(safe-area-inset-left));
  }
  .log-viewer-page .breadcrumb { flex: 1; min-width: 0; font-size: 13px; gap: 8px; }
  .log-viewer-page .breadcrumb a:not(.mobile-back-btn),
  .log-viewer-page .breadcrumb .sep,
  .log-viewer-page .row-count-label { display: none; }
  .log-viewer-page .mobile-back-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    border-radius: 8px;
    background: var(--panel-2);
    color: var(--text);
  }
  .log-viewer-page .topbar-right { flex-wrap: nowrap; width: auto; flex-shrink: 0; gap: 6px; }
  .log-viewer-page #channelsToggleBtn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
  }
  /* both .tabs-segment instances in the topbar: the mode switcher and the visibility form */
  .log-viewer-page .topbar-right .tabs-segment,
  .log-viewer-page #shareBtn,
  .log-viewer-page .viewer-toolbar { display: none; }

  .log-viewer-page .compare-toolbar { flex-wrap: wrap; padding: 8px 12px; row-gap: 6px; }

  /* icon-only on phone width — svg stays, the text label next to it goes (see .btn-label
     wrapping the label text in log.ejs); title="" already on every one of these buttons */
  .log-viewer-page .btn-label { display: none; }

  /* compare is a desktop-only feature on phone — no room for a second log's worth of chrome
     next to the one chart a phone screen can actually show. Hides both the entry point (the
     toolbar button) and the offset/mapping bar it opens; see the matching guard in log.js that
     skips even loading a persisted compare log's data on mobile, so there's nothing left over
     to control if a compare was set up earlier on a wider screen. */
  .log-viewer-page #compareTrigger { display: none; }
  .log-viewer-page #compareBar { display: none; }

  /* the Y/X stretch sliders are a fine-grained mouse-drag control that doesn't map cleanly to
     touch and duplicates what pinch-zoom (see attachTouchPinchZoom in log.js) already does with
     a gesture — dropping them hands their reserved columns/rows back to the chart itself. */
  .log-viewer-page .chart-stretch-y,
  .log-viewer-page .chart-stretch-x,
  .log-viewer-page #splitXStretchWrap { display: none !important; }

  /* full-bleed: no padding anywhere between the viewport edge and the plot itself. margin-left
     is reset too because centerYStretchControls() sets --y-gutter-reduction for the Y slider
     that no longer exists here. */
  .log-viewer-page .chart-scroll,
  .log-viewer-page .chart-frame .chart-scroll {
    /* uneven on purpose: the right side needs no margin of its own because uPlot already keeps
       ~25px there so the last X tick label isn't clipped, while the left pays for the Y axis
       gutter and the bottom for the X label row. The notch inset is added ON TOP at the top edge
       (the chart is the topmost thing on screen now that the bar moved down) and on the right,
       where a landscape notch lands; the bottom belongs to the bar below, not to us. */
    padding: calc(10px + env(safe-area-inset-top)) env(safe-area-inset-right) 10px calc(5px + env(safe-area-inset-left));
    margin-left: 0;
  }
  /* .chart-box's card chrome IS the "fat border" around the plot on a phone — border+radius+
     shadow+padding, plus two margins that exist only to leave room for the marker-panel sliver
     and the channel-bar peek, neither of which is present on touch. Scoped with
     .log-viewer-page because the unconditional .chart-box rule sits LATER in this file and
     would otherwise win on source order at equal specificity. */
  .log-viewer-page .chart-box {
    border: none;
    border-radius: 0;
    padding: 0;
    margin: 0;
    background: none;
    box-shadow: none;
  }

  /* 16px is iOS Safari's threshold: focusing an input whose computed font-size is even a hair
     under it makes Safari zoom the whole page in to "help", and it never zooms back out on blur
     — which left the UI stuck scaled, with pinch-to-unzoom swallowed by the chart's own pinch
     handler. Raising the font is the root fix; the viewport/gesture locks below are belt and
     braces, because iOS has ignored user-scalable=no since iOS 10. */
  .log-viewer-page input,
  .log-viewer-page select,
  .log-viewer-page textarea { font-size: 16px; }

  /* pan-x pan-y rather than manipulation: manipulation still permits pinch-zoom, and the whole
     point here is that pinch belongs to the chart, never to the browser's page zoom */
  .log-viewer-page { touch-action: pan-x pan-y; }

  /* the channel list is the drawer's one scrolling region: it takes whatever height is left over
     once the head above and the search/presets below have taken theirs */
  .log-viewer-page .channel-pills {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap; /* the desktop rule wraps; in a column that would spill into extra columns */
    flex: 1;
    gap: 0;
    min-height: 0;
    overflow-y: auto;
    max-height: none;
  }
  /* checkbox rows, not chips — with one row per channel the tick is what tells you at a glance
     what's plotted, so the row itself stays visually flat and the box carries the state */
  .log-viewer-page .channel-pill {
    width: 100%;
    /* without this the rows squash to fit the container instead of overflowing it, so a short
       drawer (landscape, mostly) silently collapses ten channels into one unreadable band and
       the list never becomes scrollable */
    flex-shrink: 0;
    /* tight rows on purpose — the point of this drawer is seeing as much of the channel list at
       once as possible; the checkbox still gives the row a ~30px tap target */
    padding: 4px 6px;
    gap: 7px;
    font-size: 11.5px;
    border: 0;
    border-radius: 8px;
    background: none;
    box-shadow: none;
  }
  .log-viewer-page .channel-pill::before {
    content: '';
    order: -1;
    flex-shrink: 0;
    width: 15px;
    height: 15px;
    border: 1.5px solid var(--border);
    border-radius: 4px;
    background: var(--panel-2) center/11px 11px no-repeat;
  }
  .log-viewer-page .channel-pill.active {
    background: none;
    color: var(--text);
  }
  .log-viewer-page .channel-pill.active::before {
    border-color: var(--accent);
    background-color: var(--accent);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  }
  .log-viewer-page .channel-pill .dot { margin-left: 2px; }

  /* presets are the main one-tap control in here, and at the desktop size they're a fiddly
     3px-tall target — give them a proper finger-sized row */
  .log-viewer-page .preset-chip {
    padding: 8px 10px 8px 14px;
    font-size: 13px;
    border-radius: 10px;
  }
  .log-viewer-page .preset-chip .preset-remove { opacity: 1; width: 20px; height: 20px; }
  .log-viewer-page .preset-key-hint { display: none; } /* no keyboard to press them with */
  .log-viewer-page .preset-bar { gap: 8px; margin-bottom: 10px; }
  .log-viewer-page .preset-chips { gap: 8px; }

  .mobile-channels-trigger { display: flex; }

  /* values under the centre crosshair (drawn on the canvas — see drawMobileCrosshair in log.js).
     Floats over the top of the plot rather than taking its own row: traces almost never run
     along the very top of the chart, and the phone has no vertical space to spare. Draggable
     from anywhere on it (attachReadoutDrag in log.js), so touch-action has to be off — otherwise
     the browser claims the gesture for scrolling before the drag handler ever sees it. */
  .mobile-readout[hidden] { display: none; }
  .mobile-readout {
    display: block;
    position: absolute;
    top: 6px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    touch-action: none;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    max-width: calc(100% - 20px);
    padding: 6px 10px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--panel-float);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    box-shadow: var(--shadow);
    /* --readout-scale is driven by a two-finger pinch on the panel (attachReadoutDrag in log.js)
       and clamped there, so this can't be shrunk into illegibility or grown over the whole plot */
    font-size: calc(11px * var(--readout-scale, 1));
    line-height: 1.5;
  }
  .crosshair-time { color: var(--text-dim); white-space: nowrap; }
  .crosshair-time b { color: var(--text); margin-left: 2px; }
  /* min-width:0 so the name can actually shrink inside a grid cell — without it a flex child
     refuses to go below its content width and shoves the value past the cell's right edge */
  .crosshair-row { display: flex; align-items: center; gap: 5px; white-space: nowrap; min-width: 0; }
  .crosshair-row b { color: var(--text); margin-left: auto; padding-left: 8px; font-variant-numeric: tabular-nums; }
  .crosshair-name { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; }
  .crosshair-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
  /* these rows are flex, not grid cells, so the column alignment above doesn't reach them */
  .crosshair-row .tt-cmp { margin-left: 7px; }
  /* docked variant: a strip across the bottom of the viewer (a flex sibling of the chart, so it
     takes its height off the plot rather than covering it), with the values laid out in as many
     columns as fit. Everything that made the floating version float is undone here. */
  .log-viewer-page .mobile-readout.readout-docked {
    order: 2;
    position: static;
    transform: none;
    max-width: none;
    max-height: 38dvh;
    overflow-y: auto;
    border: 0;
    border-top: 1px solid var(--border);
    border-radius: 0;
    background: var(--panel);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
    touch-action: pan-y;
    cursor: default;
    padding: 6px calc(8px + env(safe-area-inset-right)) 6px calc(8px + env(safe-area-inset-left));
  }
  /* exactly two, not auto-fill: fitting as many 118px columns as the width allowed gave three on
     a wider phone, and at that width the channel name and its value no longer fit on one line */
  .log-viewer-page .mobile-readout.readout-docked > div:first-child {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1px 12px;
  }
  /* the time reading heads the grid on its own line rather than sitting in a column */
  .log-viewer-page .readout-docked .crosshair-time { grid-column: 1 / -1; }
  .log-viewer-page .readout-docked .readout-presets { margin-top: 5px; padding-top: 5px; }
  .log-viewer-page .readout-mode-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 24px;
    margin-right: auto; /* head is flex-end; this keeps the toggle on the left, × on the right */
    padding: 0;
    border: none;
    border-radius: 7px;
    background: none;
    color: var(--text-faint);
    box-shadow: none;
  }
  .log-viewer-page .readout-mode-btn.active { color: var(--accent); background: var(--accent-soft); }

  .readout-presets[hidden] { display: none; }
  .readout-presets {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 6px;
    padding-top: 6px;
    border-top: 1px solid var(--border);
  }
  .readout-preset {
    padding: 5px 9px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--panel-2);
    color: var(--text-dim);
    font-size: inherit;
    font-weight: 600;
    line-height: 1.2;
    box-shadow: none;
  }
  .readout-preset.active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }

  /* --- channel picker as a slide-up sheet, independent of the desktop dock/collapse/peek
     mechanism above (see .channel-bar.floating) — driven purely by .mobile-open (see
     setMobileChannelsOpen in log.js), not by panelCollapsed/localStorage */
  /* no dimming: the panel is docked to the edge rather than floating over the page, and leaving
     the chart at full brightness means you watch a trace appear the moment you tick its channel
     instead of picking blind behind a scrim. The layer stays (transparent) purely so a tap
     outside still closes the panel. */
  .mobile-channels-backdrop.active {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 210;
    background: transparent;
  }
  /* a full-height drawer off the right edge rather than a bottom sheet: the channel list is the
     long, scrolling thing in here, and vertical is the direction a phone actually has to spare.
     Contents are reordered below so the list gets the height and the controls you reach for
     after reading it — search, then presets — sit at the bottom under the thumb. */
  .log-viewer-page .channel-bar {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: auto;
    z-index: 211;
    display: flex;
    flex-direction: column;
    width: min(268px, 76vw);
    max-height: none;
    padding: 10px calc(12px + env(safe-area-inset-right)) calc(10px + env(safe-area-inset-bottom)) 12px;
    padding-top: calc(10px + env(safe-area-inset-top));
    /* docked, not floating: square against the screen edges with a plain divider down its left
       side, instead of a rounded card hovering over the chart on a big soft shadow */
    border-radius: 0;
    border-left: 1px solid var(--border);
    transform: translateX(100%);
    transition: transform 0.2s ease;
    box-shadow: none;
  }
  .log-viewer-page .channel-bar.mobile-open { transform: translateX(0); }
  /* head → search → list (takes the free height) → presets. "Create channel" isn't ordered here
     at all: renderPills() moves it inside the list on mobile. */
  .log-viewer-page .channel-bar-head { order: 0; }
  .log-viewer-page .channel-search-row { order: 1; margin: 0 0 6px; }
  .log-viewer-page .channel-search {
    margin-bottom: 0; /* the row owns the spacing now */
    padding: 4px 30px 4px 9px; /* right side leaves room for the clear button */
    font-size: 11.5px;
  }
  .log-viewer-page .channel-search-clear { width: 24px; height: 22px; right: 4px; }
  .log-viewer-page .channel-pills { order: 2; }
  .log-viewer-page .preset-bar { order: 3; margin: 8px 0 0; }
  /* as a list row: flush with the channels above it rather than a full-width block button */
  .log-viewer-page .create-channel-btn {
    margin: 4px 0 0;
    padding: 6px 8px;
    border-style: dashed;
    border-radius: 8px;
    font-size: 12.5px;
    justify-content: flex-start;
    gap: 8px;
  }
  /* these two are secondary to the presets beside them — shrink them so the preset chips, which
     are what you actually tap mid-session, stay the biggest targets in the row */
  /* icon-only here (their labels are wrapped in .btn-label, hidden above): at 330px the drawer
     has no room for "Сохранить текущий набор" spelled out — left as text it either wrapped to
     two lines, making these TALLER than the preset chips beside them, or overflowed the panel.
     Both keep their title="" for a long-press. */
  .log-viewer-page .preset-save-btn {
    width: 30px;
    height: 30px;
    padding: 0;
    justify-content: center;
    flex-shrink: 0;
  }
  .log-viewer-page .preset-save-btn svg { width: 13px; height: 13px; }
  .log-viewer-page .channel-search-clear {
    position: absolute;
    top: 50%;
    right: 6px;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 7px;
    background: none;
    color: var(--text-faint);
    box-shadow: none;
  }
  .log-viewer-page .channel-search-clear:hover { background: var(--panel-2); color: var(--text); }
  /* no title row: "Каналы" only repeated what the button you just tapped already said, and the
     drawer is short on height, not on explanation. The close button keeps the row, pushed right
     and sized down to what a thumb needs. */
  .log-viewer-page .channel-bar-head { justify-content: flex-end; margin: 0; }
  .channel-bar-mobile-title { display: none; }
  .log-viewer-page .channel-bar-collapse-btn { display: none; }
  .log-viewer-page .channel-bar-mobile-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 24px;
    padding: 0;
    margin-right: -4px; /* optically flush with the panel's right padding */
    border: none;
    border-radius: 7px;
    background: none;
    color: var(--text-dim);
    box-shadow: none;
  }
  .log-viewer-page .channel-bar-mobile-close:hover { background: var(--panel-2); color: var(--text); }

  /* the two-column channel-mapping grid (compare feature) doesn't fit a phone width */
  .compare-map-list { grid-template-columns: 1fr; }
}


.marker-list { display:flex; flex-direction:column; gap:5px; }
.marker-list-empty { color:var(--text-faint); font-size:12px; padding:6px 2px; }
.marker-list-item { display:grid; grid-template-columns:auto 20px minmax(0,1fr); align-items:start; gap:6px; padding:7px; border:1px solid var(--border); border-radius:7px; color:var(--text-dim); cursor:pointer; font-size:12px; line-height:1.35; }
.marker-list-item:hover { border-color:var(--accent); }
.marker-list-item input { margin:2px 0 0; accent-color:var(--accent); }
.marker-list-number { display:grid; place-items:center; width:20px; height:20px; border-radius:50%; background:var(--accent); color:white; font-size:11px; font-weight:700; }
.marker-list-text { min-width:0; overflow-wrap:anywhere; }
.chart-marker-layer { position:absolute; inset:0; pointer-events:none; z-index:45; overflow:hidden; }
.chart-marker-band { position:absolute; top:0; bottom:0; min-width:2px; background:rgba(129,140,248,.12); border-left:1px solid rgba(129,140,248,.8); border-right:1px solid rgba(129,140,248,.8); }
.chart-marker-number { position:absolute; top:7px; left:50%; transform:translateX(-50%); display:grid; place-items:center; width:20px; height:20px; border-radius:50%; background:var(--accent); color:white; font-size:11px; font-weight:700; pointer-events:auto; cursor:help; }
.chart-marker-tooltip { position:absolute; top:25px; left:50%; transform:translateX(-50%); width:max-content; max-width:240px; padding:6px 8px; border:1px solid var(--border); border-radius:6px; background:var(--panel); box-shadow:var(--shadow); color:var(--text); font-size:12px; font-weight:400; line-height:1.35; opacity:0; visibility:hidden; transition:opacity .12s; }
.chart-marker-number:hover .chart-marker-tooltip { opacity:1; visibility:visible; }

.compare-popover {
  position: fixed;
  z-index: 210;
  width: min(320px, calc(100vw - 24px));
  padding: 8px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 28px rgba(17, 24, 39, 0.22);
}
.compare-popover[hidden] { display: none; }
.compare-popover input {
  margin: 0 0 7px;
  padding: 8px 10px;
  font-size: 13px;
}
.compare-popover-list { max-height: 260px; overflow-y: auto; }
.compare-popover-log {
  display: block;
  width: 100%;
  padding: 8px 10px;
  overflow: hidden;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
  box-shadow: none;
}
.compare-popover-log:hover, .compare-popover-log:focus { background: var(--panel-2); color: var(--accent-hover); }
.compare-popover-status { padding: 7px 10px; color: var(--text-faint); font-size: 12px; }

/* While a region action is being chosen, the graph and its zoom controls cannot receive a
   second interaction. The menu lives directly under body, so it remains clickable. */
body.region-menu-open .viewer { pointer-events: none; }

/* transient bottom-right confirmation (see showToast in log.js) — e.g. "участок сохранён" with
   an "Открыть" action, so a background save doesn't need a blocking modal to confirm it worked */
.tunelog-toast {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 300;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 380px;
  padding: 12px 12px 12px 16px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgba(15, 18, 28, 0.3);
  animation: tunelog-toast-in 0.18s ease;
}
@keyframes tunelog-toast-in {
  from { transform: translateY(8px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
.tunelog-toast-text { font-size: 13px; color: var(--text); line-height: 1.4; }
.tunelog-toast-action {
  flex-shrink: 0;
  padding: 6px 12px;
  font-size: 12.5px;
  font-weight: 600;
  border-radius: 7px;
}
.tunelog-toast-close {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--text-faint);
  box-shadow: none;
}
.tunelog-toast-close:hover { background: var(--panel-2); color: var(--text-dim); }

.chart-box {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 8px 16px 14px;
  margin-bottom: 14px;
  /* same idea as margin-bottom above (which already gives the bottom channel-bar's peek sliver
     somewhere to sit without touching the card): reserve a matching gap on the right so the
     marker panel's peek sliver has empty space to rest in instead of overlapping the card */
  margin-right: 14px;
  box-shadow: var(--shadow);
  transition: transform 0.12s ease, filter 0.12s ease;
}
.chart-box.ai-dimmed { opacity: 0.55; }
.chart-box .chart-title { font-size: 12.5px; font-weight: 600; color: var(--text-dim); margin: 6px 0 2px; }

/* Split-mode tile layout (see attachSplitDrag/applySplitDrop in log.js): each row holds one
   full-width tile or two side-by-side tiles, dragged by their .chart-title bar */
.chart-row { display: flex; align-items: flex-start; }
.chart-row .chart-box { flex: 1 1 0; min-width: 0; }
.chart-row .chart-title[draggable] { cursor: grab; user-select: none; }
.chart-row .chart-title[draggable]:active { cursor: grabbing; }
.chart-box.dragging { opacity: 0.4; }
.chart-box.drop-left { box-shadow: inset 4px 0 0 var(--accent), var(--shadow); }
.chart-box.drop-right { box-shadow: inset -4px 0 0 var(--accent), var(--shadow); }
/* narrow (already-paired) tile, hovered outside the top/bottom reorder strips — there's no
   empty seat to pair into, so a drop here evicts this exact tile instead (see applySplitDrop's
   'replace' zone). Shrinking it slightly plus a blur previews that outcome: this tile is
   stepping aside to make room, the dragged one takes its spot. Opacity was tried first but read
   as too similar to .dragging/.ai-dimmed, which already use fading for other meanings; a plain
   accent ring after that read as too close to the left/right/top/bottom pairing indicators. */
.chart-box.drop-replace { transform: scale(0.96); filter: blur(2px); }
/* top/bottom always land on the .chart-row (see showSplitDropIndicator), so the highlight
   spans the row's full width — including both tiles of a paired row — not just one tile.
   An inset box-shadow on the row itself doesn't work here: .chart-box fills the row edge to
   edge with an opaque background, so it paints right over a shadow drawn at the row's own
   border. Instead this is a real bar sitting in the 14px gap between rows (see .chart-box's
   margin-bottom), positioned outside the row's own box so nothing can cover it.
   These only fire on the very first row's top edge and the very last row's bottom edge — any
   boundary BETWEEN two rows is instead a .chart-row-gap strip (below), which unifies what used
   to be two separate per-row bands (with dead space between them) into one continuous target. */
.chart-row { position: relative; }
.chart-row.drop-top::before,
.chart-row.drop-bottom::after {
  content: '';
  position: absolute;
  left: 0;
  right: 14px; /* match .chart-box's margin-right so the bar lines up with the cards' edge */
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
  z-index: 2;
  pointer-events: none;
}
.chart-row.drop-top::before { top: -9px; }
.chart-row.drop-bottom::after { bottom: -9px; }
/* covers exactly what used to be the bottom 25% of the row above plus the top 25% of the row
   below plus the gap between them (see attachSplitRowGaps in log.js) — one strip, one target,
   no dead space. Inert except mid-drag so it never intercepts hover/click/pan on the charts. */
.chart-row-gap { position: absolute; left: 0; right: 0; z-index: 3; pointer-events: none; }
.chart-row-gap.drag-active { pointer-events: auto; }
.chart-row-gap.drop-active::after {
  content: '';
  position: absolute;
  left: 0;
  right: 14px;
  /* NOT 50% — see --bar-center (set in repositionSplitRowGaps): this zone extends further into
     the row above than into the row below, so its own midpoint isn't the true visual gap's
     midpoint between the two cards */
  top: var(--bar-center, 50%);
  transform: translateY(-50%);
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
}

.channel-bar { border-top: 1px solid var(--border); background: var(--panel); padding: 10px 24px; flex-shrink: 0; max-height: 45vh; overflow-y: auto; }

.channel-bar-head { display: flex; justify-content: center; margin: -4px 0 2px; }
.channel-bar-collapse-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 16px;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--text-faint);
  box-shadow: none;
}
.channel-bar-collapse-btn:hover { color: var(--text-dim); background: var(--panel-2); }
.channel-bar-collapse-btn svg { transition: transform 0.18s ease; }
/* pinned (docked) = upright pin; collapsed/floating = the same pin tilted loose, echoing "it's
   not holding the panel down anymore" rather than an arbitrary arrow flip */
.channel-bar-collapse-btn.flipped svg { transform: rotate(-35deg); }

/* collapsed: the panel leaves normal flow entirely (see setPanelCollapsed in log.js toggling
   this class) so chart-frame/chart-stretch-x — its flex siblings in .viewer — expand into the
   freed space, same as if it were removed from the page. It still exists positioned off the
   bottom edge of .main (which needs position:relative for this to anchor correctly rather than
   to the viewport, which would ignore the sidebar's width). Styled as a distinct floating card
   — inset on all four sides (not flush against the window edge, so its own rounded bottom
   border is actually visible instead of getting cut off by the viewport), translucent with a
   background blur — reinforcing that it's no longer part of the page flow, it's overlaid on
   top of whatever's under it, not a docked layout piece. */
.channel-bar.floating {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 16px;
  z-index: 40;
  /* resting position leaves a small sliver poking up above the bottom edge — a constant, wordless
     hint that hovering there reveals more, instead of a fully-hidden panel with no visual trace.
     Percentage transforms are relative to the element's own box, not the viewport, so this still
     works the same regardless of the bottom:16px inset above. */
  transform: translateY(calc(100% - 12px));
  transition: transform 0.16s ease;
  background: var(--panel-float);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(15, 18, 28, 0.4);
}
.channel-bar.floating.peek { transform: translateY(0); }

/* the resting sliver (see transform above) already catches hover on its own — this strip just
   widens that hit target below/around it (covering the 16px gap below the sliver too) so the
   cursor doesn't have to land exactly on a 12px-tall strip that isn't even flush with the edge */
.channel-bar-hover-zone { display: none; }
.channel-bar-hover-zone.active {
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 40px;
  z-index: 39;
}
/* anchors the clear button, which only appears on the phone drawer (and only with text in it) */
.channel-search-row { position: relative; }
.channel-search-clear { display: none; }
.channel-search-clear[hidden] { display: none; }
.channel-search {
  width: 100%;
  max-width: 280px;
  margin-bottom: 8px;
  padding: 5px 10px;
  border-radius: 6px;
  border: 1px solid var(--border);
  font-size: 12px;
  background: var(--panel-2);
}
.channel-search:focus { outline: none; border-color: var(--accent); background: var(--panel); }

/* max-height is set dynamically by JS (adjustPillsHeight) based on channel count and viewport size */
.channel-pills { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 6px; overflow-y: auto; transition: max-height 0.15s ease; }

.channel-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel);
  font-size: 11.5px;
  line-height: 1.4;
  font-weight: 500;
  color: var(--text-dim);
  cursor: pointer;
  user-select: none;
  transition: border-color 0.1s, color 0.1s, background 0.1s, opacity 0.1s;
}
/* search dims non-matches instead of removing them — keeps the full list in place so
   nothing jumps around, and staying results/counts stay legible at a glance */
.channel-pill.dimmed { opacity: 0.3; }
.channel-pill .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
/* a soft tinted fill instead of a solid block of color — reads clearly as "selected" without
   being the loudest thing in the row; the dot alone still carries the channel's own color */
.channel-pill.active { background: var(--accent-soft); color: var(--accent-hover); border-color: var(--accent); }
[data-theme="dark"] .channel-pill.active { color: var(--accent); }
.channel-pill.ai-focus { box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px rgba(99, 102, 241, 0.12); }
.channel-pill:hover:not(.active) { border-color: var(--text-faint); color: var(--text); }
/* computed (formula) channels get a dashed border plus a leading ƒ mark so they read as
   "synthetic" at a glance, distinct from a channel that came straight off the ECU */
.channel-pill.computed { border-style: dashed; }
.channel-pill.computed::before {
  content: 'ƒ';
  font-style: italic;
  font-weight: 700;
  opacity: 0.65;
  margin-right: -2px;
}
.channel-pill .pill-scale-badge {
  font-size: 10px;
  font-weight: 700;
  opacity: 0.7;
  letter-spacing: -0.2px;
}
/* pencil icon, hover-revealed on computed pills only — opens the formula edit modal, which
   has its own delete action, so a plain × isn't needed on the pill itself anymore */
.channel-pill .pill-edit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  opacity: 0;
  transition: opacity 0.1s;
  margin-left: -2px;
}
.channel-pill .pill-edit svg { width: 11px; height: 11px; }
.channel-pill:hover .pill-edit { opacity: 1; }
.channel-pill .pill-edit:hover { background: rgba(0, 0, 0, 0.12); }
.channel-pill.active .pill-edit:hover { background: rgba(0, 0, 0, 0.1); }

.pill-scale-menu {
  position: fixed;
  z-index: 200;
  display: grid;
  grid-template-columns: repeat(4, auto) 30px;
  gap: 4px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(17, 24, 39, 0.18);
  padding: 5px;
  min-height: 0;
  align-content: start;
}
.pill-scale-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px 9px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
}
.pill-scale-option span { transform:translateY(-1px); }
.pill-scale-option:hover { border-color: var(--accent); color: var(--accent-hover); }
.pill-scale-option.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-hover); }
.pill-menu-divider { grid-column:1 / -1; height:1px; background:var(--border); margin:2px 0; }
.pill-line-weight { grid-column:1 / -1; display:flex; align-items:center; justify-content:center; gap:8px; color:var(--text-dim); font-size:12px; }
.pill-line-weight button { width:27px; height:25px; padding:0; border:1px solid var(--border); border-radius:6px; background:var(--panel); color:var(--text); font:inherit; font-size:17px; line-height:1; box-shadow:none; display:grid; place-items:center; }
.pill-line-weight button span { transform:translateY(-1px); }
.pill-line-weight button:hover:not(:disabled) { border-color:var(--accent); color:var(--accent-hover); }
.pill-line-weight button:disabled { opacity:.35; cursor:not-allowed; }
.pill-line-weight span { min-width:34px; text-align:center; font-variant-numeric:tabular-nums; }
.pill-color-control { display:flex; align-items:center; justify-content:center; width:30px; height:27px; }
.pill-color-control input[type="color"] { width:30px; height:27px; padding:2px; border:1px solid var(--border); border-radius:6px; background:var(--panel); cursor:pointer; }
.pill-color-control input[type="color"]::-webkit-color-swatch-wrapper { padding:0; }
.pill-color-control input[type="color"]::-webkit-color-swatch { border:0; border-radius:3px; }
.metric-description { grid-column:1 / -1; max-width:260px; padding:3px 2px 5px; color:var(--text-faint); font-size:11.5px; line-height:1.4; }

.create-channel-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  padding: 7px 14px;
  border-radius: 8px;
  border: 1px dashed var(--accent);
  background: var(--accent-soft);
  color: var(--accent-hover);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.create-channel-btn:hover { background: var(--accent); border-style: solid; color: #fff; }

/* floating cursor tooltip — appended into uPlot's .u-over (already position:absolute), so top:0/left:0 + transform lines up with u.cursor.left/top exactly */
.chart-tooltip {
  position: absolute;
  top: 0;
  left: 0;
  display: none;
  pointer-events: none;
  background: rgba(17, 24, 39, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: #fff;
  font-size: 12px;
  padding: 8px 10px;
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
  /* must sit above every other overlay living in the same u.over container — the marker-band
     layer (z-index 45) and the region-selection dimming mask (z-index 50, shown while a drag-
     selection or a "Выделение"-mode zone is active) — otherwise the tooltip renders underneath
     the dimming mask and reads as faded/washed out right when the user is hovering the chart */
  z-index: 60;
  white-space: nowrap;
  line-height: 1.6;
}
/* Each row lays itself out, so a value sits right next to the name it belongs to. Aligning the
   numbers into shared columns instead (tried, reverted) stretched the first column to the
   longest channel name and pushed every value far away from its own label — wider tooltip,
   harder to read across. */
.chart-tooltip .tt-time { font-weight: 600; font-size: 11px; color: #9aa3b5; margin-bottom: 3px; }
.chart-tooltip .tt-row { display: flex; align-items: center; gap: 7px; transition: opacity 0.1s ease; }
.chart-tooltip .tt-name { display: flex; align-items: center; gap: 7px; }
.chart-tooltip .tt-row .tt-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.chart-tooltip .tt-row b { margin-left: 12px; font-weight: 600; }
/* the same reading converted to the other pressure unit (see altPressureSuffix in log.js) —
   deliberately quieter than the primary number so it reads as a translation, not a second value */
.unit-alt { margin-left: 4px; font-weight: 500; opacity: 0.6; }
/* the comparison log's reading, sitting in its own column beside the current one instead of on a
   duplicate row below it. Muted weight and colour are what mark it as the secondary number —
   it used to carry a dashed divider, which only highlighted that the rows weren't aligned. */
.tt-cmp {
  margin-left: 10px;
  font-weight: 500;
  opacity: 0.55;
}
.tt-cmp:empty { display: none; }
/* which row(s) the cursor is vertically on top of right now — see CURSOR_LINE_HIT_PX in
   attachTooltip; only applied at all once the cursor is close enough to at least one line */
.chart-tooltip .tt-row-dim { opacity: 0.45; }
.chart-tooltip .tt-row-hot b { color: #fff; }
.chart-tooltip .tt-row-hot .tt-dot { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.18); }

/* channel presets */
.preset-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.preset-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.preset-bar-actions { display: flex; gap: 6px; margin-left: auto; }
.preset-chip {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 6px 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
  user-select: none;
}
/* digit hotkey hint (1-9, by render order — see renderPresets) — a small floating "key cap"
   above the chip, only on hover/focus so it doesn't clutter the preset bar at rest */
.preset-key-hint {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 5px;
  background: var(--text);
  color: var(--panel);
  font-size: 10.5px;
  font-weight: 700;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  box-shadow: 0 4px 10px rgba(15, 18, 28, 0.25);
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, 4px);
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.preset-chip:hover .preset-key-hint,
.preset-chip:focus-visible .preset-key-hint { opacity: 1; transform: translate(-50%, 0); }
.preset-chip:hover { border-color: var(--accent); color: var(--accent-hover); }
/* only "active" when the current channel selection exactly matches this preset's saved set — otherwise it'd claim to be selected while the chart no longer reflects it */
.preset-chip.active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-hover); }
.preset-chip .preset-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  font-size: 13px;
  line-height: 1;
  opacity: 0;
  transition: opacity 0.1s;
}
.preset-chip:hover .preset-remove { opacity: 1; }
.preset-chip .preset-remove:hover { background: rgba(79, 70, 229, 0.18); }
.preset-save-btn {
  padding: 4px 10px;
  font-size: 11.5px;
  border-radius: 999px;
}

.preset-input-popup {
  position: fixed;
  /* above the mobile channel drawer (211) and its backdrop (210) — at 200 it opened *behind*
     the drawer that launched it, so on a phone the rename box was simply invisible */
  z-index: 220;
  /* fixed + only `left` set means the box shrinks to whatever space is left before the viewport
     edge; anchored to a button in the drawer's right corner that came out ~50px wide, with the
     input clipped to a single character. The floor makes it overflow instead, which the
     keep-on-screen pass in showPresetInput() then pulls back inside. */
  min-width: min(220px, calc(100vw - 16px));
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(17, 24, 39, 0.18);
  padding: 6px;
}
.preset-name-input {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 12.5px;
  width: 200px;
  outline: none;
  color: var(--text);
}
.preset-name-input:focus { border-color: var(--accent); }

/* uPlot legend: default is bold/large — tone it down so it reads as a legend, not a second heading */
.u-legend {
  font-size: 12px;
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.u-legend .u-label { font-weight: 500; font-size: 12px; color: var(--text-dim); }
.u-legend .u-value { font-weight: 600; font-size: 12px; color: var(--text); }
.u-legend .u-marker { border-radius: 3px; }
.u-legend th, .u-legend td { padding: 3px 6px; }

/* ---------- guest landing (no account: login pane + anonymous upload) ---------- */

.guest-page {
  min-height: 100vh;
  background:
    radial-gradient(560px circle at 12% 8%, var(--accent-soft), transparent 60%),
    radial-gradient(480px circle at 88% 94%, var(--accent-soft), transparent 55%),
    var(--bg);
}

.guest-split {
  min-height: 100vh;
  display: flex;
}

.guest-login-pane {
  width: 400px;
  flex: 0 0 400px;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  border-right: 1px solid var(--border);
  padding: 32px;
}

.guest-login-content { width: 100%; max-width: 300px; }
.guest-login-content h1 { margin: 0 0 4px; font-size: 21px; font-weight: 700; letter-spacing: -0.2px; }

.guest-upload-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 48px 32px;
  text-align: center;
}

.guest-upload-intro { max-width: 440px; margin-bottom: 24px; }
.guest-upload-intro h2 { margin: 0 0 8px; font-size: 22px; font-weight: 700; }
.guest-upload-intro p { margin: 0; font-size: 14px; color: var(--text-dim); line-height: 1.5; }

.guest-upload-card {
  width: 440px;
  max-width: 100%;
  text-align: left;
  box-shadow: 0 20px 50px -12px rgba(16, 24, 40, 0.16), var(--shadow);
}
[data-theme="dark"] .guest-upload-card { box-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.55), var(--shadow); }
.guest-upload-card .dropzone { min-height: 180px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.guest-upload-card .modal-actions { margin-top: 18px; }
.guest-upload-card .modal-actions button { width: 100%; }

@media (max-width: 760px) {
  .guest-split { display: block; }
  .guest-login-pane { width: 100%; min-height: 0; border-right: 0; border-bottom: 1px solid var(--border); padding: 32px 24px; }
  .guest-upload-main { padding: 40px 20px; }
  .guest-upload-card { width: 100%; }
}
