:root {
  --bg: #0a0b10;
  --bg-panel: #12141c;
  --bg-raised: #171a25;
  --border: #262a3a;
  --border-soft: #1c2030;
  --text: #d8e6ff;
  --text-dim: #8a93a8;
  --text-faint: #5c6478;
  --accent: #00b7ff;
  --accent-soft: #7fd7ff;
  --accent-glow: rgba(0, 183, 255, 0.35);
  --danger: #ff5f5f;
  /* The topbar's real height, kept in sync by a ResizeObserver in app.js
     whenever it wraps to more than one row (narrow window, a long
     translation, browser zoom). Every viewport-filling panel below reads
     this instead of a hardcoded 44px, so wrapping to a second row pushes
     the map/panel/views down instead of the topbar's second row covering
     them. 44px here is only the pre-JS fallback for the single-row case. */
  --topbar-h: 44px;
  /* Monospace first, because the whole interface is set in it -- then the CJK
     faces, because none of those monospace fonts contain a single Chinese,
     Japanese or Korean glyph. Without the tail the browser substitutes
     whatever it can find per-character, which produces text of mixed weight
     and baseline that looks like a rendering fault rather than a language.
     All four are system fonts on their respective platforms, so nothing is
     downloaded and the site still makes no external request. */
  --font-mono: "Cascadia Code", Consolas, "JetBrains Mono", "Courier New",
    "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC",
    "Yu Gothic", "Hiragino Sans", "Noto Sans CJK JP",
    "Malgun Gothic", "Apple SD Gothic Neo", "Noto Sans CJK KR",
    monospace;
}

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  font-family: var(--font-mono);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  /* This single-page app never legitimately scrolls at the document
     level (every scrollable region -- #panel, the charts grid, etc. --
     manages its own overflow) -- but #panel.pm-panel-collapsed's
     `transform: translateX(100%)` slides its box 380px+ past the
     viewport's right edge, and browsers count a transformed element's
     rendered position toward the document's scrollable area. Without
     this, that created a genuine new horizontal scrollbar the instant
     the panel was ever collapsed -- scrolling it shifted the ENTIRE
     page (map included) sideways, which is what read as the map
     suddenly "mushy"/stretched and interface pieces appearing at the
     top: nothing was actually broken, the whole viewport had just been
     scrolled to no longer align with what the map/Leaflet sizing code
     still assumed was on screen.
  */
  overflow-x: hidden;
}

/* ------------------------------------------------------------------ */
/* Top bar                                                             */
/* ------------------------------------------------------------------ */
#topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  min-height: 44px;
  z-index: 1100;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 6px;
  justify-content: space-between;
  padding: 7px 14px;
  background: linear-gradient(180deg, rgba(10, 11, 16, 0.92), rgba(10, 11, 16, 0.72));
  border-bottom: 1px solid var(--border-soft);
  backdrop-filter: blur(6px);
}

/* Both groups also wrap internally: on a narrow window even one of these
   two halves alone can be too wide for the viewport, so it needs to break
   into its own extra row rather than overflowing regardless of whether
   #topbar as a whole has already wrapped. */
#brand {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 6px;
  gap: 10px;
}

/* Same /favicon.svg every other page's topbar uses -- this used to be its
   own separate, smaller inline SVG that drifted out of sync with the real
   favicon once that got redesigned. */
#brand-mark {
  flex: none;
}

#brand-name {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--text);
}

.tab-btn {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-dim);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 10px;
  cursor: pointer;
  margin-left: 6px;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, transform 0.08s ease, box-shadow 0.15s ease;
}
.tab-btn:hover {
  color: var(--accent-soft);
  border-color: var(--accent);
}
.tab-btn:active {
  transform: scale(0.93);
}
.tab-btn small {
  color: var(--text-faint);
  font-size: 9px;
}
/* "Pressed in" look for a toggle button while its panel is open --
   distinct from :active (a momentary click), this stays on the whole
   time the charts view is showing. */
.tab-btn.pm-toggled {
  background: var(--accent);
  color: #051018;
  font-weight: 700;
  border-color: var(--accent);
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.35), 0 0 10px var(--accent-glow);
  transform: translateY(1px);
}
.tab-btn.pm-toggled:hover {
  color: #051018;
}

/* Generic tactile press feedback, applied broadly via JS (pmPressPulse)
   and natively on hoverable controls. Toggle switches are deliberately
   excluded here (see .switch-thumb's own slide transition below) --
   stacking an extra press-scale on top of the slide read as unwanted
   jitter and was asked to be removed specifically. */
.segment, input[type="range"]::-webkit-slider-thumb, .pm-select, input[type="color"] {
  transition: transform 0.1s ease;
}
.segment:active span {
  transform: scale(0.95);
}
.pm-select:active, input[type="color"]:active {
  transform: scale(0.96);
}
@keyframes pm-press-pulse {
  0% { transform: scale(1); }
  40% { transform: scale(0.9); }
  100% { transform: scale(1); }
}
.pm-press-pulse {
  animation: pm-press-pulse 0.25s ease;
}

.tab-overlay {
  position: absolute;
  inset: var(--topbar-h) 0 0 0;
  z-index: 2000;
  background: rgba(5, 6, 10, 0.55);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 40px;
}
.tab-panel {
  width: min(560px, 90%);
  max-height: 80vh;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.tab-panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border-soft);
  font-size: 13px;
  font-weight: 700;
  color: var(--accent-soft);
}
.tab-close {
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 14px;
  cursor: pointer;
  padding: 2px 6px;
}
.tab-close:hover { color: var(--text); }
.tab-panel-body {
  padding: 14px 16px;
  overflow-y: auto;
  font-size: 12px;
  color: var(--text);
}
.tab-panel-body table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 14px;
}
.tab-panel-body th {
  text-align: left;
  color: var(--text-faint);
  font-weight: 600;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 4px 8px;
  border-bottom: 1px solid var(--border-soft);
}
.tab-panel-body td {
  padding: 5px 8px;
  border-bottom: 1px solid var(--border-soft);
  font-variant-numeric: tabular-nums;
}
.tab-panel-body tr.record-row {
  cursor: pointer;
}
.tab-panel-body tr.record-row:hover td {
  background: rgba(0, 183, 255, 0.08);
  color: var(--accent-soft);
}
.tab-panel-body h3 {
  font-size: 11.5px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 6px 0;
}
.tab-panel-body .empty-note {
  color: var(--text-faint);
  font-size: 11.5px;
}

/* Universe picker (item 5) -- two big cards with a live-rendered preview
   each, side by side, plus a short explaner note underneath both. */
.universe-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
}
.universe-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 14px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 12px;
  cursor: pointer;
  font-family: inherit;
  color: var(--text);
  text-align: center;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.universe-card:hover {
  border-color: var(--text-dim);
}
.universe-card.universe-card-active {
  border-color: var(--accent);
  box-shadow: 0 0 14px var(--accent-glow);
}
.universe-preview {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  display: block;
  object-fit: cover;
  background: #05060a;
}
.universe-card h4 {
  margin: 4px 0 0 0;
  font-size: 14px;
  color: var(--accent-soft);
}
.universe-card p {
  margin: 0;
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.4;
}
.universe-note {
  margin: 18px 2px 0 2px;
  padding-top: 14px;
  border-top: 1px solid var(--border-soft);
  font-size: 11.5px;
  color: var(--text-faint);
  line-height: 1.5;
}

#hud-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 6px;
  gap: 8px;
  /* On its own wrapped line, #topbar's justify-content:space-between has
     nothing else on that line to space against, so this group collapses
     to the left edge instead of staying flush right the way it is when
     sharing a row with #brand. margin-left:auto absorbs the line's free
     space itself, so it stays right-aligned on either a shared row or its
     own row -- no dependency on what else is on the line. */
  margin-left: auto;
}

/* Map mode switch -- moved here from the side panel's settings list so the
   single most-used map control isn't buried under a "Карта" section a
   viewer has to already know to open. A sliding pill (one element, moved
   with a transform) rather than restyling each option on check: a slide
   reads as "the same control changing state", which a hard color swap on
   two independent buttons does not. */
#map-mode-group {
  display: flex;
  align-items: center;
  gap: 6px;
}
.pm-mapmode-segmented {
  position: relative;
  display: flex;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px;
}
.pm-mapmode-pill {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc(50% - 3px);
  background: var(--accent);
  border-radius: 999px;
  box-shadow: 0 0 12px var(--accent-glow);
  transition: left 0.28s cubic-bezier(0.34, 1.56, 0.64, 1), width 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
  z-index: 0;
}
/* One small, tasteful nod per map mode instead of a flat accent color for
   both: the color map is literally a rainbow of raw outcomes, so its pill
   slowly cycles through one; stability is about a steady state, so its
   pill breathes with a slow, unchanging pulse rather than shimmering. */
#map-mode-segmented[data-mode="direct"] .pm-mapmode-pill {
  background: linear-gradient(90deg, #ff5f5f, #ffd166, #06d6a0, #00b7ff, #b06bff, #ff5f5f);
  background-size: 300% 100%;
  animation: pm-mapmode-rainbow 9s linear infinite;
}
#map-mode-segmented[data-mode="stability"] .pm-mapmode-pill {
  animation: pm-mapmode-pulse 2.6s ease-in-out infinite;
}
@keyframes pm-mapmode-rainbow {
  from { background-position: 0% 50%; }
  to { background-position: 300% 50%; }
}
@keyframes pm-mapmode-pulse {
  0%, 100% { box-shadow: 0 0 8px var(--accent-glow); }
  50% { box-shadow: 0 0 22px var(--accent-glow); }
}
@media (prefers-reduced-motion: reduce) {
  #map-mode-segmented[data-mode="direct"] .pm-mapmode-pill,
  #map-mode-segmented[data-mode="stability"] .pm-mapmode-pill {
    animation: none;
  }
}
.pm-mapmode-seg {
  position: relative;
  z-index: 1;
  display: block;
  cursor: pointer;
  padding: 6px 14px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
  transition: color 0.2s ease;
}
.pm-mapmode-seg input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.pm-mapmode-seg input:checked + span {
  color: #051018;
}
.pm-mapmode-seg:hover input:not(:checked) + span {
  color: var(--text);
}
/* Always rendered, in a constant-width slot -- an earlier version only
   existed in the DOM (display:none/flex) while in Stability mode, which
   meant every mode switch visibly shoved the rest of the topbar sideways
   as this chip popped in and out. Dimming it in Direct mode (where it does
   nothing) costs a little permanently-reserved space, but that is a much
   smaller price than the whole topbar jumping on every mode switch. */
/* Icon-only now (checkbox alone, no "Контраст" label) -- square rather than
   a pill, since a pill's whole point was framing text that is no longer
   there. */
.pm-mapmode-contrast {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}
.pm-mapmode-contrast input {
  accent-color: var(--accent);
  margin: 0;
}
.pm-mapmode-contrast.pm-on {
  background: var(--accent);
  color: #051018;
  border-color: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
}
.pm-mapmode-contrast:hover {
  color: var(--text);
  border-color: var(--text-dim);
}
.pm-mapmode-contrast.pm-on:hover {
  color: #051018;
}
.pm-mapmode-contrast.pm-disabled {
  opacity: 0.35;
  cursor: default;
  pointer-events: none;
}

#time-hud {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 12px;
  color: var(--accent-soft);
  background: rgba(0, 183, 255, 0.08);
  border: 1px solid rgba(0, 183, 255, 0.25);
  border-radius: 5px;
  padding: 4px 10px;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
  transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
/* Whole readout (not just the small direction badge) goes amber while
   reversed -- reverse is a different MODE the clock is in, not just a
   detail worth a tiny icon; the same amber used everywhere else reverse
   shows up (the transport button, the badge). */
#time-hud.pm-direction-reverse {
  color: #ffb04d;
  background: rgba(255, 176, 77, 0.1);
  border-color: rgba(255, 176, 77, 0.3);
}
.pm-time-scale {
  font-size: 10.5px;
  color: var(--text-faint);
  white-space: nowrap;
}
#time-hud.pm-direction-reverse .pm-time-scale {
  color: rgba(255, 176, 77, 0.75);
}
#zoom-hud {
  font-size: 11px;
  color: var(--text-dim);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 10px;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}

#coord-search-group {
  display: flex;
  align-items: center;
}
.pm-coord-input {
  width: 0;
  min-width: 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--bg-raised);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 11.5px;
  margin-left: 0;
  opacity: 0;
  pointer-events: none;
  transition: width 0.15s ease, opacity 0.15s ease, margin-left 0.15s ease, padding 0.15s ease;
}
#coord-search-group.pm-expanded .pm-coord-input {
  width: 132px;
  padding: 6px 8px;
  border-color: var(--border);
  margin-left: 6px;
  opacity: 1;
  pointer-events: auto;
}
.pm-coord-input:focus {
  outline: none;
  border-color: var(--accent);
}
.pm-coord-unit-btn {
  width: 0;
  min-width: 0;
  padding: 0;
  margin-left: 0;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  font-family: var(--font-mono);
  font-size: 11px;
  transition: width 0.15s ease, opacity 0.15s ease, margin-left 0.15s ease, padding 0.15s ease;
}
#coord-search-group.pm-expanded .pm-coord-unit-btn {
  width: 34px;
  padding: 6px 4px;
  margin-left: 6px;
  opacity: 1;
  pointer-events: auto;
}

#bestiary-search {
  width: 180px;
  flex: none;
}
#bestiary-sort {
  width: auto;
  flex: none;
}

#home-btn {
  font-size: 14px;
  padding: 5px 9px;
  line-height: 1;
}

#auth-group {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: 4px;
  padding-left: 12px;
  border-left: 1px solid var(--border-soft);
}
#auth-username {
  font-size: 12px;
  font-weight: 700;
  color: var(--accent-soft);
}

#options-menu {
  position: relative;
}
#options-menu-btn {
  position: relative;
  font-size: 16px;
  line-height: 1;
  padding: 6px 10px;
}
.notif-count-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
}
#options-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 190px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 500;
}
.options-item {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  padding: 8px 10px;
  border-radius: 7px;
  cursor: pointer;
}
.options-item:hover {
  background: var(--bg-panel);
  color: var(--accent-soft);
}

#history-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 320px;
  max-height: 420px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 500;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#history-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-soft);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--accent-soft);
}
#history-clear-btn {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 11px;
  padding: 3px 8px;
  cursor: pointer;
}
#history-clear-btn:hover {
  color: var(--danger);
  border-color: var(--danger);
}
#history-panel-list {
  overflow-y: auto;
  padding: 6px;
}
.history-row {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  padding: 7px 8px;
  border-radius: 7px;
  cursor: pointer;
}
.history-row:hover {
  background: var(--bg-panel);
}
.history-row-swatch {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  flex-shrink: 0;
  box-shadow: 0 0 6px currentColor;
}
.history-row-coords {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-dim);
}
.history-row-age {
  flex-shrink: 0;
  font-size: 10.5px;
  color: var(--text-faint);
}
.history-empty-note {
  padding: 16px 10px;
  text-align: center;
  font-size: 12px;
  color: var(--text-faint);
}

#notifications-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 320px;
  max-height: 420px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 500;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#notifications-panel-header {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-soft);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--accent-soft);
}
#notifications-panel-list {
  overflow-y: auto;
  padding: 6px;
}
.notif-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  padding: 8px;
  border-radius: 7px;
  cursor: pointer;
}
.notif-row:hover {
  background: var(--bg-panel);
}
.notif-row.unread {
  background: rgba(127, 215, 255, 0.08);
}
.notif-row-text {
  color: var(--text);
}
.notif-row-text strong {
  color: var(--accent-soft);
}
.notif-row-age {
  font-size: 10.5px;
  color: var(--text-faint);
}

#options-settings-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 300px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 500;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
#options-settings-header {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--accent-soft);
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border-soft);
}

/* ------------------------------------------------------------------ */
/* Auth form (rendered into the shared #tab-overlay modal)             */
/* ------------------------------------------------------------------ */
.auth-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.auth-form label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 12px;
  color: var(--text-dim);
}
.auth-form input[type="text"],
.auth-form input[type="password"] {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 9px 11px;
  outline: none;
}
.auth-form input:focus {
  border-color: var(--accent);
}
.auth-form-error {
  font-size: 12px;
  color: var(--danger);
  min-height: 1em;
}
.auth-form-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-top: 4px;
}
.auth-form-switch {
  background: none;
  border: none;
  color: var(--text-faint);
  font-size: 11.5px;
  text-decoration: underline;
  cursor: pointer;
  font-family: var(--font-mono);
  padding: 0;
}
.auth-form-switch:hover {
  color: var(--accent-soft);
}

/* Brief on-screen confirmation for keyboard shortcuts */
#pm-shortcut-toast {
  position: fixed;
  left: 50%;
  bottom: 32px;
  transform: translate(-50%, 8px);
  background: rgba(18, 20, 28, 0.92);
  border: 1px solid var(--accent);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  padding: 8px 18px;
  border-radius: 999px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 14px var(--accent-glow);
  z-index: 4000;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
#pm-shortcut-toast.pm-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ------------------------------------------------------------------ */
/* Map                                                                  */
/* ------------------------------------------------------------------ */
#map {
  position: absolute;
  top: var(--topbar-h);
  left: 0;
  right: 380px;
  bottom: 0;
  background: var(--bg);
  cursor: crosshair;
}
#map.leaflet-container.leaflet-grab {
  cursor: crosshair;
}
#map.leaflet-container.leaflet-dragging.leaflet-grab {
  cursor: crosshair;
}

.pm-selection-ring {
  /* Pulses via a relative transform, not the raw "r" attribute -- "r" is
     now set from JS (markerScale, see updateSelectionIndicator), and a
     keyframe animation of the same attribute would just fight over it. */
  transform-box: fill-box;
  transform-origin: center;
  animation: pm-selection-pulse 1.6s ease-in-out infinite;
  filter: drop-shadow(0 0 4px rgba(127, 215, 255, 0.85));
}
@keyframes pm-selection-pulse {
  0% { transform: scale(1); opacity: 1; }
  70% { transform: scale(1.6); opacity: 0.25; }
  100% { transform: scale(1); opacity: 1; }
}
#map-offscreen-arrow {
  position: absolute;
  z-index: 462;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
#map-offscreen-arrow::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 9px solid transparent;
  border-right: 9px solid transparent;
  border-bottom: 16px solid var(--marker-color, #7fd7ff);
  filter: drop-shadow(0 0 5px rgba(127, 215, 255, 0.9));
  transform: scale(var(--marker-scale, 1));
}

#map-hover-label {
  position: absolute;
  z-index: 461;
  pointer-events: none;
  background: rgba(10, 11, 16, 0.85);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 6px 9px;
  font-size: 11px;
  line-height: 1.6;
  color: var(--text);
  white-space: nowrap;
  display: none;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.map-hover-deg {
  color: var(--text-faint);
}

.leaflet-control-zoom {
  display: none !important;
}

/* No baked/tiled raster layers any more (see live.js) -- every screen
   pixel is computed live at the canvas's own native resolution, so there
   is no fixed-resolution asset to ever look blocky/blurred at any zoom
   level, and nothing to seam between tiles either. */

/* ------------------------------------------------------------------ */
/* Side panel                                                          */
/* ------------------------------------------------------------------ */
#panel-collapse-btn {
  font-size: 14px;
  padding: 6px 9px;
}
/* Tucked into the bottom-right corner, small and low-contrast -- the
   original vertically-centered, 64px-tall version sat right in the middle
   of the map's edge and was too eye-catching ("очень в глаза бросается")
   for something that's only needed occasionally. */
#panel-expand-tab {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text-dim);
  font-size: 13px;
  opacity: 0.6;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease;
}
#panel-expand-tab:hover {
  opacity: 1;
  color: var(--accent-soft);
}
#panel {
  position: absolute;
  top: var(--topbar-h);
  right: 0;
  width: 380px;
  bottom: 0;
  z-index: 1000;
  background: var(--bg-panel);
  border-left: 1px solid var(--border);
  box-sizing: border-box;
  /* NO padding-top -- that 24px is moved onto #panel-sticky-top instead, see
     the long note there. With `overflow-y: auto` a scroll container clips at
     its PADDING box, not its content box, so any padding-top is a strip that
     scrolling content is visible inside -- and it sits ABOVE the sticky
     header, which can only ever stick to the padding edge. That strip is the
     "кусок интерфейса видно из под верхней панели" bug. */
  padding: 0 24px 24px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 24px;
  min-width: 260px;
  max-width: 1100px;
  transition: transform 0.28s ease;
}
/* Slides fully off-screen via transform (not display:none/width:0) so the
   collapse animates smoothly -- transform is GPU-composited and doesn't
   fight the map/other views' own `right`-offset transition (below) the
   way animating #panel's own width would. */
#panel.pm-panel-collapsed {
  transform: translateX(100%);
  pointer-events: none;
}
/* Scoped to a class toggled ONLY around the collapse/expand action itself
   (see setPanelCollapsed in app.js), NOT a permanent rule on these
   selectors -- `right` also changes on every single mousemove while
   dragging the ordinary resize handle, and a real bug shipped briefly
   this round from having this transition active then too: the map's
   `right` would animate over 280ms while the panel's own width followed
   the mouse instantly, so mid-drag (and especially mid-collapse, where
   the desync was much more visible) the map would already be full-width
   underneath a panel that hadn't finished sliding away yet -- exactly
   the "pieces of the interface show through under the map" symptom
   reported. Ordinary dragging must stay perfectly in lockstep (no
   transition at all); only the deliberate collapse/expand toggle gets
   the smooth animation.
*/
.pm-panel-animating #map,
.pm-panel-animating #charts-view,
.pm-panel-animating #multi-view,
.pm-panel-animating #bestiary-view,
.pm-panel-animating #admin-view,
.pm-panel-animating #panel-resize-handle {
  transition: right 0.28s ease;
}

/* Fixed to the viewport (not a child of #panel any more) specifically so
   it stays reachable regardless of #panel's own scroll position -- as a
   child, its "left:0/height:100%" was relative to the SCROLLING content
   box, so scrolling the panel to the bottom carried the handle away with
   it, off the top of the visible area. right: tracked live from
   setupResizable's onResize callback in app.js, matching #panel's width. */
#panel-resize-handle {
  position: fixed;
  top: var(--topbar-h);
  bottom: 0;
  /* -3px, centering the handle on the panel's boundary (see
     applyPanelWidth in app.js, which keeps this in sync as the panel is
     resized) rather than sitting entirely to its left, over the map. */
  right: 377px;
  width: 6px;
  cursor: col-resize;
  z-index: 1001;
  touch-action: none;
}
#panel-resize-handle::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 2px;
  width: 1px;
  background: transparent;
  transition: background 0.15s ease;
}
#panel-resize-handle:hover::after,
#panel-resize-handle.pm-dragging::after {
  background: var(--accent);
}

#canvas-wrap {
  position: relative;
}
#canvas-resize-handle {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -5px;
  height: 9px;
  cursor: row-resize;
  z-index: 10;
  touch-action: none;
}
#canvas-resize-handle::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 4px;
  height: 1px;
  background: transparent;
  transition: background 0.15s ease;
}
#canvas-resize-handle:hover::after,
#canvas-resize-handle.pm-dragging::after {
  background: var(--accent);
}

/* Flex items with overflow:hidden get an automatic min-height of 0 per
   spec, so when the panel's content is taller than the viewport, children
   like #stats-block (which has overflow:hidden for its rounded corners)
   would get flex-shrunk down to nothing instead of the panel just
   scrolling. Nothing in here should shrink -- let #panel's own
   overflow-y:auto handle overflow instead. */
#panel > * {
  flex-shrink: 0;
}

#panel::-webkit-scrollbar {
  width: 8px;
}
#panel::-webkit-scrollbar-track {
  background: transparent;
}
#panel::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}

/* Title/buttons/coordinates/trajectory-preview stay in place while
   everything below (stats, settings) scrolls underneath -- top:0 sticks
   flush to #panel's own scrollport edge (sticky offsets are measured from
   the scrolling ancestor's padding edge, which already accounts for
   #panel's padding-top, so no extra offset math is needed here). An
   opaque background is what actually hides the scrolling content as it
   passes behind -- sticky alone only affects position, not stacking. */
#panel-sticky-top {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg-panel);
  display: flex;
  flex-direction: column;
  gap: 24px;
  /* Item 3 (round 12) -- THE "кусок интерфейса видно там где не должно
     быть видно" bug, finally root-caused and reproduced after several
     rounds of failing to.

     #panel is a flex column with `gap: 24px`, and this sticky header only
     ever painted its OWN box (padding-bottom was 4px). A flex gap belongs
     to the container, not to either neighbour, so those 24px were never
     covered by this opaque background -- and #panel's own identical
     background is no help, because #panel is the SCROLLING box: its
     background paints on the scrollport, *behind* everything that scrolls
     over it. The gap was therefore a real 24px see-through window, and
     every stats row scrolling up through it stayed fully visible for those
     24px before finally passing behind this header.

     Confirmed by measurement, not guessed: screenshotting exactly that
     band at scrollTop 0 vs 260 gives clean background vs. a legible
     "Rod angles (θ1 /..." row. It never showed up in earlier attempts
     because it only exists mid-scroll -- every scripted check so far
     either started at, or reset to, scrollTop 0, where nothing has
     scrolled up far enough to leak through yet.

     Fix: extend the opaque padding to swallow the whole gap (4 + 24 = 28),
     then cancel that same 24px back off with a negative margin so the
     visual spacing below the header is byte-for-byte what it was. The next
     block now starts exactly where this background ends, leaving no
     transparent band between them. (The symmetric band ABOVE the header --
     #panel's own 24px padding-top -- was measured too and is already
     clean, so it deliberately isn't touched here.) */
  padding-bottom: 28px;
  margin-bottom: -24px;
  /* NOT overflow:hidden here -- #canvas-wrap's own -14px right margin
     (below) deliberately overflows ITS box into #panel's padding on
     purpose (an earlier round's fix to widen the canvas into space the
     viewer can't resize into themselves); clipping at THIS level would
     cut that off right at the boundary and undo it. The defensive clip
     for item 5 belongs on #canvas-wrap itself instead, containing only
     what's actually drawn inside it (canvas, loading spinner) without
     touching how the wrapper is positioned relative to its own parent.
  */

  /* --- the TOP and SIDES: the rest of the same bug (round 15) ---
     Session 12 sealed the flex GAP below this header and measured the strip
     ABOVE it as "already clean". That measurement was simply lucky about what
     happened to be scrolled into the strip at the one position it checked.
     Probing 24 scroll positions with elementsFromPoint instead found real
     content (`trail-settings-group`, `row`, `row-label`) painted as the
     TOPMOST element in that strip at 13 of them, with this header not present
     in the hit stack at all -- because the strip is above it.

     Why it exists: #panel is the scroll container, and a scroll container clips
     at its PADDING box. Its 24px padding-top therefore lies inside the
     scrollport, so content scrolls visibly through it, while `top: 0` can only
     stick this header to the padding edge -- i.e. below the strip. #panel's own
     background is no help: it paints on the scrollport, behind everything that
     scrolls over it.

     Fix: #panel gives up its padding-top (see there) and this header takes it,
     so this element's opaque box now starts at the very top of the scrollport.
     The negative side margins with matching side padding widen that box to the
     full panel width, so nothing can appear beside the header either; children
     land at exactly the same x as before, and #canvas-wrap's deliberate -14px
     right margin now overflows into this element's padding rather than
     #panel's, which looks identical. */
  margin-left: -24px;
  margin-right: -24px;
  padding-left: 24px;
  padding-right: 24px;
  padding-top: 24px;
}

/* The default scrollbar CORNER (where a vertical and a horizontal scrollbar
   meet) is not covered by ::-webkit-scrollbar-track, and Chrome paints it with
   its own light default -- that is the "белый квадрат в углу боковой панели".
   #panel only asks for overflow-y: auto, but a scroll container cannot have one
   axis `visible`, so overflow-x becomes auto too and the corner can appear. */
#panel::-webkit-scrollbar-corner {
  background: transparent;
}

#panel-header {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.panel-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.panel-header-row #panel-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.panel-header-btns {
  display: flex;
  align-items: center;
  flex: none;
}
.panel-header-row .tab-btn {
  flex: none;
}
#in-bestiary-badge {
  flex: none;
  cursor: default;
  color: #6fe0a0;
  border-color: rgba(111, 224, 160, 0.35);
  background: rgba(111, 224, 160, 0.08);
}
#in-bestiary-badge:hover {
  background: rgba(111, 224, 160, 0.08);
}
.tab-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  pointer-events: none;
}

#panel-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--accent-soft);
  letter-spacing: 0.01em;
}

#panel-coords {
  font-size: 11.5px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* A row of chips, not a pill. It WAS a single gold pill, and kept the pill's
   background, border, radius and padding after it started holding chips --
   which drew capsules inside a capsule, and turned a three-row block into a
   lozenge with the chips floating inside its round ends. The chips carry the
   look now; this only arranges them. */
#record-badge {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 5px;
  font-size: 10.5px;
  font-weight: 700;
  animation: pm-badge-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes pm-badge-in {
  0% { transform: scale(0.7); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

/* Click ripple on the map -- pure tactile feedback, no functional role */
.pm-ripple {
  position: absolute;
  width: 10px;
  height: 10px;
  margin-left: -5px;
  margin-top: -5px;
  border: 2px solid var(--accent-soft);
  border-radius: 50%;
  pointer-events: none;
  z-index: 300;
  animation: pm-ripple-anim 0.55s ease-out forwards;
}
@keyframes pm-ripple-anim {
  0% { transform: scale(0.3); opacity: 0.9; }
  100% { transform: scale(9); opacity: 0; }
}

/* Confetti burst for a brand new #1 site record */
.pm-confetti-piece {
  position: fixed;
  width: 7px;
  height: 7px;
  pointer-events: none;
  z-index: 3500;
  animation: pm-confetti-fall linear forwards;
}
@keyframes pm-confetti-fall {
  0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--pm-dx), var(--pm-dy)) rotate(var(--pm-rot)); opacity: 0; }
}

#canvas-wrap {
  border-radius: 10px;
  padding: 1px;
  height: 360px;
  min-height: 150px;
  max-height: 1400px;
  background: linear-gradient(160deg, rgba(0, 183, 255, 0.25), rgba(255, 95, 95, 0.08) 60%, transparent);
  /* Widened past #panel's own right padding, into space the user has no
     other way to reclaim (the resize handle only controls height) --
     syncCanvasSize()'s existing ResizeObserver already re-measures and
     redraws at whatever size this box actually ends up, no JS change
     needed for the canvas itself to fill it. */
  margin-right: -14px;
  transition: box-shadow 0.15s ease;
}
#canvas-wrap.pm-zoom-armed {
  box-shadow: 0 0 0 2px var(--accent), 0 0 14px var(--accent-glow);
}
#pendulum-canvas {
  cursor: zoom-in;
}

#canvas-loading {
  position: absolute;
  inset: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 38%, #0c0f18, #05060a 70%);
  border-radius: 9px;
  z-index: 5;
}
.pm-spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  animation: pm-spin 0.8s linear infinite;
}
@keyframes pm-spin {
  to { transform: rotate(360deg); }
}

#pendulum-canvas {
  display: block;
  width: 100%;
  height: 100%;
  background: radial-gradient(circle at 50% 38%, #0c0f18, #05060a 70%);
  border-radius: 9px;
}

/* ------------------------------------------------------------------ */
/* Stats                                                                */
/* ------------------------------------------------------------------ */
/* Grid instead of a single flex column: on a wide panel, plain
   label/value stat-rows would otherwise stretch full-width with a huge
   empty gap between the two -- auto-fill packs as many as actually fit
   per row instead, collapsing back to one column on a narrow panel. Bar
   rows (.row-bar-row) opt back out via grid-column: 1/-1 below, since a
   progress bar squeezed into a narrow grid cell isn't readable. */
#stats-block {
  display: grid;
  /* 230px, not 160: a label and its value have to share the cell, and at 160
     the label is what loses -- on a wide panel that produced six cramped
     columns where three roomy ones read far better, cutting "Устойчивость"
     down to "Устойчиво…" and one label to a single character. auto-fit rather
     than auto-fill so a narrow panel collapses to one full-width column
     instead of leaving empty tracks behind. */
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1px;
  background: var(--border-soft);
  border: 1px solid var(--border-soft);
  border-radius: 8px;
  overflow: hidden;
}

.stat-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font-size: 12px;
  background: var(--bg-raised);
  padding: 11px 14px;
}

.stat-key {
  color: var(--text-faint);
  /* English labels run noticeably longer than the Russian originals (e.g.
     "Max. tip acceleration" vs "Макс. ускорение кончика") -- in a narrow
     grid column that pushed the VALUE onto a second line instead, and
     since the value's own digit count shifts frame to frame, whether it
     happened to still fit varied, which read as the whole row jumping in
     height continuously. The label truncates with an ellipsis instead;
     the value (see below) never wraps or shrinks at all. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stat-val {
  color: var(--text);
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* flex-shrink:0 -- the number itself should never be the thing that
     truncates when a row is tight on width; .stat-key (the label, still
     shrinkable/ellipsized) absorbs the deficit instead. The one row whose
     VALUE is a long phrase rather than a short number (#live-stopwatch-row)
     is handled separately below instead of by shrinking this one. */
  flex-shrink: 0;
}
/* This one row's value is a whole phrase ("Xc, waiting for first repeat"),
   not a short number like its neighbors -- full width instead of sharing
   the narrower 2-column grid cell that works fine for everything else, AND
   stacked (label above value) rather than side-by-side: even at full
   width, a long label plus a long phrase-value together don't reliably
   both fit on one line without truncating one of them. */
#live-stopwatch-row {
  grid-column: 1 / -1;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
#live-stopwatch-row .stat-val {
  text-align: left;
}

#live-block {
  display: grid;
  /* 230px, not 160: a label and its value have to share the cell, and at 160
     the label is what loses -- on a wide panel that produced six cramped
     columns where three roomy ones read far better, cutting "Устойчивость"
     down to "Устойчиво…" and one label to a single character. auto-fit rather
     than auto-fill so a narrow panel collapses to one full-width column
     instead of leaving empty tracks behind. */
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1px;
  background: var(--border-soft);
  border: 1px solid rgba(0, 183, 255, 0.3);
  border-radius: 8px;
  overflow: hidden;
}
#live-block .stat-row {
  background: rgba(0, 183, 255, 0.05);
}
#live-block .stat-val {
  color: var(--accent-soft);
}
#live-block .stat-val.pm-newmax {
  animation: pm-newmax-flash 0.5s ease-out;
}
@keyframes pm-newmax-flash {
  0% { color: #ffd76a; text-shadow: 0 0 8px rgba(255, 215, 106, 0.8); }
  100% { color: var(--accent-soft); text-shadow: none; }
}
#live-block .stat-val.pm-diamond {
  animation: pm-diamond-flash 1.4s ease-out;
}
#live-block .stat-val.pm-autoslow-active {
  color: #ffb15f;
  text-shadow: 0 0 8px rgba(255, 177, 95, 0.7);
  animation: pm-autoslow-pulse 0.6s ease-in-out infinite;
}
@keyframes pm-autoslow-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.6; }
}
@keyframes pm-diamond-flash {
  0% { color: #f2fdff; text-shadow: 0 0 10px rgba(200, 245, 255, 1), 0 0 24px rgba(150, 225, 255, 0.9), 0 0 40px rgba(120, 210, 255, 0.5); }
  100% { color: var(--accent-soft); text-shadow: none; }
}

#extra-block {
  display: grid;
  /* 230px, not 160: a label and its value have to share the cell, and at 160
     the label is what loses -- on a wide panel that produced six cramped
     columns where three roomy ones read far better, cutting "Устойчивость"
     down to "Устойчиво…" and one label to a single character. auto-fit rather
     than auto-fill so a narrow panel collapses to one full-width column
     instead of leaving empty tracks behind. */
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1px;
  background: var(--border-soft);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
#extra-block .stat-row,
#extra-block .row-bar-row {
  background: var(--bg-raised);
}

.row-bar-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 11px 14px;
  font-size: 12px;
  grid-column: 1 / -1;
}
.row-bar-row .row-label {
  flex: none;
  color: var(--text-faint);
  white-space: nowrap;
}
.pm-bar {
  position: relative;
  flex: 1;
  height: 8px;
  min-width: 60px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.pm-bar-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  background: var(--accent);
  transition: width 0.12s linear;
  border-radius: 999px;
}
.pm-bar-dual {
  display: flex;
}
.pm-bar-dual .pm-bar-fill {
  position: static;
  border-radius: 0;
}
.pm-bar-t { background: #ff9a5f; }
.pm-bar-u { background: #5fb3ff; }
.pm-bar-lyap { background: linear-gradient(90deg, #4dff9e, #ffd76a, #ff5f5f); }

.stat-key[data-tip],
.switch-label[data-tip] {
  cursor: help;
  border-bottom: 1px dotted var(--text-faint);
}

.pm-value-clickable {
  cursor: pointer;
  border-bottom: 1px dotted var(--accent-soft);
  transition: color 0.12s ease;
}
.pm-value-clickable:hover {
  color: var(--accent-soft);
}

.pm-tooltip {
  position: fixed;
  z-index: 3000;
  max-width: 300px;
  background: var(--bg-raised);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 0 16px var(--accent-glow);
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px) scale(0.98);
  transition: opacity 0.12s ease, transform 0.12s ease, max-width 0.12s ease;
}
.pm-tooltip.pm-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
}
.pm-tooltip.pm-has-formula {
  max-width: 600px;
}
.pm-tooltip h4 {
  margin: 0 0 4px 0;
  font-size: 12px;
  color: var(--accent-soft);
}
.pm-tooltip .pm-formula {
  margin: 9px 0 0 0;
  padding: 11px 15px;
  background: rgba(0, 183, 255, 0.08);
  border: 1px solid rgba(0, 183, 255, 0.2);
  border-radius: 7px;
  color: var(--accent-soft);
  font-variant-numeric: tabular-nums;
  position: relative;
  /* overflow was "auto" (a scroll fallback for a formula that doesn't
     fit) but Chromium ignores scrollbar-width:thin and renders its full
     chunky default scrollbar instead -- which flashed in right as the
     clip-path reveal finished, reading as a stray "slider" popping in
     after the shimmer. The tooltip box is now wide enough (600px) that
     the fallback was essentially never needed in practice; hidden is a
     quieter fallback for the rare edge case than a flashing scrollbar. */
  overflow: hidden;
  /* "Draws itself in": the box wipes open left-to-right, then a soft
     diagonal shimmer sweeps across the freshly-revealed formula -- purely
     cosmetic, re-triggered on every hover by showTooltip() forcing a
     reflow before re-adding pm-visible. */
  clip-path: inset(0 100% 0 0 round 7px);
  transition: clip-path 0.55s cubic-bezier(0.16, 1, 0.3, 1);
}
.pm-tooltip.pm-visible .pm-formula {
  clip-path: inset(0 0 0 0 round 7px);
}
.pm-tooltip .pm-formula::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -35%;
  width: 30%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.4), transparent);
  transform: skewX(-15deg);
  opacity: 0;
  pointer-events: none;
}
.pm-tooltip.pm-visible .pm-formula::after {
  animation: pmFormulaSweep 0.75s ease-out 0.15s 1;
}
@keyframes pmFormulaSweep {
  0% { left: -35%; opacity: 0; }
  18% { opacity: 0.9; }
  100% { left: 120%; opacity: 0; }
}
.pm-formula-row {
  white-space: nowrap;
}
.pm-formula-row + .pm-formula-row {
  margin-top: 6px;
}
.pm-formula-note {
  margin-top: 5px;
  font-size: 10.5px;
  font-style: normal;
  color: var(--text-faint);
  white-space: nowrap;
}

/* ------------------------------------------------------------------ */
/* Settings groups                                                      */
/* ------------------------------------------------------------------ */
/* Grid (not flex) purely so spacing can go on margin-top per child
   instead of the `gap` shorthand -- flex `gap` on a <details> element
   turned out not to reliably apply between every child in this Chromium
   build (confirmed by measuring: consecutive .row-split siblings landed
   0px apart despite the container reporting a 16px computed gap).
   Margin-top is directly verifiable per row instead of trusting the
   shorthand. A 2-column layout for switch-only rows was tried (to stop a
   short toggle from burning a full-width line) and reverted -- at this
   panel's real width, switch labels like "Тангенц. / центростр.
   ускорение" wrapped 2-3 lines in a half column, worse than the empty
   space it was meant to fix. padding-top is intentionally smaller than
   the OPEN case below -- with several sections collapsed at once (no
   content rendered at all) the only visible gap between them IS this
   padding, and 22px per collapsed header was the "these are too far
   apart" complaint. */
.settings-group {
  border-top: 1px solid var(--border-soft);
  padding-top: 13px;
  display: grid;
}
.settings-group[open] {
  padding-top: 22px;
  padding-bottom: 2px;
}
.settings-group > * {
  margin-left: 1px;
  margin-top: 15px;
}
.settings-group > summary {
  margin-top: 0;
}

.settings-title {
  margin: 0;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
  list-style: none;
  transition: color 0.15s ease;
}
.settings-title::-webkit-details-marker {
  display: none;
}
.settings-title::before {
  content: "▸";
  display: inline-block;
  font-size: 9px;
  color: var(--accent-soft);
  transition: transform 0.15s ease;
  flex: none;
}
.settings-group[open] > .settings-title::before {
  transform: rotate(90deg);
}
.settings-title:hover {
  color: var(--text-dim);
}
.settings-icon {
  flex: none;
  width: 21px;
  height: 21px;
  color: var(--accent-soft);
  opacity: 0.9;
}
.settings-icon path, .settings-icon circle, .settings-icon rect, .settings-icon line {
  stroke: currentColor;
  fill: none;
}
.settings-title small {
  font-size: 10px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text-faint);
  opacity: 0.7;
}

.row {
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 12px;
  color: var(--text);
}
/* .row-bar-row elements carry BOTH "row" and "row-bar-row" -- .row's own
   column direction (for plain label+slider rows) otherwise silently wins
   here on source order (same specificity, .row is declared later), which
   collapsed the T/U and Lyapunov-meter bars down to a couple of pixels
   tall (flex-basis:0% resolving against the wrong/vertical main axis) --
   they LOOKED like empty rows with nothing but a label, because the bar
   really was rendering, just squashed to ~2px. The compound selector
   below has strictly higher specificity than either single-class rule, so
   it wins regardless of source order. */
.row.row-bar-row {
  flex-direction: row;
}

.row-label {
  display: flex;
  justify-content: space-between;
  color: var(--text-dim);
}
.row-value {
  color: var(--accent-soft);
  font-variant-numeric: tabular-nums;
}

/* A #part-editor-* pane's own rows had NO spacing rule at all -- .row's
   normal top-margin comes from ".settings-group > *", which only reaches
   DIRECT children of the <details>, and a pane's rows are one level
   deeper. Left alone they sat flush against each other while unrelated
   top-level blocks (a whole pane vs. the copy/paste row vs. the reset
   button) got the full 15px gap regardless of relation -- uniform spacing
   that carried no meaning. Two tiers instead: a wider gap BETWEEN groups
   (colour+custom-colour is one group, glow+opacity is another) and a
   tighter one WITHIN a group, so the spacing itself shows which controls
   go together. */
.pm-part-editor > * {
  margin-top: 14px;
}
.pm-part-editor > *:first-child {
  margin-top: 0;
}
.pm-part-row-group > * {
  margin-top: 8px;
}
.pm-part-row-group > *:first-child {
  margin-top: 0;
}

/* The opacity percentage doubles as a fast on/off switch (click it: 0<->100,
   see wirePartExtras in app.js) now that the separate show/hide toggle is
   gone -- styled distinctly from a plain read-only .row-value so that is
   discoverable rather than a hidden Easter egg. */
.pm-opacity-val {
  cursor: pointer;
  border-bottom: 1px dashed var(--accent-soft);
  transition: color 0.15s ease, border-color 0.15s ease;
}
.pm-opacity-val:hover {
  color: var(--text);
  border-color: var(--text);
}

/* Copy/paste: one row, two buttons sharing the width evenly. Plain .tab-btn
   would stack them (the panel's rows are all flex-column, see .row), so
   this gets its own explicit horizontal layout instead of relying on that. */
.pm-part-copypaste {
  display: flex;
  flex-direction: row;
  gap: 8px;
}
.pm-part-copypaste .tab-btn {
  flex: 1;
  margin-left: 0;
  text-align: center;
}

/* Appearance presets: 3 equal slots, each a small thumbnail (rendered at a
   fixed reference pose -- see PRESET_REF_T1/T2 in app.js -- so the three
   are visually comparable to each other, not just to themselves) plus its
   own save/load pair. */
.pm-preset-heading {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 8px;
}
.pm-preset-slots {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.pm-preset-slot {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.pm-preset-thumb-wrap {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* A live canvas, not a saved PNG: it re-renders from the slot's stored
   settings every frame, so it always shows the CURRENT meaning of those
   settings (a "physics" colour follows the map's colour scheme, and follows
   the pose while you drag the tip around) rather than a picture of what
   they meant at save time. */
.pm-preset-thumb-canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
  touch-action: none;
}
.pm-preset-thumb-canvas.pm-dragging {
  cursor: grabbing;
}
.pm-preset-empty {
  font-size: 10.5px;
  color: var(--text-faint);
  text-align: center;
  padding: 0 4px;
}
.pm-preset-slot-actions {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.pm-preset-slot-actions .tab-btn {
  margin-left: 0;
  width: 100%;
  text-align: center;
  font-size: 10px;
  padding: 4px 6px;
}
/* Share and clear sit side by side as icons: they are the rarer of the four
   actions, and giving them full-width text buttons of their own would have
   made a slot taller than its own thumbnail. */
.pm-preset-slot-icons {
  display: flex;
  gap: 4px;
}
.pm-preset-slot-icons .tab-btn {
  flex: 1;
  padding: 3px 0;
  font-size: 11px;
}
.pm-preset-slot-actions .tab-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.pm-preset-slot-actions .tab-btn:disabled:hover {
  color: var(--text-dim);
  border-color: var(--border);
}

/* Segmented control (map mode) ---------------------------------------- */
.segmented {
  display: flex;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 3px;
  gap: 3px;
}
.segment {
  flex: 1;
  position: relative;
  cursor: pointer;
}
.segment input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.segment span {
  display: block;
  text-align: center;
  font-size: 11.5px;
  padding: 7px 6px;
  border-radius: 6px;
  color: var(--text-dim);
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.segment input:checked + span {
  background: var(--accent);
  color: #051018;
  font-weight: 700;
  box-shadow: 0 0 12px var(--accent-glow);
}
.segment:hover input:not(:checked) + span {
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
}

/* Direct-coloring map presets -- small card grid with a live-generated
   gradient swatch per preset (see the preset wiring in app.js). */
.pm-preset-grid {
  display: grid;
  /* Was a fixed 4 columns -- at the default (narrow) panel width that
     squeezed each swatch down to ~63x40 CSS pixels, genuinely too small
     for a gradient to read as anything but mush, independent of the
     canvas's own backing resolution. auto-fill with a real minimum width
     wraps to fewer, bigger cards on a narrow panel and more on a wide
     one, instead of always cramming a fixed count into whatever space
     happens to be available. */
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
  margin-top: 8px;
}
.pm-preset-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 6px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  color: var(--text-dim);
  font-size: 10px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
}
.pm-preset-card:hover {
  border-color: var(--text-dim);
}
.pm-preset-card.pm-preset-active {
  border-color: var(--accent);
  color: var(--accent-soft);
  font-weight: 700;
  box-shadow: 0 0 10px var(--accent-glow);
}
.pm-preset-swatch {
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 5px;
  display: block;
  /* This is a PREVIEW swatch, not the map itself -- smooth (not
     pixelated) rendering, plus a much higher backing resolution set in
     the HTML (was 48x36, now 96x96, matching this square aspect ratio),
     is what actually fixed the muddy/blocky look: at the old size,
     nearest-neighbor upscaling of a coarse gradient read as mushy rather
     than a clean color preview. */
  image-rendering: auto;
}

/* Time-reverse control -- a double-chevron toggle instead of the generic
   on/off switch used elsewhere, since "which way is time flowing" reads
   more directly as a direction icon than a label+checkbox. The real state
   is still the (visually hidden) #time-reverse checkbox -- this button
   just drives it via a click handler in app.js, so keyboard shortcut
   (KeyR) and this button always agree. */
.pm-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}
/* Transport cluster -- play/pause and reverse as a pair of round icon
   buttons side by side (a small media-player toolbar), with one shared
   text label to their right describing the resulting state in words
   ("Идёт вперёд"/"Идёт назад"/"Пауза") -- the icons alone carry the
   controls, the label removes any doubt about what they currently mean. */
.pm-transport {
  display: flex;
  align-items: center;
  gap: 10px;
}
.pm-transport-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 50%;
  cursor: pointer;
  color: var(--text-dim);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
.pm-transport-btn:hover {
  border-color: var(--text-dim);
  transform: scale(1.06);
}
.pm-transport-label {
  font-size: 12px;
  color: var(--text-dim);
}

/* Play/pause morph: a triangle that scales down to nothing while two
   bars scale up from nothing, so it reads as one shape smoothly turning
   into the other rather than two icons abruptly swapping. */
.pm-transport-icon .pm-play-tri,
.pm-transport-icon .pm-pause-bar-l,
.pm-transport-icon .pm-pause-bar-r {
  fill: currentColor;
  transform-box: fill-box;
  transition: transform 0.25s ease, opacity 0.2s ease;
}
.pm-transport-icon .pm-play-tri { transform-origin: 35% 50%; }
.pm-transport-icon .pm-pause-bar-l,
.pm-transport-icon .pm-pause-bar-r { transform-origin: 50% 50%; opacity: 0; transform: scaleY(0); }
#time-pause-btn.pm-paused {
  border-color: var(--accent);
  color: var(--accent-soft);
  box-shadow: 0 0 10px var(--accent-glow);
}
#time-pause-btn.pm-paused .pm-transport-icon .pm-play-tri { opacity: 0; transform: scale(0); }
#time-pause-btn.pm-paused .pm-transport-icon .pm-pause-bar-l,
#time-pause-btn.pm-paused .pm-transport-icon .pm-pause-bar-r { opacity: 1; transform: scaleY(1); }

.pm-reverse-toggle {
  padding: 0;
}
.pm-reverse-icon path {
  fill: currentColor;
  transition: transform 0.3s ease;
}
.pm-reverse-icon path:first-child { transform-origin: 11px 10px; }
.pm-reverse-icon path:last-child { transform-origin: 12px 10px; }
/* Reversed: icon flips to point left, button glows amber (distinct from
   the accent blue used for "on" everywhere else -- reverse isn't just
   "enabled", it's a different MODE, worth a different color) and the
   chevrons drift gently to suggest motion, distinguishing it at a glance
   from a plain paused/stopped state. */
.pm-reverse-toggle.pm-reversed {
  border-color: #ffb04d;
  color: #ffb04d;
  background: rgba(255, 176, 77, 0.12);
  box-shadow: 0 0 10px rgba(255, 176, 77, 0.35);
}
.pm-reverse-toggle.pm-reversed .pm-reverse-icon {
  transform: scaleX(-1);
  animation: pm-reverse-drift 1.1s ease-in-out infinite;
}
@keyframes pm-reverse-drift {
  0%, 100% { transform: scaleX(-1) translateX(0); }
  50% { transform: scaleX(-1) translateX(-3px); }
}

/* Top-of-page direction badge, next to the time HUD (item 2 -- a
   glanceable "which way is time going" independent of reading the
   transport controls in the settings panel below). */
.pm-direction-badge {
  display: inline-block;
  font-size: 11px;
  color: var(--accent-soft);
  margin-right: 2px;
  transition: color 0.2s ease;
}
.pm-direction-badge.pm-direction-reverse {
  color: #ffb04d;
}

/* Toggle switches ------------------------------------------------------ */
.switch {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  cursor: pointer;
  user-select: none;
}
/* Item 1 (round 9): a control disabled because it's not physically
   meaningful for the current universe (e.g. "gravity at the tip" where
   the tip carries no mass), not just temporarily unavailable. */
.row-disabled-note {
  opacity: 0.4;
}
.row-disabled-note .switch {
  cursor: not-allowed;
}
.switch input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.switch-track {
  position: relative;
  width: 34px;
  height: 19px;
  flex: none;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--text-faint);
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.15s ease;
}
.switch input:checked + .switch-track {
  background: rgba(0, 183, 255, 0.25);
  border-color: var(--accent);
}
.switch input:checked + .switch-track .switch-thumb {
  transform: translateX(15px);
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent-glow);
}
.switch-label {
  font-size: 12.5px;
  color: var(--text);
}

/* Range sliders ---------------------------------------------------------- */
/* Range inputs -------------------------------------------------------
   The input is as tall as its thumb and transparent; the visible track is
   drawn by the track pseudo-element inside it. Styling the input itself as
   the track meant the thumb overflowed a 4px box and had to be dragged into
   place with a negative margin -- which has to allow for the border, and
   has to be retuned whenever the track thickens. It was a pixel or two high
   everywhere as a result. */
:root {
  --pm-track-h: 4px;
  --pm-track-h-hover: 6px;
  --pm-thumb: 15px;
}
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  /* Tall enough to contain the thumb, so nothing overflows and nothing needs
     to be nudged. */
  height: var(--pm-thumb);
  background: transparent;
  border: none;
  outline: none;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: var(--pm-track-h);
  border-radius: 999px;
  /* --pm-fill is the thumb's position as a percentage, published by app.js:
     WebKit has no fill pseudo-element, so the filled part is a hard gradient
     stop. */
  background:
    linear-gradient(to right,
      var(--accent) 0 var(--pm-fill, 0%),
      var(--bg-raised) var(--pm-fill, 0%) 100%);
  border: 1px solid var(--border);
  transition: height 0.12s ease, filter 0.12s ease;
}
input[type="range"]:hover::-webkit-slider-runnable-track,
input[type="range"]:active::-webkit-slider-runnable-track {
  height: var(--pm-track-h-hover);
  filter: brightness(1.12);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: var(--pm-thumb);
  height: var(--pm-thumb);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow), 0 0 0 3px rgba(0, 183, 255, 0.12);
  cursor: pointer;
  /* Half the track (including its 1px borders) minus half the thumb, plus a
     -1.5px correction: that plain formula reliably rendered the thumb 1.5px
     LOW of the track's actual center in this Chromium (measured directly,
     pixel by pixel, at 8x device-scale-factor -- not a value found by eye).
     Whatever WebKit box the thumb actually anchors to isn't quite the one
     the naive box-model math assumes; the formula stays so this still
     tracks --pm-track-h/--pm-thumb if either changes, it's just no longer
     pretending the two heights alone are the whole story. */
  margin-top: calc((var(--pm-track-h) + 2px - var(--pm-thumb)) / 2 - 1.5px);
  transition: transform 0.11s ease, box-shadow 0.16s ease;
}
input[type="range"]:hover::-webkit-slider-thumb,
input[type="range"]:active::-webkit-slider-thumb {
  margin-top: calc((var(--pm-track-h-hover) + 2px - var(--pm-thumb)) / 2 - 1.5px);
}
input[type="range"]:hover::-webkit-slider-thumb {
  transform: scale(1.14);
  box-shadow: 0 0 12px var(--accent-glow), 0 0 0 5px rgba(0, 183, 255, 0.16);
}
input[type="range"]:active::-webkit-slider-thumb {
  transform: scale(1.32);
  box-shadow: 0 0 16px var(--accent-glow), 0 0 0 9px rgba(0, 183, 255, 0.2);
}
/* Firefox centres the thumb on the track itself and has a real fill element,
   so it needs neither the offset nor the gradient. */
input[type="range"]::-moz-range-track {
  height: var(--pm-track-h);
  border-radius: 999px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
}
input[type="range"]::-moz-range-progress {
  height: var(--pm-track-h);
  border-radius: 999px;
  background: var(--accent);
}
input[type="range"]::-moz-range-thumb {
  width: var(--pm-thumb);
  height: var(--pm-thumb);
  border: none;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow);
  cursor: pointer;
  transition: transform 0.11s ease, box-shadow 0.16s ease;
}
input[type="range"]:hover::-moz-range-thumb {
  transform: scale(1.14);
  box-shadow: 0 0 12px var(--accent-glow);
}
input[type="range"]:active::-moz-range-thumb {
  transform: scale(1.32);
  box-shadow: 0 0 16px var(--accent-glow);
}

.pm-select {
  width: 100%;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  outline: none;
  cursor: pointer;
}
.pm-select:hover {
  border-color: var(--accent);
}

input[type="color"] {
  width: 34px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: none;
  cursor: pointer;
}

/* ------------------------------------------------------------------ */
/* Charts view -- replaces the map viewport, not a floating modal      */
/* ------------------------------------------------------------------ */
#charts-view {
  position: absolute;
  top: var(--topbar-h);
  left: 0;
  right: 380px;
  bottom: 0;
  background: var(--bg);
  z-index: 900;
  display: flex;
  flex-direction: column;
}
.charts-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border-soft);
  font-size: 13px;
  font-weight: 700;
  color: var(--accent-soft);
  flex: none;
  gap: 16px;
  flex-wrap: wrap;
}
.charts-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 400;
}
.charts-window-label {
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
}
.charts-window-label span {
  color: var(--accent-soft);
  font-variant-numeric: tabular-nums;
}
#chart-window {
  width: 140px;
}
#charts-grid {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 14px;
  align-content: start;
}
.chart-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
}
.chart-card h4 {
  margin: 0 0 3px 0;
  font-size: 12px;
  color: var(--text);
}
.chart-card p {
  margin: 0 0 8px 0;
  font-size: 10.5px;
  color: var(--text-faint);
  line-height: 1.4;
}
.chart-card canvas {
  width: 100%;
  height: 210px;
  display: block;
  border-radius: 6px;
  background: #05060a;
}
.chart-card.chart-card-phase canvas {
  height: 260px;
}
#charts-empty {
  padding: 40px;
  text-align: center;
  color: var(--text-faint);
  font-size: 13px;
}

/* ------------------------------------------------------------------ */
/* Bestiary + admin views -- same "replaces the viewport" pattern as    */
/* charts-view/multi-view.                                             */
/* ------------------------------------------------------------------ */
#bestiary-view, #admin-view {
  position: absolute;
  top: var(--topbar-h);
  left: 0;
  right: 380px;
  bottom: 0;
  background: var(--bg);
  z-index: 900;
  display: flex;
  flex-direction: column;
}
#bestiary-grid {
  flex: 1;
  overflow-y: auto;
  padding: 18px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 18px;
  align-content: start;
}
.bestiary-card {
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--bg-panel);
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.bestiary-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--accent);
  border-color: var(--accent);
}
.bestiary-card img {
  display: block;
  width: 100%;
  height: auto;
}
.bestiary-card-del {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid rgba(255, 95, 95, 0.5);
  background: rgba(10, 11, 16, 0.75);
  color: var(--danger);
  font-size: 13px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.bestiary-card-del:hover {
  background: var(--danger);
  color: #fff;
}
.bestiary-card-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 3px 8px;
  border-radius: 999px;
  backdrop-filter: blur(2px);
}
.bestiary-card-badge.stable {
  color: #4dff9e;
  background: rgba(77, 255, 158, 0.14);
  border: 1px solid rgba(77, 255, 158, 0.4);
}
.bestiary-card-badge.chaotic {
  color: #ff8fa3;
  background: rgba(255, 143, 163, 0.14);
  border: 1px solid rgba(255, 143, 163, 0.4);
}
.bestiary-universe-badge {
  left: auto;
  right: 8px;
  color: var(--accent-soft);
  background: rgba(0, 183, 255, 0.14);
  border: 1px solid rgba(0, 183, 255, 0.4);
}
#bestiary-empty, #admin-empty {
  padding: 60px 20px;
  text-align: center;
  color: var(--text-faint);
  font-size: 13px;
}
#admin-grid {
  flex: 1;
  overflow-y: auto;
  padding: 18px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: 16px;
  align-content: start;
}
#admin-grid .chart-card canvas {
  height: 220px;
}
.admin-totals {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  grid-column: 1 / -1;
}
.admin-total-tile {
  flex: 1 1 140px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
}
.admin-total-tile .val {
  font-size: 26px;
  font-weight: 700;
  color: var(--accent-soft);
  font-variant-numeric: tabular-nums;
}
.admin-total-tile .lbl {
  font-size: 11px;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-top: 4px;
}

/* Country/language breakdown -- horizontal bar lists inside their own
   chart-card, same visual family as the line charts next to them. */
.admin-breakdown-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  grid-column: 1 / -1;
}
.admin-breakdown-row {
  display: grid;
  grid-template-columns: 60px 1fr 90px;
  align-items: center;
  gap: 10px;
  padding: 4px 0;
  font-size: 12px;
}
.admin-breakdown-label {
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.admin-breakdown-bar-track {
  height: 8px;
  border-radius: 4px;
  background: var(--bg-raised);
  overflow: hidden;
}
.admin-breakdown-bar-fill {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--accent), var(--accent-soft));
  border-radius: 4px;
}
.admin-breakdown-count {
  color: var(--text-faint);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.admin-breakdown-empty {
  color: var(--text-faint);
  font-size: 12px;
  padding: 6px 0;
}

/* Shift-drag marquee selection rectangle on the map */
.pm-marquee {
  position: absolute;
  border: 1.5px dashed var(--accent);
  background: rgba(0, 183, 255, 0.1);
  pointer-events: none;
  z-index: 350;
}

.pm-zoom-frame {
  position: absolute;
  border: 2px dashed var(--accent);
  background: rgba(0, 183, 255, 0.08);
  pointer-events: none;
  z-index: 350;
  box-sizing: border-box;
  border-radius: 2px;
}
.pm-zoom-frame-label {
  position: absolute;
  transform: translate(-50%, -100%);
  background: var(--bg-raised);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 12px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--accent-soft);
  white-space: nowrap;
  pointer-events: none;
  z-index: 351;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}
.pm-marquee.pm-marquee-line {
  border: none;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow);
}

/* ------------------------------------------------------------------ */
/* Multi-select view -- same "replaces the viewport" pattern as        */
/* charts-view. All selected pendulums are drawn "strung together" on  */
/* ONE shared canvas (same pivot, same clock) rather than as separate  */
/* mini-canvases -- the point is comparing them AS an overlay, the way */
/* sensitivity-to-initial-conditions demos usually look. The list on   */
/* the right is plain rows (not its own canvas each); hovering a row   */
/* highlights that one pendulum in the shared canvas and fades the     */
/* rest, rather than the canvas being split up.                        */
/* ------------------------------------------------------------------ */
#multi-view {
  position: absolute;
  top: var(--topbar-h);
  left: 0;
  right: 380px;
  bottom: 0;
  background: var(--bg);
  z-index: 900;
  display: flex;
  flex-direction: column;
}
.multi-hint {
  font-size: 11px;
  color: var(--text-faint);
  font-weight: 400;
}
#multi-settings-menu {
  position: relative;
}
#multi-settings-btn {
  font-size: 14px;
  line-height: 1;
  padding: 5px 9px;
}
#multi-settings-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 280px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  z-index: 500;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
#multi-body {
  flex: 1;
  display: flex;
  overflow: hidden;
  position: relative;
}
#multi-canvas-wrap {
  flex: 1;
  position: relative;
  min-width: 200px;
}
#multi-canvas {
  width: 100%;
  height: 100%;
  display: block;
  background: radial-gradient(circle at 50% 42%, #0c0f18, #05060a 70%);
}
#multi-resize-handle {
  width: 7px;
  flex: none;
  cursor: col-resize;
  position: relative;
  touch-action: none;
}
#multi-resize-handle::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 3px;
  width: 1px;
  background: var(--border);
  transition: background 0.15s ease;
}
#multi-resize-handle:hover::after,
#multi-resize-handle.pm-dragging::after {
  background: var(--accent);
}
#multi-list {
  width: 320px;
  min-width: 220px;
  max-width: 640px;
  flex: none;
  overflow-y: auto;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-left: 1px solid var(--border-soft);
  background: var(--bg-panel);
}
.multi-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 13px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  cursor: pointer;
  transition: opacity 0.15s ease, border-color 0.15s ease, background 0.15s ease;
  font-size: 10.5px;
  color: var(--text-dim);
  line-height: 1.5;
}
.multi-row:hover {
  border-color: var(--accent);
}
.multi-row.pm-mini-dimmed {
  opacity: 0.3;
}
.multi-row.pm-mini-active {
  border-color: var(--accent);
  background: rgba(0, 183, 255, 0.08);
  box-shadow: 0 0 10px var(--accent-glow);
}
.multi-row.pm-mini-anchor .multi-row-swatch {
  box-shadow: 0 0 0 2px #ffd76a;
}
.multi-row.pm-mini-hidden {
  opacity: 0.35;
}
.multi-row.pm-mini-hidden .multi-row-swatch {
  background: transparent !important;
  border: 1.5px dashed var(--text-faint);
}
.multi-row-swatch {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  flex: none;
}
.multi-row-hide-btn {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-faint);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  order: 99;
  margin-left: auto;
}
.multi-row-hide-btn:hover {
  background: var(--bg-panel);
  color: var(--accent-soft);
}
.multi-row-text {
  min-width: 0;
  font-variant-numeric: tabular-nums;
}
.multi-row-text .mini-anchor-tag {
  color: #ffd76a;
  font-weight: 700;
}
.multi-row-text .mini-delta {
  color: var(--accent-soft);
}
.multi-row-text .mini-stability-tag {
  display: inline-block;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 1px 5px;
  border-radius: 4px;
  margin-left: 5px;
  vertical-align: middle;
}
.multi-row-text .mini-stability-tag.stable {
  color: #4dff9e;
  background: rgba(77, 255, 158, 0.12);
  border: 1px solid rgba(77, 255, 158, 0.35);
}
.multi-row-text .mini-stability-tag.chaotic {
  color: #ff8fa3;
  background: rgba(255, 143, 163, 0.12);
  border: 1px solid rgba(255, 143, 163, 0.35);
}

/* Pretty math formulas in tooltips: real stacked fractions + Greek/sub
   glyphs instead of plain ASCII "a/b" text -- sized up so a formula reads
   like an actual textbook line, not a cramped footnote. */
.pm-math {
  font-family: "Cambria Math", "STIX Two Math", "Times New Roman", serif;
  font-style: italic;
  font-size: 16px;
  letter-spacing: 0.2px;
}
.pm-frac {
  display: inline-flex;
  flex-direction: column;
  vertical-align: middle;
  text-align: center;
  margin: 0 3px;
  line-height: 1.15;
  font-size: 0.92em;
}
.pm-frac .pm-num, .pm-frac .pm-den {
  display: block;
  padding: 0 4px;
}
.pm-frac .pm-num {
  border-bottom: 1.5px solid currentColor;
  padding-bottom: 1px;
}
.pm-frac .pm-den {
  padding-top: 1px;
}
.pm-math sub {
  font-size: 0.68em;
}
.pm-math .pm-op {
  font-style: normal;
  margin: 0 2px;
}

/* ------------------------------------------------------------------ */
/* Onboarding tutorial -- a guided, animated walkthrough of the core    */
/* ideas (what a double pendulum is, why it's chaotic, what the map     */
/* and its colors represent, what the stability/"Lyapunov" layer        */
/* shows). Deliberately its own overlay above everything else (incl.    */
/* the generic tab-overlay) since it can be opened any time via the     */
/* topbar, not just as a first-visit modal.                             */
/* ------------------------------------------------------------------ */
.tutorial-overlay {
  position: fixed;
  inset: 0;
  z-index: 5000;
  background: radial-gradient(circle at 50% 40%, rgba(0, 183, 255, 0.08), rgba(5, 6, 10, 0.86) 65%);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}
.tutorial-card {
  width: min(1280px, 94vw);
  max-height: 92vh;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 183, 255, 0.06), 0 0 60px var(--accent-glow);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: pm-tutorial-in 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
@keyframes pm-tutorial-in {
  0% { opacity: 0; transform: scale(0.94) translateY(10px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}
.tutorial-card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 18px 28px;
  border-bottom: 1px solid var(--border-soft);
}
#tutorial-step-label {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.tutorial-card-body {
  display: flex;
  gap: 40px;
  padding: 36px 44px;
  overflow-y: auto;
}
.tutorial-canvas-wrap {
  flex: none;
  width: 620px;
  max-width: 54%;
  border-radius: 14px;
  padding: 1px;
  background: linear-gradient(160deg, rgba(0, 183, 255, 0.3), rgba(255, 95, 95, 0.1) 60%, transparent);
  align-self: flex-start;
}
#tutorial-canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  background: radial-gradient(circle at 50% 38%, #0c0f18, #05060a 70%);
  border-radius: 11px;
}
.tutorial-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.tutorial-text h2 {
  margin: 0 0 18px 0;
  font-size: 26px;
  font-weight: 700;
  color: var(--accent-soft);
  letter-spacing: 0.01em;
}
.tutorial-text p {
  margin: 0;
  font-size: 16.5px;
  line-height: 1.8;
  color: var(--text);
}
.tutorial-text p b {
  color: var(--accent-soft);
  font-weight: 700;
}
.tutorial-text .tut-credit {
  display: inline-block;
  margin-top: 10px;
  font-size: 12px;
  font-weight: 400;
  color: var(--text-faint);
}
.tutorial-text .tut-formula-block {
  display: block;
  margin: 12px 0;
  padding: 12px 16px;
  background: rgba(0, 183, 255, 0.07);
  border: 1px solid rgba(0, 183, 255, 0.2);
  border-radius: 9px;
  font-size: 15px;
  line-height: 1.65;
  /* Short formulas (a single fraction, a short definition) read best
     unbroken -- but the long multi-term rational expressions in the
     Complex track (equations of motion, RK4 update rule) are never going
     to fit one screen line at a readable size, so this wraps naturally
     instead of hard-clipping mid-formula with no visible way to see the
     rest. overflow-x stays as a last-resort safety net, not the primary
     mechanism. */
  white-space: normal;
  overflow-x: auto;
  word-spacing: 0.05em;
}
.tutorial-text .pm-math {
  font-size: 1em;
}
.tutorial-card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 28px;
  border-top: 1px solid var(--border-soft);
  flex-wrap: wrap;
  gap: 12px;
}
#tutorial-dots {
  display: flex;
  gap: 9px;
}
.tutorial-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--border);
  border: none;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s ease, transform 0.15s ease;
}
.tutorial-dot:hover {
  background: var(--text-dim);
}
.tutorial-dot.pm-dot-active {
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow);
  transform: scale(1.25);
}
.tutorial-nav {
  display: flex;
  gap: 10px;
}
.tutorial-nav .tab-btn {
  font-size: 13px;
  padding: 9px 16px;
}
#tutorial-next, #tutorial-skip {
  background: var(--accent);
  color: #051018;
  font-weight: 700;
  border-color: var(--accent);
}
#tutorial-next:hover, #tutorial-skip:hover {
  color: #051018;
  box-shadow: 0 0 12px var(--accent-glow);
}
#tutorial-skip {
  background: var(--bg-raised);
  color: var(--text-dim);
  font-weight: 600;
}
#tutorial-skip:hover {
  color: var(--accent-soft);
  border-color: var(--accent);
}
#tutorial-prev:disabled, #tutorial-next:disabled {
  opacity: 0.3;
  cursor: not-allowed;
  pointer-events: none;
}
@media (max-width: 760px) {
  .tutorial-card-body { flex-direction: column; }
  .tutorial-canvas-wrap { width: 100%; max-width: 100%; }
}

/* Per-track sizing (item 13 of the V0.20 request): the amount of material
   genuinely differs per track, so the window does too -- Simple keeps the
   original size, Medium is a step up, Complex (the full physics writeup)
   gets the most room. The level-select screen itself is deliberately sized
   to match Medium -- "average of the three", per spec. */
.tutorial-card.tt-medium, .level-select-card {
  width: min(1440px, 96vw);
}
.tutorial-card.tt-medium .tutorial-canvas-wrap { width: 660px; }
.tutorial-card.tt-medium .tutorial-card-body { padding: 38px 48px; }

.tutorial-card.tt-complex {
  width: min(1640px, 98vw);
  max-height: 95vh;
}
.tutorial-card.tt-complex .tutorial-canvas-wrap { width: 700px; }
.tutorial-card.tt-complex .tutorial-card-body { padding: 40px 52px; }
.tutorial-card.tt-complex .tutorial-text h2 { font-size: 25px; }
.tutorial-card.tt-complex .tutorial-text p { font-size: 15.5px; line-height: 1.7; }

/* -------------------------------------------------------------- */
/* Level select -- three tracks (simple/medium/complex), always    */
/* shown to a first-time visitor before the tutorial itself, and   */
/* reachable again any time via "Как это устроено?" in the topbar. */
/* -------------------------------------------------------------- */
.level-select-card {
  max-height: 92vh;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 183, 255, 0.06), 0 0 60px var(--accent-glow);
  padding: 34px 44px 40px;
  overflow-y: auto;
  animation: pm-tutorial-in 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
.level-select-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 20px;
}
.level-select-header h2 {
  margin: 0;
  font-size: 28px;
  font-weight: 700;
  color: var(--accent-soft);
  letter-spacing: 0.01em;
}
.level-select-header-right {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-shrink: 0;
}
.level-select-lang {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
}
.level-select-lang #level-select-lang {
  font-size: 13px;
  font-weight: 700;
  padding: 6px 10px;
  border-color: var(--accent);
  box-shadow: 0 0 12px var(--accent-glow);
}
.level-select-sub {
  margin: 10px 0 16px 0;
  max-width: 760px;
  font-size: 15px;
  line-height: 1.7;
  color: var(--text-dim);
}
.epilepsy-warning {
  margin: 0 0 26px 0;
  max-width: 760px;
  padding: 14px 18px;
  border-radius: 8px;
  border: 1px solid rgba(255, 190, 60, 0.5);
  background: rgba(255, 190, 60, 0.12);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.55;
  color: #f0cd83;
}
.level-select-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.level-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  gap: 10px;
  padding: 24px 22px 22px;
  background: var(--bg-raised);
  border: 1px solid var(--border-soft);
  border-radius: 13px;
  cursor: pointer;
  transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
  font-family: inherit;
  color: var(--text);
}
.level-card:hover {
  transform: translateY(-4px);
  border-color: var(--accent);
  background: var(--bg-raised-hover, var(--bg-raised));
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.45), 0 0 22px var(--accent-glow);
}
.level-card:active {
  transform: translateY(-1px) scale(0.99);
}
.level-card-badge {
  display: none;
  position: absolute;
  top: 14px;
  right: 14px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-soft);
  background: rgba(0, 183, 255, 0.12);
  border: 1px solid rgba(0, 183, 255, 0.3);
  border-radius: 5px;
  padding: 3px 7px;
}
.level-card.pm-last-opened .level-card-badge {
  display: inline-block;
}
.level-card-icon {
  width: 56px;
  height: 56px;
  color: var(--accent-soft);
  transition: color 0.16s ease;
}
.level-card:hover .level-card-icon {
  color: var(--accent);
}
.level-card-icon svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}
.level-card h3 {
  margin: 4px 0 0 0;
  font-size: 19px;
  font-weight: 700;
  color: var(--text);
}
.level-card p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text-dim);
  flex: 1;
}
@media (max-width: 900px) {
  .level-select-grid { grid-template-columns: 1fr; }
}

.tutorial-header-left {
  display: flex;
  align-items: center;
  gap: 14px;
}
.tutorial-back-btn {
  background: transparent;
  border: 1px solid var(--border-soft);
  color: var(--text-dim);
  border-radius: 7px;
  width: 30px;
  height: 30px;
  font-size: 15px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.tutorial-back-btn:hover {
  color: var(--accent-soft);
  border-color: var(--accent);
  background: rgba(0, 183, 255, 0.08);
}

#panel-hint {
  font-size: 11px;
  color: var(--text-faint);
  line-height: 1.55;
  margin: auto 0 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border-soft);
}
#panel-credit {
  font-size: 10.5px;
  color: var(--text-faint);
  text-align: center;
  margin: 4px 0 0 0;
}
#panel-credit a {
  color: var(--text-dim);
  text-decoration: none;
  border-bottom: 1px dotted var(--text-faint);
  transition: color 0.15s ease, border-color 0.15s ease;
}
#panel-credit a:hover {
  color: var(--accent-soft);
  border-color: var(--accent);
}

/* Universe tag in the view-history rows. Sized to sit quietly next to the
   colour swatch -- it is an identifier, not a headline. */
.history-row-universe {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 1px 6px;
  border-radius: 999px;
  color: #9fd0ff;
  background: rgba(80, 150, 220, 0.16);
  border: 1px solid rgba(120, 180, 240, 0.28);
}

/* One bar per minute of the recording, coloured by that minute's Lyapunov
   exponent: cyan for regular, red for strongly chaotic. Sits directly under
   the verdict it expands on. */
.chaos-strip {
  display: flex;
  gap: 2px;
  margin: 2px 0 8px;
}
.chaos-strip .chaos-seg {
  flex: 1 1 0;
  /* The bar carries its own height rather than stretching to the strip's.
     The strip is border-box, so giving IT a height of 10px and then padding
     it left the content box at zero and collapsed every bar to nothing --
     which is how the strip disappeared while still reporting ten children. */
  height: 10px;
  border-radius: 2px;
  min-width: 4px;
  transition: transform 0.12s ease;
}
.chaos-strip .chaos-seg:hover {
  transform: scaleY(1.4);
}

/* The preset grid was sized for the side panel; the options menu is narrower,
   so the cards get their own column count and a fluid swatch there rather than
   overflowing or squeezing every label down to one character. */
/* The colour block carries an inline margin-top:2px from when it sat directly
   under a heading in the panel; in the menu it lands against the diagnostics
   link instead and needs real separation. */
#options-settings-panel #preset-row {
  margin-top: 14px !important;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}
#options-settings-panel .pm-preset-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
}
#options-settings-panel .pm-preset-card {
  padding: 4px;
  font-size: 11px;
}
#options-settings-panel .pm-preset-swatch {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
}
#options-settings-panel #stability-color-row .row-label {
  font-size: 11px;
}

/* Headline tiles that double as the chart's selector. The active one is marked
   by its own metric colour rather than a generic highlight, so the tile and the
   line it drew are recognisably the same thing. */
.admin-metric-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
  margin-bottom: 14px;
}
.admin-metric-tile {
  text-align: left;
  padding: 12px 14px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid rgba(255, 255, 255, 0.07);
  color: inherit;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.admin-metric-tile:hover { background: rgba(255, 255, 255, 0.06); }
.admin-metric-tile:active { transform: scale(0.985); }
.admin-metric-tile.active {
  border-color: var(--metric-colour);
  background: color-mix(in srgb, var(--metric-colour) 12%, transparent);
}
.admin-metric-tile .lbl {
  font-size: 12px;
  color: #8a93a8;
  margin-bottom: 4px;
}
.admin-metric-tile .val {
  font-size: 26px;
  font-weight: 700;
  line-height: 1.1;
  color: #e8edf7;
}
.admin-metric-tile.active .val { color: var(--metric-colour); }
.admin-metric-tile .sub {
  font-size: 11px;
  color: #5c6478;
  margin-top: 3px;
}
.admin-main-card { margin-bottom: 16px; }
.admin-main-card canvas {
  width: 100%;
  height: 320px;
  display: block;
}

/* The dashboard is a stack of full-width sections, not a masonry of cards:
   #admin-grid is a multi-column grid, so anything that does not say otherwise
   gets squeezed into one column -- which is what put the main chart in a
   360px slot beside the tiles instead of under them. */
.admin-metric-tiles,
.admin-main-card,
#admin-grid > .admin-totals,
.admin-breakdown-grid {
  grid-column: 1 / -1;
}
#admin-grid .admin-main-card canvas {
  height: 320px;
}
/* Axis labels are drawn at their own x, so the last one runs past the right
   edge unless the canvas keeps a little room for it. */
.admin-main-card {
  padding-right: 18px;
}

/* The dashboard's own selects need to fit their longest option ("Всё время" /
   "All time"), which the shared .pm-select width does not allow for. */
#admin-period, #admin-group {
  min-width: 118px;
}

.admin-heatmap-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
.admin-heatmap-canvas-wrap {
  position: relative;
  max-width: 700px;
  margin: 0 auto;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border-soft);
}
/* The grid itself is square (t1 and t2 cover the same -pi..pi range), and
   so is the per-universe map screenshot behind it -- a non-square canvas
   stretched the heat cells and the background photo by different amounts
   on each axis, which is exactly the "non-square pixel" distortion fixed
   elsewhere on the live map, just here from a fixed pixel height instead
   of a mismatched buffer. */
#admin-heatmap-canvas {
  width: 100%;
  aspect-ratio: 1 / 1;
  height: auto;
  display: block;
}
.admin-heatmap-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.55;
}
.admin-heatmap-stats {
  margin: 10px 0 0;
}

/* The mass diagram sits over the corner of each universe preview. Massless
   parts are thin and dashed, massive ones solid and bright -- and on hover the
   massive ones pulse, so "which parts weigh here" is answered by looking
   rather than by reading. */
.universe-shot {
  position: relative;
  display: block;
}
.universe-diagram {
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 68px;
  height: 68px;
  background: rgba(6, 8, 14, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 10px;
  backdrop-filter: blur(2px);
}
.universe-diagram .ud-rod {
  stroke-linecap: round;
  fill: none;
}
.universe-diagram .ud-rod.ud-light {
  stroke: rgba(190, 205, 230, 0.34);
  stroke-width: 2.5;
  stroke-dasharray: 4 4;
}
.universe-diagram .ud-rod.ud-heavy {
  stroke: #7fd7ff;
  stroke-width: 6;
}
.universe-diagram .ud-bob.ud-light {
  fill: none;
  stroke: rgba(190, 205, 230, 0.34);
  stroke-width: 1.6;
  stroke-dasharray: 3 3;
}
.universe-diagram .ud-bob.ud-heavy {
  fill: #7fd7ff;
  stroke: rgba(10, 12, 18, 0.9);
  stroke-width: 1.5;
}
.universe-diagram .ud-pivot {
  fill: rgba(232, 237, 247, 0.85);
}

/* Only the parts that carry mass move, and only while the card is hovered or
   focused -- an animation that always runs would compete with the preview
   image behind it and with the six other cards. */
@keyframes ud-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}
.universe-card:hover .universe-diagram .ud-heavy,
.universe-card:focus-visible .universe-diagram .ud-heavy {
  animation: ud-pulse 1.05s ease-in-out infinite;
}
/* Staggered so the parts read as separate pieces rather than one blinking
   shape -- which is what makes it obvious that the elbow and the second rod
   are two different things. */
.universe-card:hover .universe-diagram [data-part="elbow"] { animation-delay: 0.13s; }
.universe-card:hover .universe-diagram [data-part="rod2"]  { animation-delay: 0.26s; }
.universe-card:hover .universe-diagram [data-part="tip"]   { animation-delay: 0.39s; }

.universe-card:hover .universe-diagram,
.universe-card:focus-visible .universe-diagram {
  border-color: rgba(127, 215, 255, 0.5);
}

@media (prefers-reduced-motion: reduce) {
  .universe-card:hover .universe-diagram .ud-heavy,
  .universe-card:focus-visible .universe-diagram .ud-heavy {
    animation: none;
  }
  /* Without motion the highlight has to come from somewhere, so the massive
     parts simply grow brighter instead of pulsing. */
  .universe-card:hover .universe-diagram .ud-heavy {
    filter: brightness(1.5);
  }
}

/* The chaos row's value is a phrase, not a number, so it gets the same
   treatment as the stopwatch row: full width and stacked, rather than fighting
   its own label for a shared cell. Without this its label was ellipsised down
   to a single character. */
#stat-chaos-row {
  grid-column: 1 / -1;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
#stat-chaos-row .stat-val {
  text-align: left;
  white-space: normal;
}
/* The strip belongs to the row above it, not to a grid cell of its own. */
#stats-block .chaos-strip {
  grid-column: 1 / -1;
  margin: 0;
  padding: 0 14px 10px;
  background: var(--bg-raised);
}

/* The number a slider is changing brightens for a moment as it changes, so the
   eye is drawn to the readout rather than hunting for which of a dozen values
   just moved. Short and low-contrast on purpose: this is a confirmation, not
   an animation to watch. */
@keyframes pm-value-bump {
  0%   { color: #eaf7ff; text-shadow: 0 0 9px rgba(120, 210, 255, 0.75); }
  100% { color: var(--accent-soft); text-shadow: none; }
}
.row-value.pm-value-live,
.charts-window-label .pm-value-live {
  animation: pm-value-bump 0.42s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  input[type="range"],
  input[type="range"]::-webkit-slider-thumb,
  input[type="range"]::-moz-range-thumb { transition: none; }
  .row-value.pm-value-live { animation: none; }
}

/* The way back to the default pendulum view. Sits in the canvas's own corner
   rather than in the settings panel, because that is where the view was
   changed and where someone will look to change it back. */
#pendulum-view-reset {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 3;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(10, 12, 18, 0.72);
  color: #cbd5e8;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease, transform 0.1s ease;
}
#pendulum-view-reset:hover {
  background: rgba(20, 26, 38, 0.9);
  color: #7fd7ff;
}
#pendulum-view-reset:active { transform: scale(0.92); }

/* Size picker and download button share a line: they are one action. */
.export-row {
  display: flex;
  gap: 8px;
  align-items: center;
}
.export-row .pm-select { flex: 1 1 auto; }
.export-row .tab-btn { flex: 0 0 auto; white-space: nowrap; }

/* Record standings for the open pendulum. One chip per board it places on,
   first place picked out -- a row of them says more than a single line of
   comma-separated names ever did. */
.record-chip {
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.09);
  color: #cbd5e8;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.14s ease, border-color 0.14s ease, transform 0.1s ease;
}
.record-chip:hover {
  background: rgba(255, 255, 255, 0.09);
  border-color: rgba(127, 215, 255, 0.4);
}
.record-chip:active { transform: scale(0.96); }
.record-chip-first {
  color: #ffd76a;
  border-color: rgba(255, 215, 106, 0.45);
  background: rgba(255, 215, 106, 0.1);
  font-weight: 600;
}

/* The calm board's tiebreak, sitting between the value and the metadata.
   Deliberately quieter than the value it qualifies -- it explains the order,
   it is not the thing being ranked. */
.pmb-rec-detail {
  font: 500 10.5px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-dim, #8b93a7);
  padding: 2px 5px;
  border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.09));
  border-radius: 5px;
  white-space: nowrap;
}

/* The line under the register button. Quiet, but not so quiet it reads as
   decoration -- it is the only place the agreement is actually presented. */
.auth-form-legal {
  margin-top: 14px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim, #8b93a7);
}
.auth-form-legal a { color: var(--accent-soft, #7fd7ff); }
.auth-form-warn {
  margin-top: 8px;
  padding: 8px 10px;
  border: 1px solid rgba(255, 176, 84, 0.35);
  border-radius: 7px;
  background: rgba(255, 176, 84, 0.07);
  color: #ffcf8f;
}
/* The one-time recovery code screen: a code the user must copy before they
   can leave, since it is never shown again after this. Large, monospace,
   and user-select:all so a single click selects the whole thing -- typing
   it out by hand from a squint is not the intended path. */
.auth-recovery-code {
  font-family: var(--font-mono);
  font-size: 18px;
  letter-spacing: 1px;
  text-align: center;
  color: var(--accent-soft, #7fd7ff);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 10px;
  user-select: all;
  word-break: break-all;
}

/* "+3 nearby": this row stands for a cluster. Deliberately dimmer than the
   universe badge next to it -- it qualifies the entry rather than identifying
   it. */
.pmb-rec-nearby {
  font-size: 10.5px;
  color: var(--text-faint, #6b7488);
  border: 1px dashed var(--border-soft, rgba(255, 255, 255, 0.14));
  border-radius: 5px;
  padding: 1px 5px;
  white-space: nowrap;
}

/* Naming a find. Sits in the same overlay as the sign-in form and follows its
   shape deliberately -- both are "a short thing to fill in", and inventing a
   second visual language for the second one would only make the site look
   assembled from parts. */
.pm-name-form { display: flex; flex-direction: column; gap: 13px; min-width: 320px; }
.pm-name-where {
  font: 400 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-faint, #6b7488);
}
.pm-name-field { display: flex; flex-direction: column; gap: 5px; }
.pm-name-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 12px;
  color: var(--text-dim, #8b93a7);
}
.pm-name-count {
  font: 400 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-faint, #6b7488);
}
.pm-name-field input,
.pm-name-field textarea {
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.12));
  border-radius: 7px;
  padding: 8px 10px;
  color: var(--text, #e8edf7);
  font: 400 14px/1.45 inherit;
  width: 100%;
  box-sizing: border-box;
}
.pm-name-field textarea { min-height: 76px; resize: vertical; }
.pm-name-field input:focus,
.pm-name-field textarea:focus {
  outline: none;
  border-color: rgba(127, 215, 255, 0.55);
}
.pm-name-error { font-size: 12.5px; line-height: 1.45; color: #ff9b9b; min-height: 1em; }
.pm-name-actions { display: flex; align-items: center; gap: 10px; margin-top: 2px; }
.pm-name-actions .auth-form-switch { margin-right: auto; }
/* Disabled says "not yet", not "broken": dimmed and inert, but still clearly
   the button you are heading for. */
.pm-name-actions .tab-btn.is-disabled { opacity: 0.42; cursor: not-allowed; }

/* ---- report a problem ---------------------------------------------------
   Its own overlay rather than the shared tab panel: this has to work on the
   bestiary and records pages too, which never load app.js, and a form for
   reporting that the site is broken should depend on as little of the site as
   possible. */
.pm-report-fab {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 4000;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.14));
  background: rgba(18, 20, 28, 0.9);
  color: #e8edf7;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 0.15s ease, transform 0.12s ease, border-color 0.15s ease;
}
.pm-report-fab:hover {
  opacity: 1;
  transform: translateY(-1px);
  border-color: rgba(127, 215, 255, 0.45);
}
.pm-report-fab:active { transform: scale(0.94); }

.pm-report-overlay {
  position: fixed;
  inset: 0;
  z-index: 4100;
  background: rgba(6, 7, 11, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  overflow: auto;
}
.pm-report-panel {
  background: var(--bg-panel, #12141c);
  border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.12));
  border-radius: 12px;
  padding: 20px 22px;
  width: min(560px, 100%);
  max-height: 90vh;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.pm-report-head { display: flex; align-items: center; gap: 10px; }
.pm-report-head h2 { margin: 0; font-size: 18px; flex: 1; }
.pm-report-lead { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-dim, #8b93a7); }

.pm-report-kinds { display: flex; gap: 7px; flex-wrap: wrap; }
.pm-report-kind {
  font-size: 12.5px;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.12));
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim, #8b93a7);
  cursor: pointer;
}
.pm-report-kind.is-on {
  color: #7fd7ff;
  border-color: rgba(127, 215, 255, 0.5);
  background: rgba(127, 215, 255, 0.1);
}

.pm-report-body,
.pm-report-contact {
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.12));
  border-radius: 8px;
  padding: 9px 11px;
  color: var(--text, #e8edf7);
  font: 400 14px/1.5 inherit;
  width: 100%;
  box-sizing: border-box;
}
.pm-report-body { min-height: 130px; resize: vertical; }
.pm-report-body:focus,
.pm-report-contact:focus { outline: none; border-color: rgba(127, 215, 255, 0.55); }

.pm-report-ctx summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--accent-soft, #7fd7ff);
}
.pm-report-ctx pre {
  margin: 8px 0 0;
  padding: 10px;
  max-height: 210px;
  overflow: auto;
  background: rgba(0, 0, 0, 0.32);
  border-radius: 7px;
  font: 400 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-dim, #8b93a7);
  white-space: pre-wrap;
  word-break: break-word;
}
.pm-report-attach {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--text-dim, #8b93a7);
  cursor: pointer;
}
.pm-report-error { font-size: 12.5px; color: #ff9b9b; min-height: 1em; line-height: 1.45; }
.pm-report-actions { display: flex; align-items: center; gap: 10px; }
.pm-report-actions .auth-form-switch { margin-right: auto; }
.pm-report-thanks { margin: 0; font-size: 18px; color: #7ee0a8; }

/* The report button in the top bar. Same weight as its neighbours on purpose:
   one that shouts is annoying, one that hides collects nothing, and the row
   people already read is neither. The bug sits slightly proud of the text so
   it reads as an icon rather than a character. */
.pm-report-btn { white-space: nowrap; }
@media (max-width: 900px) {
  /* Narrow bars are already crowded; the emoji alone still says what it is. */
  .pm-report-btn { font-size: 0; padding-left: 9px; padding-right: 9px; }
  .pm-report-btn::before { content: "🐞"; font-size: 14px; }
}
