Files
spark-control/image/app/static/style.css
T
Keysat e783653ef0 v0.23.0:0 - local / fine-tuned model support
Add models that live as a directory on a Spark (e.g. LoRA-merged fine-tunes),
not just Hugging Face repos.

- ModelDef gains local_path; a model must set exactly one of repo / local_path.
  The validator also enforces the local-path whitelist and that any
  --chat-template lives inside local_path (only that dir is mounted).
- build_launch_command bind-mounts the dir into the vLLM container at the SAME
  host==container path via the launch script's VLLM_SPARK_EXTRA_DOCKER_ARGS hook,
  then `vllm serve <dir>`. No launch-cluster.sh change (verified the upstream
  expands that var unquoted; contract noted in runbook.md).
- shellsafe.validate_local_path: absolute path, charset whitelist, no '.'/'..'.
- POST /api/models validates the full entry via ModelDef before persisting, so a
  bad entry can't be written and then break catalog load; _merge_overrides skips
  an invalid override entry instead of failing the whole catalog.
- disk.py size-probes a local path with du; disk-delete refused for local models.
- UI: "+ Add local model" dialog, `local` badge, path shown instead of an HF
  link, delete button hidden for local models.
- Tests: local launch + injection round-trip, chat-template location, traversal,
  exactly-one-source, _merge_overrides skip-invalid (94 pass). Reviewer-agent
  pass; findings addressed.
2026-06-17 22:27:41 -05:00

925 lines
28 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
--bg: #0a0a0d;
--surface: #15151a;
--surface-2: #1c1c22;
--border: #25252c;
--text: #e6e6ea;
--muted: #7e7e8a;
--accent: #4ade80;
--warn: #f59e0b;
--error: #ef4444;
--info: #60a5fa;
--radius: 10px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.muted { color: var(--muted); }
.small { font-size: 13px; }
.hidden { display: none !important; }
.spacer { flex: 1; }
.topbar {
position: sticky;
top: 0;
background: rgba(10, 10, 13, 0.85);
backdrop-filter: saturate(160%) blur(10px);
-webkit-backdrop-filter: saturate(160%) blur(10px);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
z-index: 10;
}
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; }
.logo-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 12px var(--accent); }
.current { flex: 1; text-align: right; font-size: 14px; }
.current strong { color: var(--accent); }
.topbar-btn {
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text);
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
text-decoration: none;
transition: border-color 0.15s, background 0.15s;
}
.topbar-btn:hover { background: #24242c; border-color: var(--accent); color: var(--accent); }
main {
max-width: 880px;
margin: 0 auto;
padding: 24px 20px 80px;
}
.banner {
background: var(--surface);
border: 1px solid var(--warn);
color: var(--warn);
padding: 12px 16px;
border-radius: var(--radius);
margin-bottom: 16px;
font-size: 14px;
}
.banner em { font-style: normal; background: rgba(245, 158, 11, 0.15); padding: 2px 6px; border-radius: 4px; }
/* ===== Endpoint panel ===== */
.endpoint-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 16px;
margin-bottom: 16px;
}
.ep-title { margin-bottom: 8px; letter-spacing: 0.05em; text-transform: uppercase; }
.ep-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
}
.ep-row + .ep-row { border-top: 1px solid var(--border); }
.ep-label {
color: var(--muted);
font-size: 12px;
min-width: 78px;
flex-shrink: 0;
}
.ep-value {
flex: 1;
font: 13px/1.4 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
background: var(--surface-2);
padding: 4px 8px;
border-radius: 5px;
border: 1px solid var(--border);
color: var(--text);
overflow-x: auto;
white-space: nowrap;
}
.copy-btn,
.icon-btn {
appearance: none;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--muted);
padding: 4px 10px;
border-radius: 5px;
font: 12px/1 inherit;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon-btn { padding: 5px 7px; }
.icon-btn svg { width: 14px; height: 14px; display: block; }
.copy-btn:hover,
.icon-btn:hover { color: var(--text); border-color: #34343c; }
.copy-btn.copied,
.icon-btn.copied {
color: var(--accent);
border-color: rgba(74, 222, 128, 0.4);
background: rgba(74, 222, 128, 0.08);
}
.icon-btn.copied svg { color: var(--accent); }
.copy-btn.small { padding: 3px 8px; font-size: 11px; }
.copyable { cursor: pointer; }
.copyable:hover { outline: 1px solid rgba(96, 165, 250, 0.5); }
.copyable.copied { outline: 1px solid var(--accent); background: rgba(74, 222, 128, 0.05); }
.ep-curl { margin-top: 8px; }
.ep-curl summary { cursor: pointer; padding: 4px 0; }
.ep-curl[open] summary { margin-bottom: 6px; }
.snippet {
background: #08080b;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
margin: 0 0 8px;
font: 12px/1.55 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
color: #c7c7d1;
max-height: 200px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
/* ===== Swap panel ===== */
.swap-panel {
background: var(--surface);
border: 1px solid var(--info);
border-radius: var(--radius);
padding: 16px 18px;
margin-bottom: 20px;
}
.swap-header { display: flex; align-items: center; gap: 12px; }
.swap-header #swap-title { font-weight: 600; color: var(--info); }
.timer {
font: 14px/1 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
background: var(--surface-2);
border: 1px solid var(--border);
padding: 5px 10px;
border-radius: 6px;
color: var(--text);
letter-spacing: 0.04em;
}
.spinner {
width: 14px; height: 14px;
border: 2px solid var(--info);
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.phase-row {
display: flex;
align-items: baseline;
gap: 10px;
margin-top: 14px;
}
.phase {
font-size: 16px;
font-weight: 500;
color: var(--text);
}
.phase-detail { font-size: 13px; }
.phase-track {
margin-top: 10px;
height: 6px;
background: var(--surface-2);
border-radius: 3px;
overflow: hidden;
}
.phase-fill {
height: 100%;
width: 2%;
background: linear-gradient(90deg, var(--info), var(--accent));
border-radius: 3px;
transition: width 0.5s ease-out;
}
#swap-log-details {
margin-top: 14px;
}
#swap-log-details summary {
cursor: pointer;
user-select: none;
padding: 2px 0;
}
#swap-log-details summary:hover { color: var(--text); }
#swap-log-details[open] summary { margin-bottom: 8px; }
.log {
background: #08080b;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
margin: 0;
font: 12px/1.55 ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
color: #c7c7d1;
max-height: 260px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
/* ===== Modal dialogs (Advanced / Add to catalog) ===== */
.modal {
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0;
max-width: 520px;
width: 92vw;
}
.modal::backdrop {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
}
.modal-form { padding: 22px 24px; display: flex; flex-direction: column; gap: 12px; }
.modal-form h3 { margin: 0; font-size: 17px; }
.modal-row {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
color: var(--muted);
}
.modal-row.inline { flex-direction: row; align-items: center; gap: 8px; color: var(--text); font-size: 14px; }
.modal-row > span { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.modal-row input[type='text'],
.modal-row input[type='number'],
.modal-row textarea,
.modal-row select {
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text);
padding: 7px 10px;
border-radius: 6px;
font: 13px ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}
.modal-row textarea { font-family: inherit; resize: vertical; }
.modal-row .knob-hint {
color: var(--muted);
font-size: 11px;
line-height: 1.5;
margin-top: 2px;
padding-left: 2px;
}
.modal-row.inline .knob-hint { width: 100%; margin-left: 22px; margin-top: 0; }
.modal-row input:focus, .modal-row textarea:focus, .modal-row select:focus { outline: 1px solid var(--info); border-color: var(--info); }
.modal-row input[type='range'] { padding: 0; flex: 1; }
.modal-fieldset {
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px 14px 4px;
display: flex;
flex-direction: column;
gap: 10px;
}
.modal-fieldset legend { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; padding: 0 6px; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; align-items: center; }
/* ===== Update banner ===== */
.update-banner {
background: var(--surface);
border: 1px solid rgba(96, 165, 250, 0.4);
border-radius: var(--radius);
padding: 12px 14px;
margin-top: 18px;
font-size: 13px;
}
.ub-context { margin-bottom: 8px; line-height: 1.5; }
.ub-context a { color: var(--info); text-decoration: none; }
.ub-context a:hover { text-decoration: underline; }
.ub-context em { font-style: normal; color: var(--text); font-weight: 500; }
#ub-explain-section { margin-top: 8px; }
#ub-explain-section summary { cursor: pointer; padding: 4px 0; }
.explain-content {
background: #08080b;
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px 14px;
margin-top: 8px;
font-size: 13px;
line-height: 1.6;
color: #c7c7d1;
white-space: pre-wrap;
word-break: break-word;
max-height: 320px;
overflow: auto;
}
.explain-content .reasoning {
color: var(--muted);
font-style: italic;
font-size: 11px;
border-left: 2px solid var(--border);
padding-left: 10px;
margin: 4px 0;
}
.update-banner.up-to-date {
border-color: var(--border);
color: var(--muted);
}
.update-banner.warn { border-color: var(--warn); }
.ub-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ub-row .spacer { flex: 1; }
#ub-list { margin-top: 8px; }
#ub-list summary { cursor: pointer; padding: 4px 0; }
#ub-progress { margin-top: 10px; }
/* ===== Hardware dashboard ===== */
.hardware-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.hw-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.hw-card .head {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 4px;
}
.hw-card .head .name { font-weight: 600; font-size: 15px; }
.hw-card .head .meta { color: var(--muted); font-size: 12px; margin-left: auto; }
/* WireGuard "VPN <ip>" badge in the meta line — accent (green) = on a tunnel. */
.hw-card .head .meta .wg-badge { color: var(--accent); font-weight: 600; cursor: help; }
/* Copy-this-Spark's-ssh-key button pins to the top-right corner; meta keeps
its margin-left:auto so name/meta/button read left→right→corner. */
.hw-card .head .ssh-key-btn { align-self: flex-start; padding: 3px 6px; }
.hw-card .head .ssh-key-btn svg { width: 13px; height: 13px; }
.hw-card.unreachable { border-color: rgba(239, 68, 68, 0.4); }
.hw-card.unreachable .name { color: var(--error); }
.hw-card.unreachable ol { color: var(--muted); }
.hw-card .wol-row {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
}
.hw-card .wol-row .btn { padding: 5px 10px; font-size: 12px; }
.hw-card .mac-display { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* SSH-key dialog: key line beside its copy button; long key wraps rather than scrolls. */
.sshkey-row { display: flex; align-items: flex-start; gap: 8px; }
.sshkey-row .snippet { flex: 1; margin: 0; white-space: pre-wrap; word-break: break-all; }
#sshkey-install { white-space: pre-wrap; word-break: break-all; }
.connectivity-content {
max-height: 360px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
background: var(--surface-2);
}
.conn-spark { margin-bottom: 16px; }
.conn-spark h4 { font-size: 13px; margin: 0 0 8px; color: var(--text); }
.conn-event {
font-size: 12px;
display: flex;
gap: 10px;
padding: 4px 0;
border-bottom: 1px solid rgba(255,255,255,0.04);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.conn-event:last-child { border-bottom: 0; }
.conn-event .when { color: var(--muted); flex-shrink: 0; }
.conn-event .what { flex: 1; }
.conn-event.up .what { color: var(--accent); }
.conn-event.down .what { color: var(--error); }
.conn-event.report .what { font-style: italic; }
.conn-event .muted { color: var(--muted); font-style: normal; }
.conn-event .dur { color: var(--muted); }
.conn-summary { color: var(--muted); font-size: 11px; padding: 4px 0 10px; }
.hw-metric { display: flex; align-items: center; gap: 10px; font-size: 12px; }
.hw-metric .label { color: var(--muted); width: 56px; flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.05em; font-size: 11px; }
.hw-metric .bar { flex: 1; height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; position: relative; }
.hw-metric .bar > span {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--info), var(--accent));
border-radius: 4px;
transition: width 0.4s ease-out;
}
.hw-metric .bar.warn > span { background: linear-gradient(90deg, var(--warn), var(--error)); }
.hw-metric .val {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
font-size: 12px;
color: var(--text);
min-width: 110px;
text-align: right;
}
/* ===== Section header (title + action button) ===== */
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0 12px;
}
.section-header .section-title { margin: 0; }
.section-header .spacer { flex: 1; }
.section-header .small-btn,
.btn.small-btn {
margin-left: auto;
padding: 5px 12px;
font-size: 12px;
}
/* ===== Download panel ===== */
.download-panel {
background: var(--surface);
border: 1px solid var(--info);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 16px;
}
.download-form .dl-row {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 0;
flex-wrap: wrap;
}
.dl-label {
color: var(--muted);
font-size: 12px;
min-width: 110px;
flex-shrink: 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dl-row input[type='text'] {
flex: 1;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--text);
padding: 7px 10px;
border-radius: 6px;
font: 13px ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
min-width: 200px;
}
.dl-row input[type='text']:focus { outline: 1px solid var(--info); border-color: var(--info); }
.dl-hf-link {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--info);
padding: 7px 10px;
border-radius: 6px;
text-decoration: none;
font-size: 14px;
flex-shrink: 0;
}
.dl-hf-link:hover { background: rgba(96, 165, 250, 0.08); border-color: var(--info); }
.dl-help { padding-left: 122px; line-height: 1.6; }
.dl-help a { color: var(--info); text-decoration: none; }
.dl-help a:hover { text-decoration: underline; }
.dl-help code { background: var(--surface-2); padding: 1px 5px; border-radius: 3px; font-size: 11px; }
.radio { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text); cursor: pointer; }
.radio input { accent-color: var(--accent); }
.dl-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
.dl-stats {
margin-top: 8px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}
.dl-header { display: flex; align-items: center; gap: 12px; }
.dl-header #dl-title { font-weight: 600; color: var(--info); }
#dl-log-details { margin-top: 12px; }
#dl-log-details summary { cursor: pointer; padding: 4px 0; }
/* ===== NIM install + matrix-bridge dialogs ===== */
.modal#nim-dialog,
.modal#nim-progress-dialog,
.modal#mb-update-dialog,
.modal#mb-logs-dialog { max-width: 640px; }
.nim-grid {
display: grid;
gap: 8px;
grid-template-columns: 1fr;
max-height: 240px;
overflow-y: auto;
margin-bottom: 4px;
}
.nim-card {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
display: flex;
gap: 10px;
align-items: flex-start;
}
.nim-card .info { flex: 1; }
.nim-card .name { font-weight: 600; font-size: 13px; }
.nim-card .desc { color: var(--muted); font-size: 12px; margin-top: 4px; }
.nim-card .img { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #6b6b75; font-size: 11px; margin-top: 4px; word-break: break-all; }
.nim-card .btn { padding: 6px 12px; font-size: 12px; flex-shrink: 0; }
.nim-card .links { font-size: 11px; margin-top: 4px; }
.nim-card .links a { color: var(--info); text-decoration: none; }
.nim-card .links a:hover { text-decoration: underline; }
.nim-key-warn { color: var(--warn); }
/* ===== Section titles ===== */
.section-title {
font-size: 13px;
font-weight: 500;
color: var(--muted);
margin: 24px 0 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.section-title:first-child { margin-top: 0; }
/* ===== Services panel ===== */
.services-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.service-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.service-card.running { border-color: rgba(74, 222, 128, 0.45); }
.service-card.unhealthy { border-color: rgba(239, 68, 68, 0.55); }
.service-card.missing,
.service-card.unconfigured { border-color: rgba(245, 158, 11, 0.45); }
.service-card .head {
display: flex;
align-items: center;
gap: 8px;
}
.service-card .head .name { font-weight: 600; font-size: 15px; }
.service-card .head .kind { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.service-card .head .status {
margin-left: auto;
font-size: 12px;
padding: 2px 8px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--muted);
}
.service-card.running .status { color: var(--accent); border-color: rgba(74, 222, 128, 0.4); }
.service-card.unhealthy .status { color: var(--error); border-color: rgba(239, 68, 68, 0.4); }
.service-card.missing .status,
.service-card.unconfigured .status { color: var(--warn); border-color: rgba(245, 158, 11, 0.4); }
.service-card .row {
display: flex;
align-items: center;
font-size: 12px;
color: var(--muted);
gap: 6px;
}
.service-card .row .k { width: 60px; flex-shrink: 0; }
.service-card .row .v {
color: var(--text);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
word-break: break-all;
flex: 1;
padding: 2px 4px;
border-radius: 4px;
}
.service-card .row .v.muted-v { color: var(--muted); font-family: inherit; }
.service-card .row .v.copyable:hover { outline: 1px solid rgba(96, 165, 250, 0.5); }
.service-card .row .v.copyable.copied { outline: 1px solid var(--accent); background: rgba(74, 222, 128, 0.05); }
.service-card .row .icon-btn { padding: 3px 6px; }
.service-card .row .icon-btn svg { width: 12px; height: 12px; }
.service-card .deep-row .deep-v { display: flex; align-items: center; gap: 6px; font-family: inherit; flex-wrap: wrap; }
.service-card .dh-ok { color: var(--accent); }
.service-card .dh-fail { color: var(--error); font-weight: 500; }
.service-card .dh-run-btn { font-family: inherit; }
.service-card .deep-error {
padding: 4px 8px;
background: rgba(239, 68, 68, 0.06);
border-left: 2px solid var(--error);
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
word-break: break-word;
}
.service-actions {
display: flex;
gap: 6px;
margin-top: 4px;
}
.service-actions .btn { padding: 6px 12px; font-size: 12px; flex: 1; }
.service-actions .btn.danger { color: var(--error); border-color: rgba(239, 68, 68, 0.3); }
.service-actions .btn.danger:hover:not(:disabled) { background: rgba(239, 68, 68, 0.08); border-color: var(--error); }
/* ===== Cards ===== */
.cards {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
transition: border-color 0.15s, transform 0.15s;
}
.card.active {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent) inset, 0 0 24px rgba(74, 222, 128, 0.08);
}
.card .name { font-weight: 600; font-size: 15px; }
.card .meta { display: flex; flex-wrap: wrap; gap: 6px; font-size: 12px; color: var(--muted); }
.card .desc {
font-size: 13.5px;
line-height: 1.5;
color: #b9b9c4;
}
.card .repo {
word-break: break-all;
font-size: 11px;
color: #5c5c66;
}
.card .repo a { color: inherit; text-decoration: none; }
.card .repo a:hover { color: var(--info); text-decoration: underline; }
.card .repo .hf-icon { font-size: 13px; opacity: 0.7; }
.card .repo .local-path { font-family: var(--mono, ui-monospace, monospace); opacity: 0.85; }
.tag {
background: var(--surface-2);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
}
.tag.mode-cluster { color: var(--info); border-color: rgba(96, 165, 250, 0.4); }
.tag.mode-solo { color: var(--accent); border-color: rgba(74, 222, 128, 0.4); }
.tag.cap { color: var(--muted); }
/* Semantic status pills — reuse .tag sizing so every pill on the page
renders at the same 11px / 2px×8px footprint. */
.tag.ok { color: var(--accent); border-color: rgba(74, 222, 128, 0.4); }
.tag.warn { color: var(--warn); border-color: rgba(245, 158, 11, 0.4); }
.tag.bad { color: var(--error); border-color: rgba(239, 68, 68, 0.4); }
.btn {
appearance: none;
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--text);
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
font: inherit;
font-size: 12px;
font-weight: 500;
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}
.btn:hover:not(:disabled) { background: #24242c; border-color: #34343c; }
.btn.primary { background: var(--accent); color: #052e16; border-color: var(--accent); }
.btn.primary:hover:not(:disabled) { background: #6ee19a; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn.danger { color: var(--error); border-color: rgba(239, 68, 68, 0.3); }
.btn.danger:hover:not(:disabled) { background: rgba(239, 68, 68, 0.08); border-color: var(--error); }
.btn.info { background: var(--info); color: #0a1e3d; border-color: var(--info); }
.btn.info:hover:not(:disabled) { background: #82baff; border-color: #82baff; }
.card.active .btn { background: rgba(74, 222, 128, 0.12); color: var(--accent); border-color: rgba(74, 222, 128, 0.4); }
.card-actions { display: flex; gap: 6px; }
.card-actions .btn.primary,
.card-actions .btn.info { flex: 1; }
.card .adv-btn,
.card .test-btn { padding: 8px 12px; font-size: 12px; }
.card .custom-pill { color: var(--info); border-color: rgba(96, 165, 250, 0.4); }
.card .local-pill { color: var(--warn); border-color: rgba(245, 158, 11, 0.4); }
.tag.on-disk { color: var(--accent); border-color: rgba(74, 222, 128, 0.4); }
.tag.not-on-disk { color: var(--muted); border-color: var(--border); opacity: 0.7; }
.card-actions .icon-btn.danger { color: var(--error); border-color: rgba(239, 68, 68, 0.3); margin-left: auto; }
.card-actions .icon-btn.danger:hover:not(:disabled) { background: rgba(239, 68, 68, 0.08); border-color: var(--error); color: var(--error); }
.card-actions .icon-btn.danger:disabled { opacity: 0.35; cursor: not-allowed; }
.dd-hosts { padding-left: 18px; margin: 4px 0 8px; }
.dd-hosts code { background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }
.dd-error { color: var(--error); }
.test-result {
font-size: 12px;
line-height: 1.45;
padding: 8px 10px;
border-radius: 5px;
margin-top: 4px;
border: 1px solid var(--border);
background: var(--surface-2);
}
.test-result.ok { border-color: rgba(74, 222, 128, 0.4); background: rgba(74, 222, 128, 0.04); }
.test-result.fail { border-color: rgba(239, 68, 68, 0.45); background: rgba(239, 68, 68, 0.06); word-break: break-word; }
.test-result .ok-mark { color: var(--accent); font-weight: 600; }
.test-result .fail-mark { color: var(--error); font-weight: 600; }
.footer {
margin-top: 28px;
padding-top: 16px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.health { display: flex; gap: 14px; flex-wrap: wrap; }
.health-item { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); display: inline-block; }
.dot.ok { background: var(--accent); box-shadow: 0 0 8px rgba(74, 222, 128, 0.7); }
.dot.bad { background: var(--error); box-shadow: 0 0 8px rgba(239, 68, 68, 0.7); }
.dot.warn { background: var(--warn); }
@media (max-width: 640px) {
.topbar { padding: 10px 14px; }
main { padding: 16px 14px 80px; }
.cards { grid-template-columns: 1fr; }
}
/* ===== Speech model patches (v0.11) ===== */
.speech-models { margin-top: 28px; }
.sm-blurb { max-width: 880px; margin-bottom: 14px; }
.sm-blurb code {
background: var(--surface-2);
padding: 1px 6px;
border-radius: 4px;
font-size: 12px;
}
.speech-models-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
}
.sm-header {
display: flex;
align-items: center;
gap: 10px;
}
.sm-title {
font-weight: 600;
color: var(--text);
}
/* .sm-pill removed in v0.11.0:1 — speech-models pills now reuse the shared
.tag styling (+ .tag.ok / .tag.warn / .tag.bad color modifiers) so every
pill on the page renders identically. */
.sm-models { display: flex; flex-direction: column; gap: 6px; }
.sm-model-row {
display: grid;
grid-template-columns: 160px 1fr auto;
align-items: center;
gap: 12px;
padding: 6px 0;
border-top: 1px solid var(--border);
}
.sm-model-row:first-child { border-top: none; }
.sm-model-kind { color: var(--muted); font-size: 13px; }
.sm-model-name { font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
.sm-files { display: flex; flex-direction: column; gap: 4px; }
.sm-file-row {
display: grid;
grid-template-columns: 160px 100px 1fr;
gap: 12px;
font-size: 12px;
padding: 4px 0;
}
.sm-file-name code {
background: var(--surface-2);
padding: 1px 6px;
border-radius: 4px;
}
.sm-file-ok { color: var(--accent); }
.sm-file-warn { color: var(--warn); }
.sm-file-bad { color: var(--error); }
.sm-file-sha code {
background: var(--surface-2);
padding: 1px 4px;
border-radius: 3px;
font-size: 11px;
}
.sm-meta { margin-top: 4px; }
.sm-actions { display: flex; gap: 10px; }
.sm-prog-steps {
display: flex;
flex-direction: column;
gap: 6px;
margin: 12px 0;
font-size: 13px;
}
.sm-prog-step {
padding: 6px 10px;
background: var(--surface-2);
border-radius: 6px;
}
.sm-prog-done {
font-weight: 600;
margin-top: 8px;
}
/* ===== Collapsible endpoint card (v0.11.0:1) ===== */
.endpoint-panel .ep-header {
display: flex;
align-items: center;
gap: 10px;
}
.endpoint-panel .ep-title { flex: 1; margin: 0; }
.endpoint-panel .ep-collapse-btn {
flex-shrink: 0;
transition: transform 0.2s;
}
.endpoint-panel.collapsed .ep-body { display: none; }
.endpoint-panel.collapsed .ep-collapse-btn svg { transform: rotate(-90deg); }
.endpoint-panel:not(.collapsed) .ep-header { margin-bottom: 10px; }
/* ===== Dashboard tabs (LLM / Audio) (v0.11.0:1) ===== */
.dashboard-tabs {
display: flex;
gap: 4px;
margin-top: 8px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding: 0 2px;
}
.dashboard-tab {
appearance: none;
background: transparent;
border: 1px solid transparent;
border-bottom: none;
color: var(--muted);
padding: 8px 16px;
border-radius: 6px 6px 0 0;
cursor: pointer;
font: inherit;
font-size: 14px;
font-weight: 500;
margin-bottom: -1px;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.dashboard-tab:hover { color: var(--text); }
.dashboard-tab.active {
color: var(--text);
background: var(--surface);
border-color: var(--border);
border-bottom: 1px solid var(--surface);
}
.tab-content { display: none; }
.tab-content.active { display: block; }
/* (WhisperX install banner styles removed in v0.13.0:0 — see release notes) */