1@import "uplot/dist/uPlot.min.css";
2
3:root {
4 color-scheme: dark;
5 --page: #151529;
6 --panel: #0f0f20;
7 --rim: #0a0a17;
8 --surface: #1c1c37;
9 --raised: #29294b;
10 --hover: #232342;
11 --well: #00000033;
12 --line: #2f2f53;
13 --grid: #262645;
14 --axis: #454571;
15 --text: #efeeff;
16 --text-2: #c0bee2;
17 --muted: #8b89b8;
18 --accent: #938cff;
19 --accent-wash: #938cff26;
20 --on-accent: #10112a;
21 --focus: #a9a3ff;
22 --shadow: 0 1px 2px #0005, 0 4px 14px #0003;
23 --shimmer: color-mix(in srgb, var(--raised) 55%, var(--text-2));
24
25 --series-1: #3987e5;
26 --series-2: #d95926;
27 --series-3: #199e70;
28 --series-4: #c98500;
29 --series-5: #d55181;
30 --series-6: #008300;
31 --series-7: #9085e9;
32 --series-8: #e66767;
33 --other: #605e8a;
34
35 --good: #0ca30c;
36 --warning: #fab219;
37 --serious: #ec835a;
38 --critical: #d03b3b;
39 --stopped: #605e8a;
40
41 --sans: "Name Sans", system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", "Noto Sans", sans-serif;
42 --mono: "Name Mono", ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, Consolas, monospace;
43 --radius: 8px;
44 /** Height of buttons and inputs; set it on a container to shrink every control inside. */
45 --control: 30px;
46 --radius-lg: 12px;
47 --sidebar: 300px;
48 --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
49 --ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1);
50
51 scrollbar-color: var(--axis) transparent;
52}
53
54@media (prefers-color-scheme: light) {
55 :root {
56 color-scheme: light;
57 --page: #f7f6fd;
58 --panel: #ebeaf7;
59 --rim: #dcdaee;
60 --surface: #ffffff;
61 --raised: #e9e7f8;
62 --hover: #eeedfa;
63 --well: #1f1a4d0a;
64 --line: #dad8ee;
65 --grid: #ebe9f6;
66 --axis: #bbb8da;
67 --text: #18163a;
68 --text-2: #484575;
69 --muted: #625f8c;
70 --accent: #1a46cd;
71 --accent-wash: #1a46cd14;
72 --on-accent: #ffffff;
73 --focus: #1a46cd;
74 --shadow: 0 1px 2px #1f1a4d12, 0 4px 14px #1f1a4d0a;
75 --shimmer: var(--surface);
76 --series-1: #2a78d6;
77 --series-2: #eb6834;
78 --series-3: #1baf7a;
79 --series-4: #eda100;
80 --series-5: #e87ba4;
81 --series-6: #008300;
82 --series-7: #4a3aa7;
83 --series-8: #e34948;
84 --other: #b0aed0;
85 --stopped: #b0aed0;
86 }
87}
88
89* { box-sizing: border-box; }
90
91html, body, #root { height: 100%; margin: 0; }
92
93body {
94 background: var(--page);
95 color: var(--text);
96 font: 14px/1.4 var(--sans);
97 -webkit-font-smoothing: antialiased;
98}
99
100a { color: inherit; text-decoration: none; }
101button { font: inherit; color: inherit; }
102:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 4px; }
103::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }
104
105.shell {
106 display: grid;
107 grid-template: auto minmax(0, 1fr) / var(--sidebar) 1fr;
108 height: 100%;
109}
110.shell > :is(.sidebar, .main) { grid-row: 2; }
111
112/* The preview an admin started from the account menu; loud so it can't be forgotten. */
113.viewing-as {
114 grid-column: 1 / -1;
115 display: flex;
116 align-items: center;
117 gap: 8px;
118 padding: 4px 8px 4px 16px;
119 background: var(--accent);
120 color: var(--on-accent);
121 font-size: 13px;
122}
123.viewing-as .button { margin-left: auto; --control: 24px; border: 0; background: var(--on-accent); color: var(--accent); font-weight: 600; }
124.viewing-as .button:hover:not(:disabled) { background: color-mix(in srgb, var(--on-accent) 85%, var(--accent)); }
125
126/* sidebar ---------------------------------------------------------------- */
127
128.sidebar {
129 background: var(--panel);
130 border-right: 4px solid var(--rim);
131 display: flex;
132 flex-direction: column;
133 overflow: hidden;
134 font-size: 13px;
135}
136
137.brand {
138 display: flex;
139 align-items: center;
140 gap: 10px;
141 padding: 16px 16px 14px;
142 font-weight: 700;
143 font-size: 18px;
144 letter-spacing: -0.01em;
145 color: var(--accent);
146}
147
148.brand img { flex: none; width: 38px; height: 38px; }
149
150.nav {
151 flex: 1;
152 overflow-y: auto;
153 padding: 2px 8px 12px;
154}
155
156.nav-item {
157 display: flex;
158 align-items: center;
159 gap: 10px;
160 height: 30px;
161 padding: 0 10px;
162 border: 0;
163 border-radius: var(--radius);
164 color: var(--text-2);
165 cursor: pointer;
166 user-select: none;
167 width: 100%;
168 background: none;
169 text-align: left;
170 transition: background-color 150ms, color 150ms;
171}
172
173.nav-item:hover { background: var(--hover); color: var(--text); }
174.nav-item.active { background: var(--accent-wash); color: var(--accent); font-weight: 600; }
175.nav-item .icon { width: 16px; height: 16px; flex: none; opacity: 0.9; }
176.nav-item .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
177.nav-item .badge { display: flex; gap: 8px; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
178.nav-item .tally .status-label { gap: 4px; color: var(--text-2); }
179.nav-item .tally .status { width: 8px; height: 8px; }
180
181.chevron { width: 14px; height: 14px; flex: none; color: var(--muted); transition: transform 350ms var(--ease-overshoot); }
182.chevron.open { transform: rotate(90deg); }
183
184/* A page link with its tabs folded under it; the toggle sits over the link's right end. */
185.nav-row { position: relative; }
186.nav-row .nav-item { padding-right: 36px; }
187.nav-toggle {
188 position: absolute;
189 top: 4px;
190 right: 6px;
191 display: grid;
192 place-items: center;
193 width: 22px;
194 height: 22px;
195 padding: 0;
196 border: 0;
197 border-radius: 99px;
198 background: none;
199 cursor: pointer;
200 transition: background-color 150ms;
201}
202.nav-toggle:hover { background: var(--raised); }
203.nav-toggle:hover .chevron { color: var(--text); }
204.nav-sub {
205 display: flex;
206 align-items: center;
207 gap: 8px;
208 height: 26px;
209 padding-left: 40px;
210 border-radius: var(--radius);
211 color: var(--text-2);
212 transition: background-color 150ms, color 150ms;
213}
214.nav-sub .icon { width: 14px; height: 14px; flex: none; opacity: 0.75; }
215.nav-sub:hover { background: var(--hover); color: var(--text); }
216.nav-sub[aria-current="page"] { background: var(--accent-wash); color: var(--accent); font-weight: 600; }
217/* With a tab lit below it, the page link keeps its color but drops the fill. */
218.nav-row:has(+ .nav-group.open [aria-current="page"]) .nav-item.active { background: none; }
219
220.nav-group {
221 display: grid;
222 grid-template-rows: 0fr;
223 opacity: 0;
224 transition: grid-template-rows 350ms var(--ease-out), opacity 200ms;
225}
226
227.nav-group.open { grid-template-rows: 1fr; opacity: 1; }
228.nav-group > div { min-height: 0; overflow: hidden; }
229
230.service-row {
231 display: grid;
232 grid-template-columns: 12px 16px 1fr 44px 44px 34px;
233 align-items: center;
234 gap: 6px;
235 height: 26px;
236 padding-left: 16px;
237 border-radius: var(--radius);
238 color: var(--text-2);
239 transition: background-color 150ms, color 150ms;
240}
241
242.service-row:hover { background: var(--hover); color: var(--text); }
243.service-row.active { background: var(--accent-wash); color: var(--accent); font-weight: 600; }
244.service-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
245/* Hover targets span the whole row height, not just the drawn pixels. */
246.service-row .spark { height: 100%; }
247
248
249.whoami { border-top: 2px solid var(--rim); padding: 6px 8px; }
250
251.whoami-button {
252 display: flex;
253 align-items: center;
254 gap: 10px;
255 width: 100%;
256 height: 40px;
257 padding: 0 10px 0 7px;
258 border: 0;
259 border-radius: var(--radius);
260 background: none;
261 color: var(--text-2);
262 font: inherit;
263 text-align: left;
264 cursor: pointer;
265 transition: background-color 150ms, color 150ms;
266}
267
268.whoami-button:is(:hover, :has(+ :popover-open)) { background: var(--hover); color: var(--text); }
269.whoami-button.active { background: var(--accent-wash); color: var(--accent); }
270.whoami-button .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
271.whoami-button .icon { flex: none; color: var(--muted); }
272.whoami .avatar { width: 26px; height: 26px; font-size: 12px; }
273
274/* Pinned just above the corner; a popover sits in the top layer, so the sidebar can't clip it. */
275.account-menu {
276 inset: auto auto 54px 8px;
277 width: calc(var(--sidebar) - 20px);
278 margin: 0;
279 padding: 4px;
280 border: 1px solid var(--line);
281 border-radius: var(--radius-lg);
282 background: var(--surface);
283 color: var(--text);
284 box-shadow: 0 8px 28px #0005;
285}
286
287.account-menu:popover-open { animation: menu-in 200ms var(--ease-out); }
288.view-as { display: grid; gap: 6px; margin-top: 4px; padding: 8px 10px 6px; border-top: 1px solid var(--line); font-size: 12px; }
289.view-as .button { justify-self: start; }
290@keyframes menu-in { from { opacity: 0; transform: translateY(6px); } }
291
292/* status ------------------------------------------------------------------ */
293
294.status { width: 10px; height: 10px; display: block; transition: color 300ms; }
295.status[data-tip] { box-sizing: content-box; padding: 4px; margin: -4px; }
296.status.healthy { color: var(--good); }
297.status.degraded { color: var(--warning); }
298.status.down { color: var(--critical); }
299.status.stopped { color: var(--stopped); }
300.status.working { color: var(--text-2); }
301.status:is(.deploying, .restarting, .starting) { color: var(--accent); }
302.status:is(.deploying, .restarting, .starting, .working) { transform-box: view-box; transform-origin: center; animation: spin 1.1s cubic-bezier(0.55, 0.15, 0.45, 0.85) infinite; }
303@keyframes spin { to { transform: rotate(360deg); } }
304
305.status-label { display: inline-flex; align-items: center; gap: 6px; color: var(--text-2); }
306
307/* main -------------------------------------------------------------------- */
308
309.main {
310 overflow-y: auto;
311 background: radial-gradient(900px 320px at 50% -160px, var(--accent-wash), transparent);
312}
313
314.page {
315 max-width: 1400px;
316 margin: 0 auto;
317 --gutter: 28px;
318 padding: 22px var(--gutter) 48px;
319}
320
321.list-page { height: 100%; display: flex; flex-direction: column; padding-bottom: 0; }
322.list-page > * { flex: none; }
323.list-page .summary { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 350ms var(--ease-out), opacity 200ms; }
324.list-page .summary.open { grid-template-rows: 1fr; opacity: 1; }
325.list-page .summary > div { min-height: 0; overflow: hidden; }
326.summary-toggle {
327 display: grid;
328 place-items: center;
329 height: 16px;
330 margin: 4px 0;
331 border: 0;
332 border-radius: 4px;
333 background: linear-gradient(var(--line), var(--line)) center / 100% 1px no-repeat;
334 color: var(--muted);
335 cursor: pointer;
336}
337.summary-toggle svg { padding: 0 6px; box-sizing: content-box; background: var(--page); border-radius: 4px; transition: transform 350ms var(--ease-overshoot); }
338.summary-toggle:hover { color: var(--text); }
339.summary-toggle .flipped { transform: rotate(180deg); }
340/* Negative margins leave room for card shadows the scroller would otherwise clip. */
341.list-body {
342 flex: 1;
343 min-height: 0;
344 overflow: auto;
345 display: flex;
346 flex-direction: column;
347 margin: 0 -8px;
348 padding: 2px 8px 24px;
349 transition: padding-bottom 200ms var(--ease-out);
350}
351.list-body > * { flex: none; }
352.list-body > .fill { flex: 1 1 0; min-height: 240px; max-height: none; }
353:is(.list-page, .list-body) > .segmented { align-self: flex-start; }
354.list-page > .segmented { margin-bottom: 12px; }
355/* Pinned headers draw their rule as a shadow, since a sticky cell's background covers the collapsed border. */
356:is(.list-body, .list-body > *) > table.data > thead th {
357 position: sticky;
358 top: 0;
359 z-index: 1;
360 background: var(--surface);
361 border-bottom: 0;
362 box-shadow: inset 0 -1px var(--line);
363}
364.list-body.flush { margin: 0 calc(-1 * var(--gutter)); padding: 0; }
365.list-body.flush > :not(.fill, .edge, table) { margin-inline: var(--gutter); }
366.list-body.flush > :last-child:not(.fill) { margin-bottom: 24px; }
367:is(.list-body.flush, .list-body.flush > :is(.fill, .edge)) > table.data > * > tr > :first-child { padding-left: var(--gutter); }
368:is(.list-body.flush, .list-body.flush > :is(.fill, .edge)) > table.data > * > tr > :last-child { padding-right: var(--gutter); }
369.list-body.flush > :is(table.data, :not(.card) > table.data) > thead th { background: var(--page); }
370/* Room for a toast, so it never covers the end of a list. */
371body:has(.toast) .list-body { padding-bottom: 64px; }
372
373.page-head {
374 display: flex;
375 align-items: center;
376 gap: 12px;
377 margin-bottom: 18px;
378 min-height: 36px;
379}
380
381.page-head h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }
382.page-head .sub { color: var(--muted); font-size: 13px; }
383
384h2 {
385 font-size: 14px;
386 font-weight: 650;
387 color: var(--accent);
388 margin: 28px 0 10px;
389}
390
391:is(td, dd, p) > a:not([class]), .page-head a.sub {
392 color: var(--accent);
393 text-decoration: underline dotted;
394 text-underline-offset: 3px;
395 border-radius: 3px;
396 transition: background-color 150ms;
397}
398
399:is(td, dd, p) > a:not([class]):hover, .page-head a.sub:hover {
400 text-decoration-style: solid;
401 background: var(--accent-wash);
402}
403
404.card {
405 background: var(--surface);
406 border: 1px solid var(--line);
407 border-radius: var(--radius-lg);
408 box-shadow: var(--shadow);
409 padding: 14px 16px;
410 min-width: 0;
411}
412
413.card-title { font-size: 13px; font-weight: 650; margin-bottom: 6px; display: flex; gap: 8px; align-items: baseline; }
414.card-title .sub { color: var(--muted); font-weight: 400; }
415.card-title .reading { font-weight: 500; font-variant-numeric: tabular-nums; }
416
417.grid { display: grid; gap: 8px; }
418.grid.charts { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
419.grid.tiles { grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); }
420
421.tile { padding: 12px 14px; }
422.tile .label { color: var(--text-2); font-size: 12px; }
423.tile .value { white-space: nowrap; font-size: 20px; font-weight: 700; letter-spacing: -0.01em; margin-top: 2px; font-variant-numeric: tabular-nums; }
424.tile .detail { color: var(--muted); font-size: 12px; margin-top: 2px; }
425
426.segmented {
427 position: relative;
428 display: inline-flex;
429 gap: 2px;
430 background: var(--well);
431 border: 1px solid var(--line);
432 border-radius: 10px;
433 padding: 2px;
434}
435
436/* One weight for every label, so choosing one never nudges its neighbours. */
437.segmented :is(button, a) {
438 position: relative;
439 display: inline-flex;
440 align-items: center;
441 gap: 5px;
442 border: 0;
443 background: none;
444 padding: 3px 10px;
445 border-radius: 7px;
446 color: var(--text-2);
447 cursor: pointer;
448 font-size: 12px;
449 font-weight: 550;
450 white-space: nowrap;
451 transition: background-color 200ms, color 200ms, transform 200ms var(--ease-overshoot);
452}
453.segmented > :is(button, a):hover { color: var(--text); background: var(--hover); }
454.segmented > :is(button, a):active { transform: scale(0.94); }
455.segmented :is(.pill, .ink) { position: absolute; pointer-events: none; }
456.segmented .pill {
457 z-index: 1;
458 top: 2px;
459 bottom: 2px;
460 border-radius: 7px;
461 background: var(--accent);
462 transition: left 300ms var(--ease-out), width 300ms var(--ease-out);
463}
464.segmented .ink { z-index: 2; inset: 0; display: flex; gap: 2px; padding: 2px; transition: clip-path 300ms var(--ease-out); }
465.segmented .ink > * { color: var(--on-accent); transition: none; }
466/* A tab's item count, after a space; `alarm` marks a tab whose count is trouble. */
467.segmented .count { opacity: 0.6; font-variant-numeric: tabular-nums; }
468.segmented > .alarm { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
469
470.button {
471 display: inline-flex;
472 align-items: center;
473 justify-content: center;
474 gap: 6px;
475 height: var(--control);
476 padding: 0 12px;
477 border-radius: var(--radius);
478 border: 2px solid var(--line);
479 background: var(--well);
480 cursor: pointer;
481 white-space: nowrap;
482 font-size: 13px;
483 font-weight: 500;
484 transition: background-color 150ms, border-color 150ms, color 150ms, transform 200ms var(--ease-overshoot);
485}
486
487.button:hover:not(:disabled) { background: var(--raised); border-color: var(--axis); }
488.button:active:not(:disabled) { transform: scale(0.95); }
489.button.danger { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
490.button.danger:hover:not(:disabled) {
491 background: color-mix(in srgb, var(--critical) 14%, transparent);
492 border-color: color-mix(in srgb, var(--critical) 60%, var(--line));
493}
494.button:disabled { opacity: 0.5; cursor: default; }
495.button[aria-busy="true"] { cursor: progress; }
496.button.icon-only { width: var(--control); padding: 0; border-radius: 99px; }
497
498/* The pending spinner slides in when a button gets aria-busy. */
499.button::before {
500 content: "";
501 flex: none;
502 width: 0;
503 height: 14px;
504 margin-right: -6px;
505 opacity: 0;
506 background: conic-gradient(transparent 15%, currentColor) right / 14px 14px no-repeat;
507 mask: radial-gradient(7px at calc(100% - 7px) 50%, transparent 5px, #000 5.5px 6.5px, transparent 7px);
508 transition: width 250ms var(--ease-out), margin 250ms var(--ease-out), opacity 200ms;
509}
510.button[aria-busy="true"]::before { width: 14px; margin-right: 0; opacity: 1; animation: spin 700ms linear infinite; }
511
512.button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
513.button.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 85%, var(--text)); border-color: transparent; }
514.button.primary.danger { background: var(--critical); border-color: var(--critical); color: #fff; }
515.button.primary.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--critical) 85%, #000); border-color: transparent; }
516
517kbd {
518 display: inline-grid;
519 place-items: center;
520 min-width: 18px;
521 height: 18px;
522 padding: 0 5px;
523 border-radius: 5px;
524 background: var(--raised);
525 box-shadow: inset 0 -1px 0 var(--line);
526 color: var(--muted);
527 font: 500 11px/1 var(--sans);
528}
529/* Inset 3px from the button's inner edge on top, bottom and right, with the radius shrunk to match. */
530.button kbd {
531 height: calc(var(--control) - 10px);
532 margin-right: -9px;
533 border-radius: calc(var(--radius) - 5px);
534 background: color-mix(in srgb, currentColor 16%, transparent);
535 box-shadow: none;
536 color: inherit;
537 opacity: 0.8;
538}
539
540/* A round icon link or button in a list row; it shows while the row is hovered or holds focus. */
541.row-icon {
542 display: inline-grid;
543 place-items: center;
544 flex: none;
545 justify-self: center;
546 width: 22px;
547 height: 22px;
548 padding: 0;
549 border: 0;
550 border-radius: 99px;
551 background: var(--raised);
552 color: var(--text-2);
553 cursor: pointer;
554 opacity: 0;
555 transition: opacity 150ms, background-color 150ms, color 150ms;
556}
557:is(tr, .service-row):is(:hover, :focus-within) .row-icon { opacity: 1; }
558.row-icon:hover { background: var(--accent); color: var(--on-accent); }
559/* In the sidebar the link fills its whole cell so it's easy to hit; the 20px circle is drawn behind the icon. */
560.service-row .row-icon { position: relative; isolation: isolate; width: 100%; height: 100%; background: none; }
561.service-row .row-icon::before {
562 content: "";
563 position: absolute;
564 inset: 50% auto auto 50%;
565 width: 20px;
566 height: 20px;
567 translate: -50% -50%;
568 border-radius: 99px;
569 background: var(--raised);
570 z-index: -1;
571 transition: background-color 150ms;
572}
573.service-row .row-icon:hover { background: none; }
574.service-row .row-icon:hover::before { background: var(--accent); }
575
576/* app icons --------------------------------------------------------------- */
577
578/* The way out to the real app a page wraps; it follows the page title. */
579.open-app {
580 --control: 24px;
581 display: inline-flex;
582 align-items: center;
583 gap: 5px;
584 height: 24px;
585 padding: 0 6px 0 4px;
586 border: 1px solid var(--line);
587 border-radius: 99px;
588 background: var(--surface);
589 color: var(--text-2);
590 font-size: 12px;
591 font-weight: 500;
592 white-space: nowrap;
593 transition: border-color 150ms, background-color 150ms, color 150ms;
594}
595.open-app:hover { border-color: var(--axis); background: var(--hover); color: var(--text); }
596.open-app :is(img, .monogram) { width: 16px; height: 16px; }
597.open-app .monogram { font-size: 9px; border-radius: 4px; }
598.open-app .arrow { color: var(--muted); }
599.open-app:hover .arrow { color: var(--text); }
600
601.monogram {
602 display: grid;
603 place-items: center;
604 border-radius: 22%;
605 font-weight: 700;
606 color: #fff;
607}
608
609.service-row .monogram { width: 16px; height: 16px; font-size: 9px; border-radius: 4px; }
610.service-row img { width: 16px; height: 16px; }
611picture { display: flex; }
612
613/* charts ------------------------------------------------------------------ */
614
615.chart { position: relative; }
616.chart .u-legend { font-size: 12px; color: var(--text-2); text-align: left; margin-top: 4px; }
617.chart .u-legend th { font-weight: 400; }
618.chart .u-legend .u-marker { width: 10px; height: 4px; border-radius: 2px; }
619.chart .u-legend .u-series > * { padding: 1px 6px 1px 0; }
620.chart .u-legend .u-value { font-variant-numeric: tabular-nums; color: var(--text); }
621.chart .u-legend .u-series:first-child { display: none; }
622.chart .u-cursor-x { border-right: 1px dashed var(--axis); }
623.chart .u-cursor-y { display: none; }
624
625.donut-row { display: flex; gap: 20px; align-items: center; }
626.legend { display: grid; gap: 6px; font-size: 13px; }
627.legend-item { display: grid; grid-template-columns: 10px 1fr auto; gap: 8px; align-items: center; color: var(--text-2); }
628.legend-item .swatch { width: 10px; height: 10px; border-radius: 3px; }
629.legend-item .v { color: var(--text); font-variant-numeric: tabular-nums; }
630
631/* tables & logs ----------------------------------------------------------- */
632
633table.data { width: 100%; border-collapse: collapse; font-size: 13px; }
634table.data th { text-align: left; font-weight: 500; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); }
635table.data th.num { text-align: right; }
636table.data td { padding: 6px 8px; border-bottom: 1px solid var(--grid); font-variant-numeric: tabular-nums; }
637table.data tbody tr { transition: background-color 150ms; }
638
639/* The shared text input and select. */
640.search {
641 width: 100%;
642 height: var(--control);
643 padding: 0 10px;
644 border-radius: var(--radius);
645 border: 2px solid var(--line);
646 background: var(--well);
647 color: var(--text);
648 font: inherit;
649 transition: border-color 150ms, box-shadow 150ms;
650}
651
652.search::placeholder { color: var(--muted); }
653/* A search field with a leading icon; the label is the box. */
654.search-box {
655 display: flex;
656 align-items: center;
657 gap: 8px;
658 height: var(--control);
659 padding: 0 10px;
660 border: 2px solid var(--line);
661 border-radius: var(--radius);
662 background: var(--well);
663 color: var(--muted);
664 transition: border-color 150ms, box-shadow 150ms;
665}
666.search-box:hover { border-color: var(--axis); }
667.search-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
668.search-box input { flex: 1; min-width: 0; height: 100%; border: 0; padding: 0; background: none; color: var(--text); font: inherit; outline: none; }
669.search-box input::placeholder { color: var(--muted); font-family: var(--sans); }
670.search:hover { border-color: var(--axis); }
671.search:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
672
673.logs {
674 font: 12px/1.55 var(--mono);
675 background: var(--surface);
676 border: 1px solid var(--line);
677 border-radius: var(--radius-lg);
678 box-shadow: var(--shadow);
679 padding: 8px 0;
680 margin-top: 8px;
681 max-height: 70vh;
682 overflow: auto;
683}
684
685.log { display: grid; grid-template-columns: 76px 1fr; gap: 12px; padding: 0 12px; white-space: pre-wrap; word-break: break-word; }
686.log:hover { background: var(--hover); }
687.log time { color: var(--muted); }
688.log.warn { background: color-mix(in srgb, var(--warning) 7%, transparent); }
689.log.error { background: color-mix(in srgb, var(--critical) 10%, transparent); color: inherit; }
690.log mark { background: var(--accent-wash); color: var(--accent); border-radius: 3px; }
691
692.empty { color: var(--muted); padding: 24px 0; text-align: center; }
693.empty.failed { display: grid; justify-items: center; gap: 10px; padding: 24px 12px; color: var(--text-2); }
694.empty.failed p { margin: 0; max-width: 60ch; }
695.empty.unconnected { display: grid; justify-items: center; gap: 8px; padding: 24px 12px; }
696.empty.unconnected svg { width: 20px; height: 20px; }
697.empty.unconnected p { margin: 0; max-width: 60ch; }
698.stale-note { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; grid-column: 1 / -1; margin: 0 0 8px; font-size: 12px; color: var(--text-2); }
699.metric .empty { height: 196px; display: grid; place-content: center; }
700.card.metric { padding: 10px 10px 4px 12px; }
701
702/* shared pieces ----------------------------------------------------------- */
703
704.spacer { flex: 1; }
705.muted { color: var(--muted); }
706.error { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
707.nowrap { white-space: nowrap; }
708.tile .warning { color: color-mix(in srgb, var(--warning) 70%, var(--text)); }
709.inline { display: inline-flex; align-items: center; gap: 6px; }
710.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
711/* Live numbers the stream stopped updating. */
712.tile.stale, .stale .spark { opacity: 0.45; transition: opacity 300ms; }
713.small { --control: 26px; }
714.reveal {
715 display: grid;
716 flex: none;
717 opacity: 0;
718 transition: grid-template-columns 200ms var(--ease-out), grid-template-rows 200ms var(--ease-out), opacity 200ms;
719}
720.reveal.x { grid-template-columns: 0fr; }
721.reveal.y { grid-template-rows: 0fr; }
722.reveal.shown { opacity: 1; }
723.reveal.x.shown { grid-template-columns: 1fr; }
724.reveal.y.shown { grid-template-rows: 1fr; }
725/* The clip margin keeps focus rings and press effects from being cut off. */
726.reveal > div { min-width: 0; min-height: 0; overflow: clip; overflow-clip-margin: 4px; }
727
728.sort {
729 display: inline-flex;
730 align-items: center;
731 gap: 4px;
732 border: 0;
733 background: none;
734 padding: 0;
735 color: inherit;
736 font-weight: inherit;
737 white-space: nowrap;
738 cursor: pointer;
739}
740.sort:hover { color: var(--text); }
741
742.meter { display: flex; gap: 2px; height: 6px; min-width: 80px; border-radius: 3px; background: var(--raised); overflow: hidden; }
743.meter span { background: var(--series-1); transition: width 300ms var(--ease-out); }
744.meter span.failed { background: var(--critical); }
745
746.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; font-size: 13px; }
747.kv dt { color: var(--muted); }
748.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
749.mono { font-family: var(--mono); font-size: 12px; }
750.chip {
751 display: inline-block;
752 padding: 0 8px;
753 border: 0;
754 border-radius: 99px;
755 background: var(--raised);
756 color: inherit;
757 font: 12px/20px var(--sans);
758 white-space: nowrap;
759 transition: background-color 150ms, color 150ms;
760}
761button.chip { cursor: pointer; }
762:is(a, button).chip:hover { background: var(--accent-wash); color: var(--accent); }
763.chip[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }
764.chip.warn { background: color-mix(in srgb, var(--warning) 16%, transparent); color: color-mix(in srgb, var(--warning) 65%, var(--text)); }
765.chips { display: flex; flex-wrap: wrap; gap: 4px; }
766
767/* Headline numbers as small pills beside the page title; what each one is lives in its tooltip. */
768.stats { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
769.stat {
770 display: inline-flex;
771 align-items: center;
772 gap: 5px;
773 height: 24px;
774 padding: 0 10px 0 8px;
775 border: 1px solid var(--line);
776 border-radius: 99px;
777 background: var(--surface);
778 color: var(--muted);
779 font-size: 12px;
780 white-space: nowrap;
781 font-variant-numeric: tabular-nums;
782}
783.stat b { color: var(--text); font-weight: 600; }
784.stat svg { flex: none; }
785.stat .status-label { gap: 5px; color: var(--text-2); }
786a.stat { transition: border-color 150ms, background-color 150ms; }
787a.stat:hover { border-color: var(--axis); background: var(--hover); }
788.stat .button { --control: 20px; margin-right: -8px; padding: 0 6px; border: 0; border-radius: 99px; font-size: 12px; }
789.stat .button.icon-only { width: 20px; padding: 0; }
790.stat-skeleton { height: 24px; border-radius: 99px; }
791
792.check input { accent-color: var(--accent); margin: 0; }
793.check:has(input:disabled) { cursor: progress; }
794
795/* Checkbox.tsx: the real input sits invisibly under the drawn box, which takes its states. */
796label.checkbox { position: relative; display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-2); cursor: pointer; }
797.checkbox input { position: absolute; left: 0; width: 15px; height: 15px; margin: 0; opacity: 0; pointer-events: none; }
798.checkbox .box {
799 flex: none;
800 display: grid;
801 place-items: center;
802 width: 15px;
803 height: 15px;
804 border: 1px solid var(--axis);
805 border-radius: 4px;
806 background: var(--surface);
807 color: var(--on-accent);
808 transition: background-color 150ms, border-color 150ms;
809}
810.checkbox .box svg { grid-area: 1 / 1; width: 11px; height: 11px; stroke-width: 3.5; opacity: 0; transform: scale(0.5); transition: opacity 150ms, transform 200ms var(--ease-overshoot); }
811.checkbox:hover .box { border-color: var(--muted); }
812.checkbox input:is(:checked, :indeterminate) + .box { border-color: var(--accent); background: var(--accent); }
813.checkbox input:checked:not(:indeterminate) + .box .tick, .checkbox input:indeterminate + .box .dash { opacity: 1; transform: none; }
814.checkbox input:focus-visible + .box { outline: 2px solid var(--focus); outline-offset: 2px; }
815.checkbox:has(input:disabled) { color: var(--muted); cursor: default; }
816.checkbox input:disabled + .box { opacity: 0.45; }
817
818/* Row buttons fill the row's height; the cell's 1px height lets the inner div resolve 100%. */
819td.actions { width: 1%; height: 1px; padding: 0; }
820td.actions > div { display: flex; justify-content: flex-end; height: 100%; }
821td.actions :is(button, a) {
822 display: inline-flex;
823 text-decoration: none;
824 align-items: center;
825 gap: 6px;
826 min-height: 36px;
827 padding: 0 14px;
828 border: 0;
829 background: none;
830 color: var(--accent);
831 font-size: 13px;
832 font-weight: 500;
833 white-space: nowrap;
834 cursor: pointer;
835 transition: background-color 150ms, color 150ms, opacity 150ms;
836}
837td.actions :is(button, a):hover:not(:disabled) { background: var(--raised); }
838td.actions :is(button, a).danger { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
839td.actions :is(button, a).danger:hover:not(:disabled) { background: color-mix(in srgb, var(--critical) 12%, transparent); }
840td.actions button:disabled { opacity: 0.45; cursor: default; }
841td.actions button[aria-busy="true"] { opacity: 1; cursor: progress; }
842td.actions button[aria-busy="true"]::before {
843 content: "";
844 width: 12px;
845 height: 12px;
846 border: 1.5px solid currentColor;
847 border-right-color: transparent;
848 border-radius: 50%;
849 animation: spin 700ms linear infinite;
850}
851td.actions :is(button, a):focus-visible { outline-offset: -2px; }
852.service-icon { display: grid; }
853.service-icon img, .service-icon .monogram { width: 32px; height: 32px; font-size: 16px; }
854
855.copy {
856 border: 0;
857 border-radius: 4px;
858 margin: 0 -3px;
859 padding: 0 3px;
860 background: none;
861 font: inherit;
862 text-align: inherit;
863 cursor: copy;
864 transition: background-color 150ms;
865}
866.copy:hover { background: var(--accent-wash); }
867
868.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
869
870.skeleton {
871 display: block;
872 min-height: 12px;
873 border-radius: var(--radius);
874 background: linear-gradient(100deg, var(--raised) 40%, var(--shimmer) 50%, var(--raised) 60%) 0 0 / 300% 100%;
875 opacity: 0.6;
876 animation: shimmer 1.6s ease-in-out infinite;
877}
878.row-skeleton { height: 14px; margin: 11px var(--gutter); }
879@keyframes shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
880
881/* overlays ---------------------------------------------------------------- */
882
883.dialog {
884 width: min(440px, calc(100% - 32px));
885 padding: 0;
886 border: 1px solid var(--line);
887 border-radius: var(--radius-lg);
888 background: var(--surface);
889 color: var(--text);
890 box-shadow: 0 24px 64px #0007;
891 animation: dialog-in 260ms var(--ease-overshoot);
892}
893.dialog::backdrop { background: #06061280; animation: fade-in 200ms; }
894.dialog.closing { animation: dialog-out 140ms ease-in forwards; }
895.dialog.closing::backdrop { animation: fade-in 140ms reverse forwards; }
896.dialog form { display: grid; gap: 12px; padding: 18px 20px 16px; }
897.dialog fieldset { display: grid; gap: 12px; min-width: 0; margin: 0; padding: 0; border: 0; }
898.dialog h2 { margin: 0; color: var(--text); font-size: 16px; }
899.dialog .description { min-width: 0; color: var(--text-2); font-size: 13.5px; }
900.dialog .description p { margin: 0 0 6px; }
901.dialog .description p:last-child { margin-bottom: 0; }
902.dialog .field { display: grid; gap: 5px; color: var(--text-2); font-size: 12px; }
903.dialog .field .hint { color: var(--muted); }
904.dialog .segmented { justify-self: start; }
905.dialog .row { display: grid; grid-auto-columns: 1fr; grid-auto-flow: column; gap: 10px; }
906.dialog .field select.search { padding-right: 4px; }
907.dialog .field .search:user-invalid { border-color: color-mix(in srgb, var(--critical) 60%, var(--line)); }
908.dialog .error { margin: 0; font-size: 13px; }
909.dialog .actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
910@keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.95); } }
911@keyframes dialog-out { to { opacity: 0; transform: scale(0.97); } }
912@keyframes fade-in { from { opacity: 0; } }
913
914.tooltip {
915 position: fixed;
916 inset: auto;
917 margin: 0;
918 border: 0;
919 overflow: visible;
920 pointer-events: none;
921 box-sizing: content-box;
922 width: max-content;
923 max-width: 300px;
924 padding: 5px 9px;
925 border-radius: 6px;
926 background: var(--text);
927 color: var(--page);
928 font-size: 12px;
929 line-height: 1.35;
930 text-align: center;
931 text-wrap: balance;
932 overflow-wrap: anywhere;
933 animation: tip-in 240ms var(--ease-overshoot);
934}
935.tooltip::after {
936 content: "";
937 position: absolute;
938 top: 100%;
939 left: var(--arrow);
940 width: 9px;
941 height: 9px;
942 border-radius: 2px;
943 background: inherit;
944 transform: translate(-50%, -60%) rotate(45deg);
945 z-index: -1;
946}
947.tooltip[data-side="bottom"] { animation-name: tip-in-below; }
948.tooltip[data-side="bottom"]::after { top: 0; transform: translate(-50%, -40%) rotate(45deg); }
949@keyframes tip-in { from { opacity: 0; transform: translateY(5px) scale(0.94); } }
950@keyframes tip-in-below { from { opacity: 0; transform: translateY(-5px) scale(0.94); } }
951
952.toasts {
953 position: fixed;
954 z-index: 50;
955 left: var(--sidebar);
956 right: 0;
957 bottom: 20px;
958 display: grid;
959 justify-items: center;
960 pointer-events: none;
961}
962.toast {
963 display: flex;
964 align-items: center;
965 gap: 12px;
966 max-width: min(640px, calc(100% - 32px));
967 padding: 6px 6px 6px 14px;
968 border: 1px solid var(--line);
969 border-radius: var(--radius);
970 background: var(--raised);
971 box-shadow: 0 8px 28px #0005;
972 font-size: 13px;
973 pointer-events: auto;
974 animation: toast-in 300ms var(--ease-overshoot);
975}
976.toast .close { display: grid; place-items: center; width: 24px; height: 24px; border: 0; border-radius: var(--radius); background: none; color: var(--muted); cursor: pointer; }
977.toast .close:hover { background: var(--hover); color: var(--text); }
978@keyframes toast-in { from { opacity: 0; transform: translateY(12px) scale(0.96); } }
979
980@media (prefers-reduced-motion: reduce) {
981 *, ::before, ::after {
982 animation-duration: 1ms !important;
983 animation-iteration-count: 1 !important;
984 transition-duration: 1ms !important;
985 }
986}
987
988/* TimeChart `inline`: values floating inside the plot instead of a legend. */
989.chart .values { position: absolute; inset: 0; pointer-events: none; font-size: 12px; font-variant-numeric: tabular-nums; }
990.chart .values > span {
991 position: absolute;
992 right: 4px;
993 display: flex;
994 align-items: center;
995 gap: 5px;
996 height: 16px;
997 padding: 0 5px;
998 border-radius: 4px;
999 background: color-mix(in srgb, var(--page) 78%, transparent);
1000 color: var(--text-2);
1001 white-space: nowrap;
1002 transform: translateY(-50%);
1003}
1004.chart .values > span::before { content: ""; width: 8px; height: 2px; border-radius: 1px; background: var(--key); }
1005.chart .values b { color: var(--text); font-weight: 600; }
1006.chart .values.hover { inset: 0 auto 0 auto; }
1007.chart .values.hover > span { right: auto; left: 8px; }
1008.chart .values.hover.flip > span { left: auto; right: 8px; }
1009.chart .values time {
1010 position: absolute;
1011 top: 100%;
1012 left: 0;
1013 margin-top: 3px;
1014 padding: 1px 6px;
1015 border-radius: 4px;
1016 background: var(--raised);
1017 color: var(--text);
1018 font-size: 11px;
1019 white-space: nowrap;
1020 transform: translateX(-50%);
1021}
1022
1023/* Charts stacked flush on one time axis and crosshair; each title sits in a gap in its divider. */
1024.strip-head { display: flex; align-items: center; gap: 8px; height: 24px; }
1025.strip-head::before, .strip-head .rule { content: ""; height: 0; border-top: 1px solid var(--line); }
1026.strip-head::before { width: 10px; }
1027.strip-head .rule { flex: 1; }
1028.strip-head h3 { margin: 0; color: var(--text-2); font-size: 13px; font-weight: 650; }
1029.strip-head .segmented { --control: 24px; font-size: 12px; }
1030.strip-head .now {
1031 display: inline-flex;
1032 align-items: center;
1033 gap: 3px;
1034 color: var(--accent);
1035 font-size: 18px;
1036 font-weight: 650;
1037 white-space: nowrap;
1038 transition: opacity 300ms;
1039}
1040.strip-head .now svg { color: var(--muted); }
1041.strip-head .now svg:not(:first-child) { margin-left: 8px; }
1042.strip-head .now.stale { opacity: 0.4; }
1043.strip-head .now .status-label { font-size: 14px; }
1044.strip .empty.failed { min-height: 64px; padding: 8px 0; }
1045