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

:root {
  --bg: #09090b;
  --panel: #18181b;
  --surface: #27272a;
  --border: #3f3f46;
  --text: #f4f4f5;
  --muted: #a1a1aa;
  --accent: #10b981;
  --accent-hover: #34d399;
  --green: #10b981;
  --yellow: #f59e0b;
  --red: #f43f5e;
  --grey: #71717a;
  --radius: 10px;
  --font: system-ui, -apple-system, 'Segoe UI', sans-serif;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

header {
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  padding: 18px 24px;
  display: flex;
  align-items: center;
  gap: 12px;
}

header h1 {
  font-size: 18px;
  font-weight: 600;
  white-space: nowrap;
}

.header-spacer {
  flex: 1;
}

.header-nav {
  display: flex;
  gap: 8px;
  align-items: center;
  white-space: nowrap;
}

.nav-meta {
  color: var(--muted);
  font-size: 12px;
  white-space: nowrap;
}

.nav-link {
  color: var(--muted);
  text-decoration: none;
  font-size: 14px;
  font-weight: 700;
  transition: color .15s;
}

.nav-link:hover {
  color: var(--text);
}

.nav-link.active {
  color: var(--text);
}

.nav-link-logout {
  font-size: 14px;
}

.header-separator {
  color: var(--border);
  font-size: 14px;
  user-select: none;
}

header span {
  font-size: 12px;
  color: var(--muted);
}

#gpu-warning {
  background: #451a03;
  border-bottom: 1px solid #78350f;
  padding: 8px 24px;
  color: var(--yellow);
  font-size: 13px;
}

.layout {
  display: flex;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

/* ---- Left Panel ---- */
.sidebar {
  width: 260px;
  min-width: 200px;
  background: var(--panel);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.sidebar-header {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.sidebar-header h2 {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
}

.btn-icon {
  background: none;
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: var(--radius);
  padding: 4px 8px;
  cursor: pointer;
  font-size: 13px;
  transition: color .15s, border-color .15s;
}

.btn-icon:hover {
  color: var(--text);
  border-color: var(--accent);
}

#model-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 8px 16px;
}

.model-item {
  padding: 8px 10px;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background .12s;
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: relative;
}

.model-item:hover {
  background: var(--surface);
}

.model-item .btn-delete-model {
  display: none;
  position: absolute;
  top: 6px;
  right: 6px;
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 13px;
  padding: 2px 5px;
  border-radius: 4px;
}

.model-item .btn-delete-model:hover {
  color: var(--red);
  background: rgba(244, 63, 94, 0.15);
}

.model-item:hover .btn-delete-model {
  display: block;
}

.model-item.selected {
  background: rgba(16, 185, 129, 0.1);
  border: 1px solid rgba(16, 185, 129, 0.5);
}

.model-item .name {
  font-weight: 500;
  font-size: 13px;
  word-break: break-word;
}

.model-item .path {
  font-size: 11px;
  color: var(--muted);
  word-break: break-all;
}

.model-item .badges {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}

.model-item .badge {
  font-size: 10px;
  background: var(--surface);
  color: var(--muted);
  padding: 1px 5px;
  border-radius: 4px;
}

.model-item .badge-quant {
  background: rgba(16, 185, 129, 0.15);
  color: var(--accent);
}

.model-item .badge-size {
  color: var(--muted);
}

.model-item.selected .badge {
  background: rgba(16, 185, 129, 0.1);
}

#model-search {
  margin: 8px 8px 0;
  padding: 6px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  font-size: 12px;
  font-family: var(--font);
  width: calc(100% - 16px);
}

#model-search:focus {
  outline: none;
  border-color: var(--accent);
}

#model-empty {
  color: var(--muted);
  font-size: 13px;
  padding: 16px 10px;
  text-align: center;
}

/* ---- Main ---- */
.main {
  flex: 1;
  min-width: 0;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* ---- Card ---- */
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
}

.card h2 {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 16px;
}

/* ---- Form ---- */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
  gap: 14px;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding-bottom: 18px;
}

.form-group.full {
  grid-column: 1 / -1;
}

/* Fields side by side on one full-width row (Target node + Docker Image,
   Model Path + Display Name + Note). When one side is hidden (e.g. Target node
   in single-node mode) the others fill the row. */
.launch-pair {
  grid-column: 1 / -1;
  display: flex;
  gap: 14px;
}
.launch-pair > .form-group {
  flex: 1 1 0;
  min-width: 0;
}

/* A field that shares a row but needs less room than its siblings (Display
   Name, which holds a short label rather than a path). Basis is 0 on all of
   them, so the grow ratio alone sets the widths: 1 : 0.6 : 1. */
.launch-pair > .form-group-narrow {
  flex-grow: 0.6;
}

/* Keep the inputs on a row bottom-aligned when only some labels carry an
   info-tip - the tip is 14px tall and a bare label is ~15px, so without a
   shared floor the fields would sit a pixel apart. */
.launch-pair > .form-group > label,
.launch-pair > .form-group > .label-row {
  min-height: 16px;
}

/* The `hidden` attribute alone loses to .form-group's display:flex, so hide
   node selectors explicitly when clustering is off. */
.node-select-row[hidden] {
  display: none;
}

.form-group label {
  font-size: 12px;
  color: var(--muted);
  font-weight: 500;
}

.label-row {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}

.launch-inline-settings-grid .label-row {
  width: auto;
  justify-content: flex-start;
}

.info-tip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 12px;
  line-height: 1;
  cursor: help;
  outline: none;
}

.info-tip:hover,
.info-tip:focus,
.info-tip:focus-visible {
  color: var(--text);
}

.info-tip::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 240px;
  padding: 8px 10px;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.4;
  white-space: normal;
  text-align: left;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease, visibility 0.12s ease;
  pointer-events: none;
  z-index: 100;
}

.info-tip:hover::after,
.info-tip:focus::after,
.info-tip:focus-visible::after {
  opacity: 1;
  visibility: visible;
}

/* Wider tooltip variant for fields whose explanation genuinely needs the
   room (llama-server flag semantics, vendor differences). Add `info-tip-wide`
   alongside `info-tip` on the trigger span. */
.info-tip.info-tip-wide::after {
  max-width: 360px;
}

.info-tip.clip-right::after {
  left: auto;
  right: 0;
  transform: none;
}

.info-tip.clip-left::after {
  left: 0;
  right: auto;
  transform: none;
}

.toggle-row {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  width: fit-content;
  max-width: 100%;
}

.toggle-row>label:first-child {
  flex: 0 1 auto;
}

/* display:grid overrides .form-group's display:flex/flex-direction:column (this
   element carries both classes). The pairs use display:contents so their
   label+toggle become DIRECT grid items and share the table's columns across
   rows - so the left-column toggles line up vertically (same trick as the App
   Settings toggles). Layout: [label][toggle]  [label][toggle], left-column
   pairs stack (Queue sharing over Embedding), the right column holds Auto Restart. */
.launch-toggle-table {
  display: grid;
  grid-template-columns: max-content 40px max-content 40px;
  justify-content: start;
  align-items: center;
  column-gap: 12px;
  row-gap: 12px;
  max-width: 100%;
}

.launch-toggle-pair {
  display: contents;
}

/* breathing room between the two toggle columns */
.launch-toggle-pair.toggle-right > label:not(.toggle-switch) {
  padding-left: 36px;
}

/* Image & PDF Input splits its toggles into two adjacent .launch-toggle-tables
   (Offload alone, then Accept PDF + Try text layer) so the hint <p>s can flow
   at the full container width between them instead of blowing up max-content
   grid columns from inside. Each grid then computes its own max-content col 1
   in isolation, and "Offload mmproj to GPU" is longer than "Accept PDF
   uploads" — so without this the two switches don't line up vertically the
   way Queue sharing / Embedding Model do (they're one grid, so alignment is
   automatic). Pin col 1 (and col 3 for symmetry) to a shared min that clears
   the widest label with a bit of breathing room, and the toggles land at the
   same X in both tables. Gated to >480px because the phone breakpoint below
   collapses .launch-toggle-table to a single 1fr 40px column, and this
   .launch-toggle-table.mmproj-toggle-table selector has higher specificity
   than that plain-class rule — without the min-width guard the 4-column
   ~400px template would win on phones and overflow the viewport. */
@media (min-width: 481px) {
  .launch-toggle-table.mmproj-toggle-table {
    grid-template-columns: minmax(160px, max-content) 40px minmax(160px, max-content) 40px;
  }
}

.launch-settings-section {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  background: var(--surface);
}

.launch-settings-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.launch-settings-section-header h3 {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

/* Anti-Loop section carries TWO independent sub-toggles (DRY sampler + Output
   Loop Detection), each rendered as a nested section-header with an h4 and a
   toggle-switch. Give the h4 the same reset as the h3 above so it stops
   inheriting the browser's default h4 margins (which would push the toggle
   off-line and add erratic top-spacing), and space the two sub-sections apart
   with a top margin instead of inline styles. The FIRST subheader still needs
   a small gap below the section's intro paragraph. */
.launch-settings-section-header.anti-loop-subheader {
  margin-top: 10px;
}
.launch-settings-section-header.anti-loop-subheader h4 {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.launch-inline-settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, max-content));
  gap: 12px 14px;
  align-items: end;
}

/* Form-groups whose "input" is a toggle-switch rather than a wide input/
   select. The shared .label-row uses width:100% + justify-content:space-
   between so the info-tip icon sits at the far right of the field cell -
   great when the cell is filled by a wide input below, terrible when the
   input is a 40px toggle that leaves a huge gap between the label and
   the icon. Shrink-wrap the label-row so the icon hugs the label text. */
.form-group-toggle > .label-row {
  width: auto;
  justify-content: flex-start;
}

/* Native <details> "Advanced" subsection inside the Speculative Decoding
   panel. Keep the summary looking like the section-header style (small,
   muted, clickable) and add breathing room above so it doesn't crowd the
   Draft Model field. Content padding matches the reveal-inner's rhythm. */
.spec-advanced {
  margin-top: 14px;
}
.spec-advanced > summary {
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  padding: 2px 0;
  user-select: none;
}
.spec-advanced > summary:hover {
  color: var(--text);
}
.spec-advanced[open] > summary {
  margin-bottom: 8px;
}

/* Inner grid for launch-form sections that group top-level fields (GPU
   Settings, Container & Proxy Settings). Mirrors the outer .form-grid
   exactly - same columns, gap, and bare inputs - so the fields inside
   look and space identically to Context Size / Parallel Slots up top.
   Not to be confused with .launch-inline-settings-grid, which is the
   tight 120px-min bottom-aligned layout the toggleable sub-sections use
   (Proxy-Side Sampling, Speculative Decoding, Image Input).
   Top-alignment (not end-alignment like the shared grid) keeps all
   labels on the same baseline even when one cell grows a live hint
   below its input; end-alignment would drag every other field's label
   down to match the tallest cell. */
.launch-section-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
  gap: 14px;
  padding-top: 10px;
  align-items: start;
}

/* Whole field (or an individual field) can be greyed out contextually:
   Intel vendor => the device/split knobs are no-ops in-container; GPU
   Layers = 0 => no GPU at all; < 2 visible GPUs => Split Mode + Tensor
   Split are moot; split_mode = none => tensor split is ignored.
   Two things to avoid here:
   1. pointer-events:none on the container would kill hover on the field's
      info-tip and users would lose the explanation of *why* it's greyed;
      instead we rely on `disabled=true` set in JS on the input/select.
   2. Setting `opacity` on the container fades every descendant via CSS
      opacity cascade - including the info-tip's tooltip pseudo-element,
      which is then capped at the container's opacity and appears washed
      out. Fade the visible controls individually instead so the tooltip
      stays fully opaque.
   The disabled input/select/button are already visually greyed by the
   browser via their disabled attribute, so this label rule is what
   actually makes the state distinct. */
.gpu-field-disabled .label-row > label {
  color: var(--muted);
}

/* Collapsible body for the Speculative Decoding / Proxy Sampling sections:
   hidden until its section toggle is on, then it expands into view. Animating
   grid-template-rows 0fr->1fr grows to the content's natural height with no
   magic max-height; the inner element clips its overflow while collapsed. */
.launch-settings-reveal {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .2s ease;
}

.launch-settings-reveal.open {
  grid-template-rows: 1fr;
}

.launch-settings-reveal-inner {
  overflow: hidden;
  min-height: 0;
}

/* Once fully expanded, stop clipping so the first field row's info-tip tooltips
   (which open upward) aren't cut off at the section's top edge. Clipping only
   matters while the height animates. JS adds this after the transition ends. */
.launch-settings-reveal.reveal-expanded .launch-settings-reveal-inner {
  overflow: visible;
}

.cleanup-toggle-grid {
  display: grid;
  grid-template-columns: minmax(0, max-content) 40px;
  align-items: center;
  column-gap: 10px;
  width: fit-content;
  max-width: 100%;
  padding-bottom: 18px;
}

.cleanup-settings-grid {
  display: grid;
  grid-template-columns: minmax(0, max-content) 40px 1fr;
  column-gap: 10px;
  row-gap: 12px;
  width: 100%;
}

.cleanup-settings-grid .cleanup-toggle-grid {
  display: contents;
}

/* Label + info-tip share the first column; keep the icon hugging the label text
   rather than letting .label-row's space-between push it to the column edge
   (columns here are sized to the widest row). Mirrors the launch grid override. */
.cleanup-settings-grid .label-row {
  width: auto;
  justify-content: flex-start;
}

/* These toggle rows used to rely on a col-3 hint paragraph to fill the row; now
   that the hint lives in the label's info-tip, pin label/toggle to their columns
   so each pair stays on its own row instead of bleeding into the empty col 3. */
.cleanup-settings-grid .cleanup-toggle-grid > .label-row { grid-column: 1; }
.cleanup-settings-grid .cleanup-toggle-grid > .label-row + .toggle-switch { grid-column: 2; }

.cleanup-settings-grid .cleanup-input-row {
  display: contents;
}

.cleanup-settings-grid .cleanup-input-row > label {
  grid-column: 1 / 2;
  align-self: center;
}

.cleanup-settings-grid .cleanup-input-row > input {
  grid-column: 2;
  justify-self: start;
}

.cleanup-settings-grid .cleanup-input-row > .hint-text {
  grid-column: 1 / -1;
}

.cleanup-section-divider {
  grid-column: 1 / -1;
  height: 1px;
  background: var(--border);
  margin: 2px 0 8px;
}

.cleanup-last-ran {
  margin-top: 6px;
}

.toggle-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  width: 40px;
  height: 22px;
  flex-shrink: 0;
  cursor: pointer;
}

.toggle-switch input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.toggle-slider {
  position: relative;
  width: 100%;
  height: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background .15s, border-color .15s;
}

.toggle-slider::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  background: var(--text);
  border-radius: 50%;
  transition: transform .15s ease;
}

.toggle-switch input:checked+.toggle-slider {
  background: rgba(16, 185, 129, 0.2);
  border-color: var(--accent);
}

.toggle-switch input:checked+.toggle-slider::after {
  transform: translateX(18px);
  background: var(--accent);
}

.toggle-switch input:focus-visible+.toggle-slider {
  outline: 2px solid rgba(16, 185, 129, 0.45);
  outline-offset: 2px;
}

input,
select,
textarea {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  padding: 7px 10px;
  font-size: 13px;
  font-family: var(--font);
  transition: border-color .15s;
  width: 100%;
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
}

textarea {
  resize: vertical;
  min-height: 60px;
}

select option {
  background: var(--surface);
}

.btn {
  padding: 8px 18px;
  border-radius: var(--radius);
  border: none;
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  transition: opacity .15s, filter .15s;
}

.btn:hover {
  filter: brightness(1.1);
}

.btn:active {
  filter: brightness(0.9);
}

.btn:disabled {
  opacity: 0.7;
  cursor: wait;
  filter: none;
}

.btn-primary {
  background: var(--accent);
  color: #fff;
  font-weight: 600;
}

.btn-danger {
  background: #9f1239;
  color: #fff;
}

.btn-secondary {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
}

.form-actions {
  margin-top: 16px;
  display: flex;
  gap: 10px;
  align-items: center;
}

/* Two-group form-actions row used at the bottom of the launch settings modal:
   primary actions (Launch / Save Preset / status) pinned left, preset-tool
   buttons (export / import / copy llama-server command) pinned right. Wraps
   on narrow viewports so the tool buttons drop below the primaries rather
   than pushing them off-screen. */
.form-actions-split {
  justify-content: space-between;
  flex-wrap: wrap;
}
.form-actions-primary,
.form-actions-secondary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* Small icon-only utility button used by the preset-tool row. Matches the
   .btn baseline (border/radius from that class) but strips the padding + text
   weight so a 32px square icon reads as an affordance next to the wider
   Launch / Save Preset buttons rather than competing with them. */
.btn.btn-tool {
  padding: 6px 10px;
  background: var(--surface);
  color: var(--muted);
  border: 1px solid var(--border);
  font-size: 13px;
  line-height: 1;
}
.btn.btn-tool:hover {
  color: var(--text);
  border-color: var(--accent);
}
.btn.btn-tool:disabled {
  opacity: .5;
  cursor: not-allowed;
}

/* ---- Instances ---- */
#instance-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

#no-instances {
  color: var(--muted);
  text-align: center;
  padding: 32px;
  font-size: 13px;
}

.instance-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.inst-info {
  flex: 1;
  min-width: 180px;
}

.inst-info .model {
  font-weight: 600;
  font-size: 14px;
}

.inst-info .meta {
  color: var(--muted);
  font-size: 12px;
  margin-top: 2px;
}

.inst-meta-accent {
  color: var(--accent);
}

.instance-queue-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.inst-bar-row {
  margin-bottom: 2px;
  font-size: 11px;
}

.inst-bar-row .gpu-bar-label {
  min-width: 30px;
}

.inst-bar-row .gpu-bar-text {
  min-width: 0;
  text-align: left;
  color: var(--muted);
}

.inst-bar-row .inst-mini-bar {
  height: 5px;
}

.inst-bar-gpu {
  margin-top: 2px;
  font-size: 12px;
  color: var(--muted);
}

.instance-queue-label {
  color: var(--muted);
}

.instance-queue-text {
  font-size: 11px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.status-badge {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
}

.status-starting {
  background: rgba(245, 158, 11, 0.15);
  color: var(--yellow);
}

/* Transient state during an async stop: docker SIGTERM grace is running in a
   background thread; the instance is on its way out but not gone yet. Orange,
   to read as "winding down" and stay distinct from yellow "starting". */
.status-stopping {
  background: rgba(249, 115, 22, 0.15);
  color: #f97316;
}

.status-healthy {
  background: rgba(16, 185, 129, 0.15);
  color: var(--green);
}

.status-dead {
  background: rgba(244, 63, 94, 0.15);
  color: var(--red);
}

.status-stopped {
  background: rgba(113, 113, 122, 0.15);
  color: var(--grey);
}

.status-sleeping {
  background: rgba(139, 92, 246, 0.15);
  color: #a78bfa;
}

.badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .03em;
}

.badge-ok {
  background: rgba(16, 185, 129, 0.15);
  color: var(--green);
}

.badge-warn {
  background: rgba(245, 158, 11, 0.15);
  color: var(--yellow);
}

.badge-info {
  background: rgba(99, 102, 241, 0.15);
  color: #818cf8;
}

.inst-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

/* Instance-card controls, split into two semantic groups so a narrower viewport
   can push the read-only 'secondary' row (Logs / Stats) below the stateful
   'primary' row (badge + Stop / Restart / Remove) - see the max-width: 820px
   block below. On desktop both groups sit side-by-side as one continuous flex
   line inside the card, preserving the wide-viewport density (image #2). */
.inst-controls {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.inst-controls-primary,
.inst-controls-secondary {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

/* ---- GPU VRAM bars ---- */
.gpu-bar-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 12px;
}

.gpu-bar-label {
  min-width: 60px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gpu-bar-label-col {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 60px;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}

.gpu-bar-temp {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.gpu-bar-track {
  flex: 1;
  height: 14px;
  background: var(--surface);
  border-radius: 3px;
  overflow: hidden;
  position: relative;
}

.gpu-bar-fill {
  height: 100%;
  border-radius: 3px;
  transition: width .3s;
}

.gpu-bar-text {
  min-width: 100px;
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.gpu-bar-stack {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.gpu-bar-subrow {
  display: flex;
  align-items: center;
  gap: 6px;
}

.gpu-bar-subrow-label {
  font-size: 0.75em;
  width: 3em;
  color: var(--muted);
}

.gpu-bar-subtext {
  width: 3.5em;
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.meter-svg {
  --meter-fill: var(--accent);
  display: block;
  width: 100%;
  overflow: hidden;
}

.meter-track {
  fill: var(--surface);
}

.meter-fill {
  fill: var(--meter-fill);
  transition: width .3s;
}

.gpu-bar-meter {
  flex: 1;
  height: 14px;
}

.instance-queue-meter {
  flex: 1;
  max-width: 120px;
  height: 8px;
}

.progress-tone-success {
  --meter-fill: var(--accent);
}

.progress-tone-warning {
  --meter-fill: var(--yellow);
}

.progress-tone-danger {
  --meter-fill: var(--red);
}

/* ---- Sidebar Downloads Section ---- */
.sidebar-section {
  border-top: 1px solid var(--border);
  flex-shrink: 0;
  background: var(--panel);
  z-index: 2;
  box-shadow: 0 -10px 18px rgba(0, 0, 0, 0.2);
}

.sidebar-section-header {
  padding: 10px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  user-select: none;
}

.sidebar-section-header h2 {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
}

.sidebar-section-header .chevron {
  color: var(--muted);
  font-size: 11px;
  transition: transform .2s;
}

.sidebar-section-header.collapsed .chevron {
  transform: rotate(-90deg);
}

#downloads-panel {
  padding: 6px 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 480px;
  max-height: 40vh;
  overflow-y: auto;
}

#downloads-panel.hidden {
  display: none;
}

.section-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.collapsible-heading {
  cursor: pointer;
  user-select: none;
}

.collapsible-heading .chevron {
  color: var(--muted);
  font-size: 11px;
  transition: transform .2s;
}

/* Buttons parked in a collapsible heading, next to the chevron. They stop
   click propagation so they don't also toggle the section. */
.heading-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}

.btn-compact {
  padding: 5px 12px;
  font-size: 12px;
}

.collapsible-heading.collapsed .chevron {
  transform: rotate(-90deg);
}

.collapsible-heading.collapsed {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: none;
}

.collapsible-body.hidden {
  display: none;
}

.settings-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}

.settings-tab {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: 999px;
  padding: 7px 12px;
  cursor: pointer;
  font: inherit;
  transition: color .15s, border-color .15s, background .15s;
}

.settings-tab:hover {
  color: var(--text);
  border-color: var(--accent);
}

.settings-tab.active {
  background: rgba(16, 185, 129, 0.12);
  border-color: rgba(16, 185, 129, 0.45);
  color: var(--text);
}

.settings-tab:focus-visible {
  outline: 2px solid rgba(16, 185, 129, 0.45);
  outline-offset: 2px;
}

.tab-panel[hidden] {
  display: none;
}

.dl-item {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.dl-item+.dl-item {
  margin-top: 6px;
}

.dl-item-top {
  display: flex;
  align-items: center;
  gap: 6px;
}

.dl-item-name {
  font-size: 12px;
  font-weight: 500;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dl-status {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  padding: 2px 6px;
  border-radius: 10px;
  flex-shrink: 0;
}

.dl-status-downloading {
  background: rgba(16, 185, 129, 0.15);
  color: var(--accent);
}

.dl-status-completed {
  background: rgba(16, 185, 129, 0.15);
  color: var(--green);
}

.dl-status-paused {
  background: rgba(245, 158, 11, 0.15);
  color: #f59e0b;
}

.dl-status-failed {
  background: rgba(244, 63, 94, 0.15);
  color: var(--red);
}

.dl-status-cancelled {
  background: rgba(113, 113, 122, 0.15);
  color: var(--grey);
}

.dl-item-actions {
  display: flex;
  gap: 4px;
}

.btn-xs {
  padding: 3px 8px;
  font-size: 11px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color .15s, border-color .15s;
}

.btn-xs:hover {
  color: var(--text);
  border-color: var(--accent);
}

.btn-xs.danger:hover {
  color: var(--red);
  border-color: var(--red);
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.spinner {
  display: inline-block;
  width: 10px;
  height: 10px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
  flex-shrink: 0;
}

#dl-empty {
  color: var(--muted);
  font-size: 12px;
  text-align: center;
  padding: 10px;
}

.modal-sm {
  width: min(480px, 95vw);
}

/* ---- Log Modal ---- */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .6);
  z-index: 100;
  align-items: center;
  justify-content: center;
}

.modal-overlay.open {
  display: flex;
}

.modal {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  width: min(900px, 95vw);
  max-height: 80vh;
  display: flex;
  flex-direction: column;
}

.modal-header {
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.modal-header h3 {
  font-size: 14px;
  font-weight: 600;
}

.modal-body {
  flex: 1;
  overflow-y: auto;
  padding: 14px 18px;
}

#log-output {
  font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
  font-size: 12px;
  line-height: 1.6;
  color: #b0b0b0;
  white-space: pre-wrap;
  word-break: break-all;
}

/* Stats modal */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}

.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
}

.stat-value {
  font-size: 20px;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.stat-label {
  margin-top: 2px;
  font-size: 12px;
  color: var(--muted);
}

.stat-sub {
  margin-top: 4px;
  font-size: 11px;
  color: var(--muted);
}

.stats-span {
  margin-top: 12px;
  font-size: 11px;
  color: var(--muted);
  text-align: center;
}

.stats-empty {
  text-align: center;
  color: var(--muted);
  padding: 24px 12px;
}

.stats-empty i {
  font-size: 28px;
  color: var(--accent);
  margin-bottom: 10px;
}

.stats-empty p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
}

#log-progress:not([hidden]) + #log-output {
  margin-top: 14px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

/* ---- Download progress bars (log modal) ---- */
#log-progress {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.dlp-empty {
  color: var(--muted);
  font-size: 13px;
  padding: 6px 0;
}

.dlp-overall {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.dlp-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 5px;
  font-size: 12px;
}

.dlp-title {
  font-weight: 600;
}

.dlp-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.dlp-stat {
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.dlp-bar {
  height: 8px;
  background: var(--surface);
  border-radius: 4px;
  overflow: hidden;
}

.dlp-bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 4px;
  transition: width .4s ease;
}

.dlp-part-done .dlp-bar-fill {
  background: var(--green);
}

.dlp-bar-stalled .dlp-bar-fill {
  background: var(--grey);
}

.dlp-bar-indeterminate {
  width: 35% !important;
  border-radius: 4px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: dlpIndeterminate 1.2s linear infinite;
}

@keyframes dlpIndeterminate {
  0% { margin-left: -35%; }
  100% { margin-left: 100%; }
}

.dlp-part-pending .dlp-name,
.dlp-part-pending .dlp-stat {
  opacity: .65;
}

.dlp-error {
  color: var(--red);
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}

#btn-toggle-raw-log.active {
  border-color: var(--accent);
  color: var(--text);
}

/* ---- Toast ---- */
/* Sits below the header (~65px tall = 18+28+18+1) so it never covers the
   nav links on the right. Reported on /logging where the "Dashboard" link
   is the only way back and a toast blocked it for the whole 4s lifetime;
   the same overlap exists on every page that includes #toast-container. */
#toast-container {
  position: fixed;
  top: 80px;
  right: 24px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 200;
  pointer-events: none;
}

.toast {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 11px 18px;
  font-size: 14px;
  max-width: 360px;
  pointer-events: auto;
  animation: slideIn .2s ease;
  display: flex;
  align-items: center;
  gap: 8px;
}

.toast-success {
  border-left: 3px solid var(--green);
}

.toast-error {
  border-left: 3px solid var(--red);
}

.toast-info {
  border-left: 3px solid var(--accent);
}

@keyframes slideIn {
  from {
    transform: translateX(20px);
    opacity: 0;
  }

  to {
    transform: translateX(0);
    opacity: 1;
  }
}

/* Scrollbar */
::-webkit-scrollbar {
  width: 6px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
}

/* -------------------------------------------------------------------------
   Login page
   ------------------------------------------------------------------------- */
body.login-page {
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

.login-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 32px;
  width: 100%;
  max-width: 380px;
}

.login-card h1 {
  font-size: 20px;
  margin-bottom: 4px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.login-card .subtitle {
  color: var(--muted);
  font-size: 13px;
  margin-bottom: 20px;
}

.login-card .form-group {
  margin-bottom: 14px;
}

.login-card label {
  display: block;
  font-size: 13px;
  color: var(--muted);
  margin-bottom: 4px;
}

.login-card input[type="text"],
.login-card input[type="password"] {
  width: 100%;
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  font-size: 14px;
  font-family: var(--font);
}

.login-card input:focus {
  outline: none;
  border-color: var(--accent);
}

.login-card .error {
  background: rgba(196, 90, 90, 0.15);
  border: 1px solid var(--red);
  color: var(--red);
  border-radius: 4px;
  padding: 8px 10px;
  font-size: 13px;
  margin-bottom: 14px;
}

.login-card .btn-login {
  width: 100%;
  padding: 10px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  font-family: var(--font);
  cursor: pointer;
}

.login-card .btn-login:hover {
  filter: brightness(1.1);
}

/* ---- Utility classes (extracted from inline styles) ---- */

.header-logo {
  height: 28px;
  width: auto;
}

.btn-group {
  display: flex;
  gap: 6px;
}

.btn-icon-sm {
  font-size: 12px;
}

.text-meta {
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
}

.status-text {
  color: var(--muted);
  font-size: 12px;
}

.status-text-right {
  color: var(--muted);
  font-size: 12px;
  margin-left: auto;
}

.hint-text {
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 14px;
}

.text-warning {
  color: var(--yellow);
}

.text-success {
  color: var(--green);
}

.text-danger {
  color: var(--red);
}

.form-actions-compact {
  margin-top: 0;
  margin-bottom: 14px;
  flex-wrap: wrap;
  align-items: stretch;
}

.form-actions-compact .btn {
  flex-shrink: 0;
}

.form-actions-compact .input-narrow {
  flex: 1 1 240px;
  min-width: 0;
}

.form-actions-end {
  justify-content: flex-end;
}

.input-narrow {
  max-width: 240px;
}

.download-settings-input {
  width: 138px;
}

.cleanup-settings-input {
  width: 80px;
}

.launch-settings-input {
  width: 90px;
}

.launch-settings-select {
  width: auto;
}

.spec-draft-model-row {
  padding-top: 12px;
  padding-bottom: 0;
}

.gpu-layers-suggestion {
  margin-top: 4px;
  font-size: 0.82em;
  min-height: 1.1em;
  text-wrap: wrap;
}

.cleanup-settings-grid-spaced {
  margin-bottom: 14px;
}

.inline-field {
  display: flex;
  align-items: center;
  gap: 4px;
}

.form-grid-single {
  grid-template-columns: 1fr;
}

.modal-header-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
}

.checkbox-accent {
  accent-color: var(--accent);
  width: 30px;
}

.confirm-message {
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-line;
}

.key-reveal-box {
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 10px 14px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.key-reveal-value {
  flex: 1;
  font-size: 12px;
  word-break: break-all;
  color: var(--accent);
}

.icon-copy-success {
  color: var(--green);
}

.clipboard-copy-buffer {
  position: fixed;
  opacity: 0;
  pointer-events: none;
}

.list-empty-state {
  color: var(--muted);
  font-size: 12px;
  text-align: center;
  padding: 12px;
}

.list-meta-code,
.list-meta-date {
  font-size: 11px;
  color: var(--muted);
}

/* ---- Star / Favorite button ---- */
.btn-star {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 18px;
  padding: 2px 4px;
  transition: color .15s, transform .15s;
  flex-shrink: 0;
  line-height: 1;
}

.btn-star:hover {
  color: var(--yellow);
  transform: scale(1.15);
}

.btn-star.active {
  color: var(--yellow);
}

.btn-star.active i {
  font-weight: 900;
}

/* Star in model list sidebar */
.model-item .btn-star {
  font-size: 14px;
  padding: 0 4px 0 0;
  align-self: flex-start;
  margin-top: 1px;
}

.model-item-row {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  width: 100%;
}

.model-item-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Repo info in settings tab bar */
.settings-tab-repo {
  margin-left: auto;
  font-size: 12px;
  color: var(--muted);
  align-self: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 260px;
}
.settings-tab-repo a {
  color: var(--muted);
  text-decoration: none;
}
.settings-tab-repo a:hover {
  color: var(--text);
  text-decoration: underline;
}

/* Star button in settings tab bar */
.settings-tab-star {
  margin-left: auto;
  font-size: 18px;
}
.settings-tab-repo:not([hidden]) ~ .settings-tab-star {
  margin-left: 8px;
}

/* Those trailing controls describe the selected model, so they belong to the
   launch form even though they sit in the tab bar. Hide the whole group on any
   other tab - including controls added here in future. */
.tabbed-card:not([data-active-tab="launch"]) .settings-tab-repo,
.tabbed-card:not([data-active-tab="launch"]) .settings-tab-update,
.tabbed-card:not([data-active-tab="launch"]) .settings-tab-star {
  display: none;
}

/* "Check for updates" / "Update available" pill in the settings tab bar. Sits
   between the repo link and the star, so it only claims the auto margin when
   the repo link (which normally holds it) is hidden. */
.settings-tab-update {
  margin-left: auto;
  align-self: center;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  font-size: 12px;
  white-space: nowrap;
  cursor: pointer;
}
.settings-tab-repo:not([hidden]) ~ .settings-tab-update {
  margin-left: 8px;
}
.settings-tab-update:hover:not(:disabled) {
  color: var(--text);
  border-color: var(--muted);
}
.settings-tab-update:disabled {
  opacity: 0.6;
  cursor: default;
}
/* Same trap as .node-select-row above: the display rule beats the `hidden`
   attribute, so without this the button stays painted with no model selected
   and for models with no known source repo - where it has nothing to check. */
.settings-tab-update[hidden] {
  display: none;
}
/* Armed: the next click re-pulls, so make it read as an action, not a status. */
.settings-tab-update.update-available {
  color: var(--yellow);
  border-color: currentColor;
}

.login-logo {
  height: 22px;
  width: auto;
}

/* ------------------------------------------------------------------ */
/* Clustering                                                          */
/* ------------------------------------------------------------------ */
.node-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  background: var(--surface);
  color: var(--muted);
  vertical-align: middle;
}
.node-badge i { font-size: 10px; }

.cluster-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  vertical-align: middle;
  margin-right: 2px;
}
.cluster-dot-online { background: var(--green); }
.cluster-dot-offline { background: var(--grey); }

/* Per-node groups inside the System / GPU cards */
.cluster-node-group { margin-bottom: 12px; }
.cluster-node-group:last-child { margin-bottom: 0; }
.cluster-node-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text);
  margin-bottom: 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.cluster-node-head .text-meta { margin-left: auto; }
.cluster-stat-empty {
  font-size: 12px;
  color: var(--muted);
  padding: 2px 0 4px;
}

/* Cluster settings tab */
.cluster-section-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .03em;
  margin: 14px 0 8px;
}
.cluster-self {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.cluster-self-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}
.cluster-self-row .cluster-k {
  width: 120px;
  flex-shrink: 0;
  color: var(--muted);
}
.cluster-self-tag {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--accent);
  background: rgba(16, 185, 129, 0.15);
  padding: 1px 6px;
  border-radius: 4px;
}
.instance-card-remote { opacity: 0.92; }
.inst-remote-note { color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }

/* Cluster: ghost (available-on-other-node) model entries */
.model-ghost-divider {
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .03em;
  margin: 10px 6px 6px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.model-item-ghost { opacity: 0.7; cursor: default; }
.model-item-ghost .name { font-style: italic; }
.btn-ghost-download {
  background: transparent;
  border: none;
  color: var(--accent);
  cursor: pointer;
  padding: 6px 8px;
  align-self: center;
}
.btn-ghost-download:hover { color: var(--accent-hover); }

/* ---------------------------------------------------------------------------
   Logging page (/logging)
   --------------------------------------------------------------------------- */
.logging-page {
  max-width: 1320px;
  margin: 0 auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* body is height:100vh/overflow:hidden, so this needs to be its own scroll
     container (like the dashboard's .main) or its content gets clipped with no
     way to scroll. flex:1 fills the space under the header; min-height:0 lets a
     tall conversations list actually overflow-scroll instead of pushing out. */
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.logging-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.logging-title {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* segmented time-window control */
.logging-window {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.lw-btn {
  background: var(--surface);
  color: var(--muted);
  border: none;
  border-left: 1px solid var(--border);
  padding: 6px 14px;
  font-size: 12px;
  cursor: pointer;
}
.lw-btn:first-child { border-left: none; }
.lw-btn:hover { color: var(--text); }
.lw-btn.active { background: var(--green); color: #fff; }

/* Recent-Conversations filters (model + date). Sits between the title and
   the "Clear all" button in the card header. The model dropdown is
   populated from the fetched list on every load; the date is a native
   picker matched against last_seen_at on the same local calendar day. Both
   compose with the page-level time window (AND). All filtering is
   client-side, over the same /api/request-log/conversations?limit=200
   response the list already renders from - no backend changes. Filters
   only tighten the recent-200 window, so a date earlier than that window
   reaches will legitimately show nothing. */
.logging-filters {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.logging-filters select,
.logging-filters input[type="date"] {
  width: auto;
  padding: 5px 8px;
  font-size: 12px;
  min-width: 0;
}
.logging-filters select { max-width: 200px; }
.lf-clear {
  background: none;
  border: 1px solid transparent;
  color: var(--muted);
  padding: 4px 8px;
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 12px;
}
.lf-clear:hover { color: var(--text); border-color: var(--border); }

.logging-banner {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: 14px;
  font-size: 13px;
  color: var(--muted);
}
.logging-banner a { color: var(--text); }

.logging-conv-list {
  display: flex;
  flex-direction: column;
}

.logging-conv-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 200px 80px 110px 160px;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 10px 4px;
  cursor: pointer;
  color: var(--text);
  font-size: 13px;
}
.logging-conv-row:last-child { border-bottom: none; }
.logging-conv-row:hover { background: var(--surface); }

.lc-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lc-model,
.lc-meta,
.lc-when { color: var(--muted); font-size: 12px; }
.lc-model { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lc-meta { text-align: right; font-variant-numeric: tabular-nums; }
.lc-when { text-align: right; white-space: nowrap; }

.logging-empty { color: var(--muted); padding: 16px 4px; font-size: 13px; }
.logging-untitled { color: var(--muted); font-style: italic; }
.logging-dim { color: var(--muted); }

/* conversation drill-down turns */
.conv-turn {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  margin-bottom: 12px;
}
.conv-turn:last-child { margin-bottom: 0; }
.conv-turn-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  font-size: 12px;
}
.conv-turn-n { font-weight: 600; }
.conv-msg { margin-bottom: 8px; }
.conv-msg-role {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 3px;
}
.conv-msg pre {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--mono, monospace);
  font-size: 12.5px;
  background: var(--surface);
  border-radius: 6px;
  padding: 8px 10px;
  max-height: 320px;
  overflow: auto;
}
.conv-msg-resp pre { background: var(--panel); border: 1px solid var(--border); }
.conv-metrics { font-size: 12px; color: var(--muted); }
.conv-metrics summary { cursor: pointer; }
.conv-metrics-body { margin-top: 6px; font-variant-numeric: tabular-nums; }

/* ============================================================
   Responsive / mobile
   ------------------------------------------------------------
   The desktop layout is a fixed two-pane flexbox (260px sidebar +
   independently scrolling .main) inside a height:100vh, overflow:hidden
   body. That model has no room to breathe below ~820px, so there we
   collapse to a single column that scrolls as one natural document.
   ============================================================ */
/* ---- Intermediate viewport: split card controls into two rows ----
   Kicks in before the full card-stack (820px below). Reason: with a 260px
   sidebar and the info block wanting real estate, `[info | badge | Stop |
   Logs | Stats]` on one flex line gets cramped well before the card is
   narrow enough to stack. Split the controls into stacked primary
   (badge + state buttons) + secondary (Logs / Stats) sub-rows; the card
   itself stays a flex row so `.inst-info` keeps growing on the left. */
@media (max-width: 1080px) {
  .inst-controls {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  /* Right-align both sub-rows: the card is still flex-row here so the
     controls hug the card's right edge; align-items: stretch on the parent
     makes both sub-rows the width of the widest one, so flex-end lines
     their right edges up (badge + Stop against Logs + Stats). Without this,
     the smaller sub-row starts flush left and looks ragged against the
     wider one (see the reported alignment gap in image #5). */
  .inst-controls-primary,
  .inst-controls-secondary {
    justify-content: flex-end;
  }
  /* Force badge + every button to the same width in this zone so the two
     sub-rows produce a clean 2-column grid: [HEALTHY][Stop] above
     [Logs][Stats] with matched column edges. Without this the shorter
     "Stop" label makes column 2 narrower than the "Stats" column below
     it (image #6). box-sizing so padding is inside the min-width;
     text-align: center so "HEALTHY" sits above "Logs" instead of
     hugging the badge's own shrunk-to-fit width. */
  .inst-controls .btn {
    min-width: 90px;
    box-sizing: border-box;
    text-align: center;
  }
  .inst-controls-primary .status-badge {
    min-width: 90px;
    box-sizing: border-box;
    text-align: center;
  }
  /* "Restart" is a longer label than "Stop" / "Logs" / "Stats" and its
     natural width overflowed the 90px min, so it stuck out past the
     other buttons (image #7). Tighter horizontal padding drops the
     natural width back inside 90px so the min-width applies and the
     column edges line up. */
  .inst-controls .btn-restart {
    padding-left: 10px;
    padding-right: 10px;
  }
}

@media (max-width: 820px) {
  /* Scroll the whole document instead of two independent panes, and
     clamp horizontal overflow from any stray fixed-width child. */
  html,
  body {
    overflow-x: hidden;
  }
  body {
    height: auto;
    min-height: 100vh;
    min-height: 100dvh;
    overflow-y: auto;
  }

  header {
    padding: 12px 16px;
    flex-wrap: wrap;
    gap: 8px 10px;
  }

  #gpu-warning {
    padding: 8px 16px;
  }

  /* Stack the sidebar above main; both size to their content. */
  .layout {
    flex-direction: column;
    flex: none;
    overflow: visible;
  }
  .sidebar {
    width: 100%;
    min-width: 0;
    border-right: none;
    border-bottom: 1px solid var(--border);
    overflow: visible;
  }
  /* Keep the model library from pushing everything else off-screen;
     it scrolls internally while the page scrolls around it. */
  #model-list {
    max-height: 50vh;
  }
  .main {
    flex: none;
    overflow: visible;
    padding: 16px;
    gap: 16px;
  }

  .card {
    padding: 16px;
  }

  /* Paired launch fields (Target node + Docker Image, etc.) stack. */
  .launch-pair {
    flex-direction: column;
  }

  /* Action rows wrap rather than overflow the card. */
  .form-actions {
    flex-wrap: wrap;
  }

  /* Modals go (near) full-screen instead of a centered 900px box. */
  .modal-overlay {
    padding: 10px;
  }
  .modal,
  .modal-sm {
    width: 100%;
    max-width: 100%;
    max-height: 90vh;
    max-height: 90dvh;
  }

  /* The logging page is its own scroll container on desktop; let it
     flow with the document on mobile like .main does. */
  .logging-page {
    overflow: visible;
    height: auto;
    padding: 16px;
  }

  /* The 5-column conversation row (~550px of fixed columns) collapses
     to a wrapping list: title on its own line, then model / turns /
     tokens inline with the timestamp pushed to the right. */
  .logging-conv-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 12px;
  }
  .lc-title {
    flex: 1 1 100%;
    white-space: normal;
  }
  .lc-model,
  .lc-meta {
    text-align: left;
  }
  .lc-when {
    margin-left: auto;
  }

  /* 16px font on focusable fields stops iOS Safari auto-zooming. */
  input[type="text"],
  input[type="number"],
  input[type="password"],
  input[type="search"],
  select,
  textarea {
    font-size: 16px;
  }
}

@media (max-width: 480px) {
  header {
    padding: 10px 12px;
  }
  header h1 {
    font-size: 16px;
  }
  .main,
  .logging-page {
    padding: 12px;
  }
  .card {
    padding: 13px;
  }

  /* Stack instance card content so actions/bars aren't crushed into a
     sliver next to the model name. */
  .instance-card {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }
  .inst-info {
    min-width: 0;
  }

  /* Below 820px the card itself stacks and .inst-controls spans the full
     card width. Undo the right-align from the 1080px block so buttons
     don't hug the right edge of a full-width row on phones - left-align
     matches the pre-refactor mobile look. */
  .inst-controls-primary,
  .inst-controls-secondary {
    justify-content: flex-start;
  }

  /* Segmented time-window control spans the row and splits evenly. */
  .logging-window {
    width: 100%;
  }
  .lw-btn {
    flex: 1;
  }

  /* Launch toggle pairs (Queue sharing / Auto Restart / Embedding) collapse from
     the desktop two-column table to a single label|toggle column. The 4-column
     max-content grid never wrapped, so the right column (Auto Restart) ran off
     the viewport on phones. */
  .launch-toggle-table {
    grid-template-columns: 1fr 40px;
  }
  .launch-toggle-pair.toggle-right > label:not(.toggle-switch) {
    padding-left: 0;
  }

  /* Request recording / Retention: stack label above its control instead of
     pinning the fixed-width input to the narrow toggle column, which pushed it
     off the right edge once the label column consumed the row. */
  .cleanup-settings-grid .cleanup-input-row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 5px;
    grid-column: 1 / -1;
  }
}

/* ---- Portrait phones: monitoring-first order ----
   Landscape phones are ≥820px wide and keep the desktop two-column layout, so
   this never touches them. In portrait the width-based stack above applies,
   but it leads with the Model Library (eating the top ~50vh) and buries
   Running Instances at the very bottom — a config-first layout on a device you
   mostly open to *monitor and control*. Flip the priority: Running Instances
   and the live System/GPU widgets float to the top, launch/settings config
   sits below them, and the Model Library (only needed while launching) drops
   to the bottom. Reorder only — collapse stays JS-driven so the chevrons and
   localStorage state stay in sync across orientations. */
@media (max-width: 820px) and (orientation: portrait) {
  /* Model Library (sidebar) drops below the main content. */
  .main {
    order: 1;
  }
  .sidebar {
    order: 2;
    border-bottom: none;
    border-top: 1px solid var(--border);
  }

  /* Within main: Instances first, then the compact monitoring widgets,
     then the (natural-order) launch/settings card. */
  .instances-card {
    order: -3;
  }
  #system-info-card {
    order: -2;
  }
  #gpu-vram-card {
    order: -1;
  }
}

/* Storage degraded banner - shown while the database is unreachable and this
   node is serving from its local mirror. */
.storage-degraded-banner {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  margin-bottom: 16px;
  padding: 12px 16px;
  border: 1px solid var(--yellow);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: rgba(245, 158, 11, 0.08);
  color: var(--text);
  font-size: 13px;
  line-height: 1.5;
}

.storage-degraded-banner[hidden] {
  display: none;
}

.storage-degraded-banner i {
  color: var(--yellow);
  margin-top: 2px;
  flex-shrink: 0;
}

.storage-degraded-banner strong {
  color: var(--yellow);
}

/* Small identity footer inside the main scroll column - version + repo link,
   no visual weight. It lives inside <main> (not <body>) so it does NOT stick
   to the viewport bottom and does NOT span the sidebar. `margin-top: auto`
   pins it to the bottom of the main column when content is short, and it
   flows naturally after the last card when content is tall. */
.app-footer {
  margin-top: auto;
  padding: 4px 0 0;
  text-align: center;
  font-size: 11px;
  color: var(--muted);
  opacity: 0.7;
}

.app-footer a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted currentColor;
}

.app-footer a:hover {
  opacity: 1;
  color: var(--text);
}

/* The footer's repo link doubles as a subtle "please star this" affordance:
   one anchor, one URL, with a star icon in front of the repo path. Muted at
   rest so it reads as ordinary attribution; the star warms to amber on
   hover to nod at what a click can do (both browse the repo and star it).
   Kept quiet on purpose - a page about launching models is not the place
   for a real CTA, this is just a low-key invitation for users who care to
   find it. */
.app-footer-star {
  margin-right: 3px;
  transition: color .15s;
}
.app-footer-repo:hover .app-footer-star {
  color: var(--yellow);
}
