:root {
  --bg:           #0d1117;
  --bg-soft:      #11161c;
  --card:         #161b22;
  --card-soft:    #1b2129;
  --border:       #2a3038;
  --border-soft:  #20252b;
  --text:         #e6edf3;
  --text2:        #8b949e;
  --text3:        #6b7280;
  --accent:       #58a6ff;
  --accent-soft:  rgba(88, 166, 255, 0.15);
  --accent-hover: #79c0ff;
  --green:        #3fb950;
  --green-soft:   rgba(63, 185, 80, 0.15);
  --red:          #f85149;
  --red-soft:     rgba(248, 81, 73, 0.15);
  --orange:       #d29922;
  --radius:       6px;
  --radius-sm:    4px;
  --topbar-h:     52px;
  --upload-h:     0px;
  --shadow-sm:    0 1px 0 rgba(0, 0, 0, 0.2);
  --focus-ring:   0 0 0 2px rgba(88, 166, 255, 0.35);
  --gutter:       1px;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg:           #f6f8fa;
    --bg-soft:      #eef1f5;
    --card:         #ffffff;
    --card-soft:    #f6f8fa;
    --border:       #d0d7de;
    --border-soft:  #e6ebf0;
    --text:         #1f2328;
    --text2:        #656d76;
    --text3:        #8c959f;
    --accent:       #0969da;
    --accent-soft:  rgba(9, 105, 218, 0.12);
    --accent-hover: #0550ae;
    --green:        #1a7f37;
    --green-soft:   rgba(26, 127, 55, 0.12);
    --red:          #cf222e;
    --red-soft:     rgba(207, 34, 46, 0.12);
    --orange:       #9a6700;
    --shadow-sm:    0 1px 0 rgba(0, 0, 0, 0.04);
    --focus-ring:   0 0 0 2px rgba(9, 105, 218, 0.35);
  }
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body { height: 100%; }

body {
  /* Roboto Mono everywhere — the design system has a single typeface
     across nav, body, code, data, labels, and buttons. Form controls
     and pre/code blocks below `font-family: inherit` from this root. */
  font-family: var(--font-mono);
  background: var(--bg);
  color: var(--text);
  line-height: 1.4;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.hidden { display: none !important; }

/* ---- App shell ---- */
#app.ide-only {
  display: flex;
  flex-direction: column;
  height: 100vh;
  width: 100%;
}

/* ---- Top bar ---- */
.ide-topbar {
  height: var(--topbar-h);
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0 1rem;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-width: fit-content;
  padding-right: 0.6rem;
  border-right: 1px solid var(--border-soft);
}
.brand::before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.brand h1 {
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.brand-sub {
  color: var(--text2);
  font-size: 0.78rem;
  font-weight: 500;
}

.toolbar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
}
.toolbar-label {
  color: var(--text2);
  font-size: 0.78rem;
  margin-right: 0.1rem;
  font-weight: 500;
}
.sep {
  width: 1px;
  height: 22px;
  background: var(--border-soft);
  margin: 0 0.35rem;
}

/* ---- Inputs ---- */
input[type="text"], input[type="password"], input[type="number"], select {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 0.32rem 0.55rem;
  font-size: 0.83rem;
  outline: none;
  font-family: inherit;
  transition: border-color 0.12s, box-shadow 0.12s;
}
input:focus, select:focus {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
input::placeholder { color: var(--text3); }

/* Wide enough for "192.168.178.200" and "127.0.0.1:8766" without
   placeholder truncation. */
#ide-quick-ip { width: 20ch; }
#ide-auth-token { width: 11ch; }
/* Long enough to show "127.0.0.1:8766 — vsim-1.0.0 — A" and the
   typical "192.168.178.200 — v1.2.0-b — A" without truncation. */
#ide-device-select { min-width: 18ch; max-width: 40ch; }

/* ---- Device picker ----
   Custom dropdown replacing the native <select>. Trigger button is
   styled to match adjacent text inputs (same border, padding, font);
   menu is an absolutely-positioned <ul> below it. A hidden <select>
   sits in the same .device-picker container as the source of truth
   for .value / 'change' events — see the trigger wiring in ide.js. */
.device-picker {
  position: relative;
  display: inline-block;
  font-family: var(--font-mono);
}
.device-picker__trigger {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.32rem 0.55rem;
  min-width: 22ch;
  max-width: 40ch;
  font-size: 0.83rem;
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.device-picker__trigger:hover  { border-color: var(--border-strong, var(--border)); }
.device-picker__trigger:focus,
.device-picker__trigger[aria-expanded="true"] {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
  outline: none;
}
.device-picker__led {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text3);
  transition: background 0.12s, box-shadow 0.12s;
}
.device-picker__led[data-state="ok"]  {
  background: var(--status-live);
  box-shadow: 0 0 6px var(--status-live);
  animation: pulse-live 2.4s ease-in-out infinite;
}
.device-picker__led[data-state="err"] {
  background: var(--status-fault);
  box-shadow: 0 0 6px var(--status-fault);
}
.device-picker__label {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: left;
}
.device-picker__caret {
  flex: 0 0 auto;
  font-size: 18px !important;
  color: var(--text2);
  transition: transform 0.12s;
}
.device-picker__trigger[aria-expanded="true"] .device-picker__caret {
  transform: rotate(180deg);
}

.device-picker__menu {
  position: absolute;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  max-height: 240px;
  overflow-y: auto;
  z-index: 60;
}
.device-picker__menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  font-size: 0.83rem;
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
}
.device-picker__menu-item:hover,
.device-picker__menu-item[aria-selected="true"] {
  background: var(--bg-soft);
}
.device-picker__menu-empty {
  padding: 8px 10px;
  font-size: 0.83rem;
  color: var(--text3);
  font-style: italic;
}

/* ---- Buttons ---- */
button {
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.35rem 0.85rem;
  font-size: 0.83rem;
  cursor: pointer;
  font-weight: 500;
  font-family: inherit;
  transition: background 0.12s, border-color 0.12s, color 0.12s, box-shadow 0.12s;
}
button:hover { background: var(--card-soft); border-color: var(--text2); }
button:focus-visible { box-shadow: var(--focus-ring); border-color: var(--accent); }
button:active { transform: translateY(1px); }
button:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }

.btn-primary {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

.btn-ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text2);
}
.btn-ghost:hover { color: var(--text); background: var(--card-soft); }

.btn-mini {
  padding: 0.18rem 0.55rem;
  font-size: 0.74rem;
  border-radius: 3px;
}

/* Aria-pressed toggles + custom .is-active treat-the-same. */
button[aria-pressed="true"], button.is-active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-hover);
}

/* ---- Upload progress strip — lives at the bottom of the build-log
   pane (web/index.html). Hidden until ide.js drops the .hidden class
   on it. Borrows the pane-header's chrome (top border + subdued bg)
   so it reads as part of the same surface. */
/* Build / upload progress block — sits at the bottom of the build-log
   pane. Label stacks ABOVE the bar so they can never overlap, and the
   bar always has the full pane width. Long stage labels truncate with
   an ellipsis instead of wrapping or pushing the bar around. */
.upload-status {
  flex: 0 0 auto;
  padding: 8px 12px;
  background: var(--card);
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.upload-status #ide-upload-msg {
  color: var(--text2);
  font-size: 0.74rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.upload-status .progress-bar { width: 100%; margin: 0; }

.progress-bar {
  height: 4px;
  background: var(--border-soft);
  border-radius: 2px;
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent), var(--accent-hover));
  border-radius: 2px;
  width: 0%;
  transition: width 0.2s;
}

/* ---- Main layout ----
   Track sizes are driven by CSS custom properties (--telemetry-w,
   --editor-h, --log-w) so the JS drag handlers can update them in
   place. The 6px tracks between panes are the resizer DOM elements
   themselves. */
.ide-layout {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 6px var(--telemetry-w, 35%);
  /* Single explicit row pinned to the container's height. Without
     this, the implicit row defaults to grid-auto-rows: auto and
     resolves to its tallest child's intrinsic size — when the
     panes' content has a large min-height stack (pane-header +
     plots-min + cmd-strip + telemetry-console-min, etc.), the row
     would grow past the flex-1 allocation and push the panes'
     bottoms under the viewport. With overflow: hidden on the body,
     those clipped bottoms are why the runtime / telemetry log
     scroll handles + last lines disappeared on shorter screens. */
  grid-template-rows: minmax(0, 1fr);
  grid-gap: 0;
  background: var(--border);
  min-height: 0;
}
/* When telemetry is hidden, the column collapses entirely (resizer
   too) and main-area takes the full width. */
.ide-layout.telemetry-hidden {
  grid-template-columns: minmax(0, 1fr) 0 0;
}
.ide-layout.telemetry-hidden > .resizer.main-telemetry,
.ide-layout.telemetry-hidden > .telemetry-pane { display: none; }

.pane {
  background: var(--bg);
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

.main-area {
  grid-column: 1; grid-row: 1;
  display: grid;
  grid-template-rows: var(--editor-h, 60%) 6px minmax(0, 1fr);
  grid-gap: 0;
  background: var(--border);
  min-height: 0;
  min-width: 0;
}
.editor-pane { grid-row: 1; }
.bottom-area {
  grid-row: 3;
  display: grid;
  /* Track 3 is `minmax(420px, 1fr)` not `minmax(0, 1fr)` so the
     console column can't shrink below its content min and the pane
     overflow under the telemetry sidebar. The telemetry resizer's
     data-other-min-px clamps the parent to leave enough room. */
  grid-template-columns: var(--log-w, 50%) 6px minmax(420px, 1fr);
  grid-gap: 0;
  background: var(--border);
  min-width: 0;
}
/* Bottom-area panes get their own mins so they stay usable when the
   telemetry pane is dragged wide and the main column shrinks.
   Build log: room for Build + Build & Upload + clear icon now that
              those buttons live in its header.
   Console:   title + "disconnected" status pill + 3 icon controls.
              380 was the empirical-minimum that fits at idealised
              metrics, but font rendering varies — 420 leaves headroom
              so the clear button never tucks under the sidebar edge. */
.log-pane     { grid-column: 1; min-width: 360px; }
.console-pane { grid-column: 3; min-width: 420px; }
/* Keep pane titles on one line — wrapping pushes button rows down
   and starts cropping. */
.pane-title { white-space: nowrap; }

.telemetry-pane {
  grid-column: 3; grid-row: 1;
  /* The header packs status + info + window selector + Pause + Clear,
     and the cmd-strip below packs CMD label + name + args + send.
     Below ~420 px the trailing buttons get clipped off the right
     edge. Hard min keeps the chrome readable; if the user wants
     more room for code they can hide the pane outright via the
     toolbar toggle. */
  min-width: 420px;
  /* Inline-size container so #ide-telemetry-stats can drop low-priority
     segments based on the pane's actual width (not the viewport). */
  container-type: inline-size;
  container-name: telpane;
}

/* Telemetry pane header: don't wrap. The stats element fills the
   available middle space (flex-grow: 1) but right-aligns its text
   so the action buttons (#ide-add-plot / download) sit immediately
   next to the stats, not pinned to the far right with a gap. The
   default `.pane-actions { margin-left: auto }` in the global rule
   would re-introduce that gap, so we zero it out here. */
.telemetry-pane > .pane-header { flex-wrap: nowrap; min-width: 0; }
.telemetry-pane > .pane-header > .pane-actions { margin-left: 0; }
.telemetry-pane > .pane-header #ide-telemetry-stats {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  /* The pane-header inherits `text-transform: uppercase` and
     `letter-spacing: 0.20em` from conduit-chrome.css for the title
     row treatment. We don't want either inside the stats readout —
     uppercase turns "12s" into "12S" and the wide letter-spacing
     pulls units away from their numbers. */
  text-transform: none;
  letter-spacing: 0;
}
.telemetry-pane > .pane-header #ide-telemetry-stats > span { white-space: nowrap; }
.telemetry-pane > .pane-header #ide-telemetry-stats .stat-sep {
  margin: 0 8px;
  color: var(--text3);
}

/* Drop priority as the telemetry pane narrows: points first, then
   channels, then bytes. Time stays. Each segment is hidden together
   with the .stat-sep that immediately follows it, so dropping the
   leftmost segment removes the dangling " · " too. */
@container telpane (max-width: 540px) {
  #ide-telemetry-stats .stat-points,
  #ide-telemetry-stats .stat-points + .stat-sep { display: none; }
}
@container telpane (max-width: 460px) {
  #ide-telemetry-stats .stat-channels,
  #ide-telemetry-stats .stat-channels + .stat-sep { display: none; }
}
@container telpane (max-width: 380px) {
  #ide-telemetry-stats .stat-bytes,
  #ide-telemetry-stats .stat-bytes + .stat-sep { display: none; }
}

/* ---- Resizers (drag handles between panes) ---- */
.resizer {
  background: var(--border);
  position: relative;
  z-index: 5;
  transition: background-color 0.15s;
}
.resizer:hover, .resizer.dragging { background: var(--accent, #58a6ff); }
.resizer-v { cursor: col-resize; }
.resizer-h { cursor: row-resize; }
/* Position each resizer in its parent grid. */
.ide-layout > .resizer.main-telemetry { grid-column: 2; grid-row: 1; }
.main-area  > .resizer-h               { grid-row: 2; }
.bottom-area > .resizer-v              { grid-column: 2; }
/* Bigger grab hitbox than the visual sliver. */
.resizer-v::after, .resizer-h::after {
  content: '';
  position: absolute;
  top: -3px; bottom: -3px; left: -3px; right: -3px;
}
/* Block text selection while a drag is in progress (set on body via JS). */
body.resizing, body.resizing * {
  user-select: none !important;
  cursor: inherit !important;
}
body.resizing-v { cursor: col-resize; }
body.resizing-h { cursor: row-resize; }

/* ---- Pane header ---- */
.pane-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;             /* actions wrap below title when narrow */
  gap: 6px 10px;
  /* Whole-pixel padding so the gap between the right edge of the
     pane-actions group and the pane edge doesn't sub-pixel-jitter as
     the pane resizes. 0.75rem ≈ 12 px but rounds inconsistently when
     the pane lands on fractional widths during a drag. */
  padding: 6px 12px;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  min-height: 36px;
}
.pane-title {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text2);
  font-weight: 700;
}
.pane-hint {
  color: var(--text2);
  font-size: 0.74rem;
}
.pane-hint-info {
  color: var(--text3);
  font-variant-numeric: tabular-nums;
  font-family: var(--font-mono);
}
.pane-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Status dot — left of the pane-actions group */
.status-dot {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.74rem;
  color: var(--text2);
  font-weight: 500;
}
.status-dot::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--text3);
  box-shadow: 0 0 0 2px transparent;
  transition: background 0.2s, box-shadow 0.2s;
}
.status-dot[data-state="ok"]::before {
  background: var(--green);
  box-shadow: 0 0 0 3px var(--green-soft);
}
.status-dot[data-state="err"]::before {
  background: var(--red);
  box-shadow: 0 0 0 3px var(--red-soft);
}

/* ---- Editor (Monaco) ---- */
#ide-source.ide-monaco {
  flex: 1 1 auto;
  min-height: 0;
  background: var(--bg);
  font-family: var(--font-mono);
  color: var(--text2);
  overflow: hidden;
}
#ide-source.ide-monaco .monaco-editor { padding: 0; }

/* ---- Build log + runtime console ---- */
#ide-log, #ide-console {
  flex: 1 1 auto;
  background: var(--bg);
  color: var(--text);
  padding: 0.55rem 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.77rem;
  line-height: 1.45;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
  min-height: 0;
}
#ide-log { color: var(--text2); }

/* Runtime console terminal — uses Conduit surface tokens so it
   inverts cleanly with the theme toggle. */
.terminal {
  background: var(--bg);
  color: var(--ink-dim);
}

code {
  background: var(--card);
  padding: 0.05rem 0.3rem;
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 0.85em;
}

/* ---- Telemetry pane internals ---- */
/* Container for stacked plot widgets — each Chart instance appends a
   .plot subtree here. Scrollable so adding many plots doesn't blow
   the pane out of its column.
   min-height keeps the chart from being squeezed to zero by the
   cmd-strip + telemetry-console + headers below it (each have their
   own fixed minimums); the user can still reclaim space by hiding
   the telemetry pane outright via the topbar toggle. */
.plots-container {
  flex: 1 1 auto;
  min-height: 220px;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background: var(--bg-soft);
}
.plot {
  flex: 1 1 220px;
  /* Ensures the trace area stays legible when several plots stack —
     below this we let .plots-container's overflow-y kick in and add a
     scrollbar instead of squeezing every plot into invisibility. */
  min-height: 220px;
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid var(--border);
}
.plot:last-child { border-bottom: none; }
.plot.dragging {
  opacity: 0.55;
  /* Slight scale-down to acknowledge the drag is active even before
     the drop target has been chosen. */
  outline: 1px dashed var(--accent, #58a6ff);
}
/* Position indicator on the candidate drop sibling. The container
   relies on these classes set/cleared from the dragover handler. */
.plot.drop-above { box-shadow: inset 0 2px 0 var(--accent, #58a6ff); }
.plot.drop-below { box-shadow: inset 0 -2px 0 var(--accent, #58a6ff); }

/* Empty state when the user has deleted every plot. */
.plots-empty {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text3);
  font-size: 0.78rem;
  font-style: italic;
  padding: 1rem;
  text-align: center;
}

.plot-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.5rem;
  background: var(--card);
  border-bottom: 1px solid var(--border);
  /* Anchor for the absolutely-positioned .plot-channel-menu popover. */
  position: relative;
}

/* Channel picker popover — opened from the .plot-picker-btn icon in
   the plot header. Lets the user toggle individual series visibility
   (or all-on / all-off) without having to fish through the wrapped
   uPlot legend when the channel set is large. Hidden by default;
   chart.js flips the [hidden] attribute. */
.plot-channel-menu {
  position: absolute;
  right: 0.5rem;
  top: calc(100% + 4px);
  z-index: 50;
  min-width: 180px;
  max-width: 280px;
  padding: 4px 0;
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  font-size: 0.78rem;
  color: var(--text);
}
.plot-channel-menu__row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
}
.plot-channel-menu__row:hover { background: var(--bg-soft); }
.plot-channel-menu__all { font-weight: 500; }
.plot-channel-menu__sep {
  height: 1px;
  background: var(--border-soft);
  margin: 2px 0;
}
.plot-channel-menu__list {
  /* Sized to show exactly 3 rows; the 4th onwards scrolls. Each row
     is `4px + 0.78rem * 1.25 + 4px` ≈ 24-26 px depending on font
     loading, so 78 px gives a comfortable 3-row window. Bump if you
     change row padding/line-height. */
  max-height: 78px;
  overflow-y: auto;
}
.plot-channel-menu__marker {
  width: 10px;
  height: 2px;
  flex: 0 0 auto;
  border-radius: 1px;
}
.plot-channel-menu__label {
  font-family: var(--font-mono);
}
.plot-channel-menu__empty {
  padding: 8px 10px;
  color: var(--text3);
  font-style: italic;
}
/* Drag handle — only this part of the plot is grabbable; clicking
   buttons / typing in the title input still works as expected. */
.plot-drag {
  display: inline-flex;
  align-items: center;
  color: var(--text3);
  cursor: grab;
  user-select: none;
  padding: 0 0.1rem;
}
.plot-drag:hover { color: var(--text); }
.plot-drag:active { cursor: grabbing; }
body.plot-dragging, body.plot-dragging * { cursor: grabbing !important; }
.plot-title {
  flex: 1 1 auto;
  min-width: 4em;
  background: transparent;
  border: 1px solid transparent;
  padding: 0.15rem 0.35rem;
  font-size: 0.78rem;
  color: var(--text);
  border-radius: 3px;
}
.plot-title:focus, .plot-title:hover {
  border-color: var(--border);
  background: var(--bg);
  outline: none;
}
/* .plot-info was removed — recording-volume info is now shown once at
   the telemetry pane header (#ide-telemetry-stats), since the data is
   centrally owned by the dataStore and not by individual plots. */
.chart-wrap {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  background: var(--bg-soft);
  box-shadow: inset 0 1px 0 var(--border-soft);
  /* uPlot needs a positively-sized container; the flex chain provides it. */
  overflow: hidden;
}

.chart-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text3);
  font-size: 0.78rem;
  font-style: italic;
  pointer-events: none;
}

/* uPlot dark-theme overrides — its default CSS is light-themed. */
.uplot {
  font-family: var(--font-mono);
  color: var(--text);
}
.u-title { color: var(--text); }

/* Legend in normal flow below the chart — uPlot doesn't include the
   legend in its `height` arg (only the canvas + axes), so .chart-wrap
   has to leave room for it. CHROME_H in chart.js is sized to match
   this padding; bump them together if you change either. */
.u-legend {
  background: var(--card-soft);
  border-top: 1px solid var(--border-soft);
  color: var(--text);
  font-size: 0.7rem;
  font-family: var(--font-mono);
  padding: 1px 8px;
  text-align: left;
}
.u-legend table { border-collapse: collapse; margin: 0 auto; }
.u-legend th, .u-legend td { padding: 0 6px; line-height: 1.25; }
.u-legend tr { display: inline-table; vertical-align: middle; }
.u-legend .u-marker { width: 10px; height: 2px; }
.u-legend .u-series th { color: var(--text2); font-weight: 500; text-align: left; }
.u-legend .u-series.u-off > * { opacity: 0.4; }
.u-legend .u-value {
  font-variant-numeric: tabular-nums;
  min-width: 6ch;
  text-align: right;
  color: var(--text);
}
.u-cursor-pt { background: var(--accent); border: none; }
.u-axis { color: var(--text2); }
.u-select { background: rgba(88, 166, 255, 0.12); }

/* Command strip — two visually distinct groups + status log. */
.cmd-strip {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem;
  border-top: 1px solid var(--border);
  background: var(--card);
  font-size: 0.78rem;
}

.cmd-group {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25rem 0.45rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  background: var(--card-soft);
}

.cmd-strip .cmd-label {
  color: var(--text2);
  font-weight: 700;
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding-right: 0.15rem;
}
.cmd-strip .cmd-num {
  width: 4em;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 2px 4px;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.cmd-strip .cmd-text {
  width: 7em;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 2px 4px;
  font-family: var(--font-mono);
}
.cmd-strip .cmd-text-wide { width: 11em; }

.cmd-strip .cmd-select {
  background: var(--bg);
  color: var(--text2);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 4px 1px 5px;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
}

/* Segmented-button cluster: removes the gap between adjacent buttons,
   merges borders, rounds outer corners only. */
.btn-cluster {
  display: inline-flex;
}
.btn-cluster .btn-mini {
  border-radius: 0;
  margin-left: -1px;
  position: relative;
}
.btn-cluster .btn-mini:first-child {
  border-top-left-radius: 3px;
  border-bottom-left-radius: 3px;
  margin-left: 0;
}
.btn-cluster .btn-mini:last-child {
  border-top-right-radius: 3px;
  border-bottom-right-radius: 3px;
}
.btn-cluster .btn-mini:hover,
.btn-cluster .btn-mini:focus-visible { z-index: 1; }

/* The inline cmd-log strip was removed — command outcomes go
   straight to the telemetry console at the bottom of the same pane. */

/* ---- Telemetry console (inset at the bottom of the telemetry pane) ----
   Inside .telemetry-pane, NOT a top-level pane — sits below the cmd
   strip with a fixed minimum height and an internal scroll. The header
   shares the .pane-header look so it reads as part of the same family
   as Build log / Runtime console headers. */
.telemetry-console {
  /* Fixed height range (not %-based) so the plots-container above
     gets a deterministic share of the pane. Compact: header (~30px) +
     two-line body (~36px) — events scroll inside. */
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  min-height: 64px;
  max-height: 90px;
  border-top: 1px solid var(--border);
  background: var(--bg);
  overflow: hidden;
}
.telemetry-console .pane-header {
  /* Slightly more compact than the main pane headers so this inset
     doesn't compete visually with the telemetry pane's own header. */
  min-height: 30px;
  padding: 0.25rem 0.6rem;
}
.netcon-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  padding: 0.4rem 0.6rem;
  background: var(--bg-soft);
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.netcon-body:empty::before {
  content: 'no events yet — command results and download summaries land here';
  color: var(--text3);
  font-style: italic;
}
.netcon-body::-webkit-scrollbar       { width: 6px; }
.netcon-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.netcon-entry {
  white-space: pre-wrap;
  word-break: break-word;
  line-height: 1.35;
  margin-bottom: 0.1rem;
  color: var(--text2);
}
.netcon-entry.ok   { color: var(--green, #3fb950); }
.netcon-entry.err  { color: var(--red,   #ff7b72); }
.netcon-entry.info { color: var(--text); }
.netcon-entry .netcon-ts {
  opacity: 0.55;
  margin-right: 0.4em;
}

/* Inline progress bar entry — single row that updates in place. */
.netcon-progress-bar {
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  margin-top: 0.2rem;
  overflow: hidden;
}
.netcon-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--accent, #58a6ff);
  transition: width 0.12s linear;
}
.netcon-entry.netcon-progress.ok  .netcon-progress-fill { background: var(--green, #3fb950); }
.netcon-entry.netcon-progress.err .netcon-progress-fill { background: var(--red,   #ff7b72); }

/* ---- Inline SVG icons ---- */
.icon, .btn-icon > svg, button > svg {
  pointer-events: none;
  display: inline-block;
  vertical-align: -2px;
  fill: currentColor;
}
/* Compact button variant for icon-only triggers (Pause, Clear, etc.).
   Whole-pixel padding so the rightmost icon's distance to the pane
   edge is identical at every viewport — see comment on .pane-header. */
.btn-mini.btn-icon, .btn-ghost.btn-icon {
  padding: 4px 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.btn-icon[aria-pressed="true"] {
  background: var(--card-active, rgba(88, 166, 255, 0.18));
  border-color: var(--accent, #58a6ff);
  color: var(--accent, #58a6ff);
}
/* Panel-toggle button is a stateful toggle but its glyph already
   communicates the state (left-arrow vs right-arrow). The accent
   highlight on top would just be noise, so opt this one out. */
#ide-toggle-telemetry[aria-pressed="true"] {
  background: transparent;
  border-color: var(--border);
  color: inherit;
}

/* ---- Editable command presets (telemetry pane) ---- */
.cmd-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  align-items: center;
  padding: 0.25rem 0;
}
.cmd-preset {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--card);
}
.cmd-preset > .cmd-preset-send {
  border: none;
  background: transparent;
  padding: 0.2rem 0.5rem;
  font-size: 0.74rem;
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
}
.cmd-preset > .cmd-preset-send:hover { color: var(--accent, #58a6ff); }
.cmd-preset > .cmd-preset-edit,
.cmd-preset > .cmd-preset-delete {
  border: none;
  background: transparent;
  padding: 0.2rem 0.25rem;
  cursor: pointer;
  color: var(--text2);
  display: inline-flex;
  align-items: center;
}
.cmd-preset > .cmd-preset-edit:hover    { color: var(--accent, #58a6ff); }
.cmd-preset > .cmd-preset-delete:hover  { color: var(--red, #ff7b72); }
.cmd-preset.editing {
  flex-wrap: wrap;
  gap: 0.2rem;
  padding: 0.3rem;
  border-color: var(--accent, #58a6ff);
}
.cmd-preset.editing input {
  font-size: 0.74rem;
  padding: 0.15rem 0.3rem;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 3px;
}
.cmd-preset-add {
  font-size: 0.74rem;
}

/* ---- Narrow viewport layouts ----
   Resizers are hidden at narrow widths; the layout falls back to a
   single column. Telemetry sits below the editor, build log + console
   share a row. */
@media (max-width: 960px) {
  .ide-layout {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(260px, 1fr) minmax(280px, 1fr);
  }
  .main-area     { grid-column: 1; grid-row: 1; }
  .telemetry-pane { grid-column: 1; grid-row: 2; min-width: 0; }
  .resizer       { display: none; }
}
@media (max-width: 620px) {
  .topbar { flex-wrap: wrap; height: auto; padding: 0.5rem; }
  .toolbar { flex-wrap: wrap; }
  .main-area {
    grid-template-rows: minmax(220px, 1fr) 0 minmax(180px, auto);
  }
  .bottom-area {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 1fr;
  }
  .log-pane     { grid-column: 1; grid-row: 1; }
  .console-pane { grid-column: 1; grid-row: 2; }
  .resizer      { display: none; }
}
