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 animation: rise 450ms var(--ease-out) backwards;
320}
321
322@keyframes rise { from { opacity: 0; transform: translateY(8px); } }
323
324.list-page { height: 100%; display: flex; flex-direction: column; padding-bottom: 0; }
325.list-page > * { flex: none; }
326.list-page .summary { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 350ms var(--ease-out), opacity 200ms; }
327.list-page .summary.open { grid-template-rows: 1fr; opacity: 1; }
328.list-page .summary > div { min-height: 0; overflow: hidden; }
329.summary-toggle {
330 display: grid;
331 place-items: center;
332 height: 16px;
333 margin: 4px 0;
334 border: 0;
335 border-radius: 4px;
336 background: linear-gradient(var(--line), var(--line)) center / 100% 1px no-repeat;
337 color: var(--muted);
338 cursor: pointer;
339}
340.summary-toggle svg { padding: 0 6px; box-sizing: content-box; background: var(--page); border-radius: 4px; transition: transform 350ms var(--ease-overshoot); }
341.summary-toggle:hover { color: var(--text); }
342.summary-toggle .flipped { transform: rotate(180deg); }
343/* Negative margins leave room for card shadows the scroller would otherwise clip. */
344.list-body {
345 flex: 1;
346 min-height: 0;
347 overflow: auto;
348 display: flex;
349 flex-direction: column;
350 margin: 0 -8px;
351 padding: 2px 8px 24px;
352 transition: padding-bottom 200ms var(--ease-out);
353}
354.list-body > * { flex: none; }
355.list-body > .fill { flex: 1 1 0; min-height: 240px; max-height: none; }
356:is(.list-page, .list-body) > .segmented { align-self: flex-start; }
357.list-page > .segmented { margin-bottom: 12px; }
358/* Pinned headers draw their rule as a shadow, since a sticky cell's background covers the collapsed border. */
359:is(.list-body, .list-body > *) > table.data > thead th {
360 position: sticky;
361 top: 0;
362 z-index: 1;
363 background: var(--surface);
364 border-bottom: 0;
365 box-shadow: inset 0 -1px var(--line);
366}
367.list-body.flush { margin: 0 calc(-1 * var(--gutter)); padding: 0; }
368.list-body.flush > :not(.fill, .edge, table) { margin-inline: var(--gutter); }
369.list-body.flush > :last-child:not(.fill) { margin-bottom: 24px; }
370:is(.list-body.flush, .list-body.flush > :is(.fill, .edge)) > table.data > * > tr > :first-child { padding-left: var(--gutter); }
371:is(.list-body.flush, .list-body.flush > :is(.fill, .edge)) > table.data > * > tr > :last-child { padding-right: var(--gutter); }
372.list-body.flush > :is(table.data, :not(.card) > table.data) > thead th { background: var(--page); }
373/* Room for a toast, so it never covers the end of a list. */
374body:has(.toast) .list-body { padding-bottom: 64px; }
375
376.page-head {
377 display: flex;
378 align-items: center;
379 gap: 12px;
380 margin-bottom: 18px;
381 min-height: 36px;
382}
383
384.page-head h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }
385.page-head .sub { color: var(--muted); font-size: 13px; }
386
387h2 {
388 font-size: 14px;
389 font-weight: 650;
390 color: var(--accent);
391 margin: 28px 0 10px;
392}
393
394:is(td, dd, p) > a:not([class]), .page-head a.sub {
395 color: var(--accent);
396 text-decoration: underline dotted;
397 text-underline-offset: 3px;
398 border-radius: 3px;
399 transition: background-color 150ms;
400}
401
402:is(td, dd, p) > a:not([class]):hover, .page-head a.sub:hover {
403 text-decoration-style: solid;
404 background: var(--accent-wash);
405}
406
407.card {
408 background: var(--surface);
409 border: 1px solid var(--line);
410 border-radius: var(--radius-lg);
411 box-shadow: var(--shadow);
412 padding: 14px 16px;
413 min-width: 0;
414}
415
416.card-title { font-size: 13px; font-weight: 650; margin-bottom: 6px; display: flex; gap: 8px; align-items: baseline; }
417.card-title .sub { color: var(--muted); font-weight: 400; }
418.card-title .reading { font-weight: 500; font-variant-numeric: tabular-nums; }
419
420.grid { display: grid; gap: 8px; }
421.grid.charts { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
422.grid.tiles { grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); }
423
424.tile { padding: 12px 14px; }
425.tile .label { color: var(--text-2); font-size: 12px; }
426.tile .value { white-space: nowrap; font-size: 20px; font-weight: 700; letter-spacing: -0.01em; margin-top: 2px; font-variant-numeric: tabular-nums; }
427.tile .detail { color: var(--muted); font-size: 12px; margin-top: 2px; }
428
429.segmented {
430 position: relative;
431 display: inline-flex;
432 gap: 2px;
433 background: var(--well);
434 border: 1px solid var(--line);
435 border-radius: 10px;
436 padding: 2px;
437}
438
439/* One weight for every label, so choosing one never nudges its neighbours. */
440.segmented :is(button, a) {
441 position: relative;
442 display: inline-flex;
443 align-items: center;
444 gap: 5px;
445 border: 0;
446 background: none;
447 padding: 3px 10px;
448 border-radius: 7px;
449 color: var(--text-2);
450 cursor: pointer;
451 font-size: 12px;
452 font-weight: 550;
453 white-space: nowrap;
454 transition: background-color 200ms, color 200ms, transform 200ms var(--ease-overshoot);
455}
456.segmented > :is(button, a):hover { color: var(--text); background: var(--hover); }
457.segmented > :is(button, a):active { transform: scale(0.94); }
458.segmented :is(.pill, .ink) { position: absolute; pointer-events: none; }
459.segmented .pill {
460 z-index: 1;
461 top: 2px;
462 bottom: 2px;
463 border-radius: 7px;
464 background: var(--accent);
465 transition: left 300ms var(--ease-out), width 300ms var(--ease-out);
466}
467.segmented .ink { z-index: 2; inset: 0; display: flex; gap: 2px; padding: 2px; transition: clip-path 300ms var(--ease-out); }
468.segmented .ink > * { color: var(--on-accent); transition: none; }
469/* A tab's item count, after a space; `alarm` marks a tab whose count is trouble. */
470.segmented .count { opacity: 0.6; font-variant-numeric: tabular-nums; }
471.segmented > .alarm { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
472
473.button {
474 display: inline-flex;
475 align-items: center;
476 justify-content: center;
477 gap: 6px;
478 height: var(--control);
479 padding: 0 12px;
480 border-radius: var(--radius);
481 border: 2px solid var(--line);
482 background: var(--well);
483 cursor: pointer;
484 white-space: nowrap;
485 font-size: 13px;
486 font-weight: 500;
487 transition: background-color 150ms, border-color 150ms, color 150ms, transform 200ms var(--ease-overshoot);
488}
489
490.button:hover:not(:disabled) { background: var(--raised); border-color: var(--axis); }
491.button:active:not(:disabled) { transform: scale(0.95); }
492.button.danger { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
493.button.danger:hover:not(:disabled) {
494 background: color-mix(in srgb, var(--critical) 14%, transparent);
495 border-color: color-mix(in srgb, var(--critical) 60%, var(--line));
496}
497.button:disabled { opacity: 0.5; cursor: default; }
498.button[aria-busy="true"] { cursor: progress; }
499.button.icon-only { width: var(--control); padding: 0; border-radius: 99px; }
500
501/* The pending spinner slides in when a button gets aria-busy. */
502.button::before {
503 content: "";
504 flex: none;
505 width: 0;
506 height: 14px;
507 margin-right: -6px;
508 opacity: 0;
509 background: conic-gradient(transparent 15%, currentColor) right / 14px 14px no-repeat;
510 mask: radial-gradient(7px at calc(100% - 7px) 50%, transparent 5px, #000 5.5px 6.5px, transparent 7px);
511 transition: width 250ms var(--ease-out), margin 250ms var(--ease-out), opacity 200ms;
512}
513.button[aria-busy="true"]::before { width: 14px; margin-right: 0; opacity: 1; animation: spin 700ms linear infinite; }
514
515.button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
516.button.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 85%, var(--text)); border-color: transparent; }
517.button.primary.danger { background: var(--critical); border-color: var(--critical); color: #fff; }
518.button.primary.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--critical) 85%, #000); border-color: transparent; }
519
520kbd {
521 display: inline-grid;
522 place-items: center;
523 min-width: 18px;
524 height: 18px;
525 padding: 0 5px;
526 border-radius: 5px;
527 background: var(--raised);
528 box-shadow: inset 0 -1px 0 var(--line);
529 color: var(--muted);
530 font: 500 11px/1 var(--sans);
531}
532/* Inset 3px from the button's inner edge on top, bottom and right, with the radius shrunk to match. */
533.button kbd {
534 height: calc(var(--control) - 10px);
535 margin-right: -9px;
536 border-radius: calc(var(--radius) - 5px);
537 background: color-mix(in srgb, currentColor 16%, transparent);
538 box-shadow: none;
539 color: inherit;
540 opacity: 0.8;
541}
542
543/* A round icon link or button in a list row; it shows while the row is hovered or holds focus. */
544.row-icon {
545 display: inline-grid;
546 place-items: center;
547 flex: none;
548 justify-self: center;
549 width: 22px;
550 height: 22px;
551 padding: 0;
552 border: 0;
553 border-radius: 99px;
554 background: var(--raised);
555 color: var(--text-2);
556 cursor: pointer;
557 opacity: 0;
558 transition: opacity 150ms, background-color 150ms, color 150ms;
559}
560:is(tr, .service-row):is(:hover, :focus-within) .row-icon { opacity: 1; }
561.row-icon:hover { background: var(--accent); color: var(--on-accent); }
562/* In the sidebar the link fills its whole cell so it's easy to hit; the 20px circle is drawn behind the icon. */
563.service-row .row-icon { position: relative; isolation: isolate; width: 100%; height: 100%; background: none; }
564.service-row .row-icon::before {
565 content: "";
566 position: absolute;
567 inset: 50% auto auto 50%;
568 width: 20px;
569 height: 20px;
570 translate: -50% -50%;
571 border-radius: 99px;
572 background: var(--raised);
573 z-index: -1;
574 transition: background-color 150ms;
575}
576.service-row .row-icon:hover { background: none; }
577.service-row .row-icon:hover::before { background: var(--accent); }
578
579/* app icons --------------------------------------------------------------- */
580
581/* The way out to the real app a page wraps; it follows the page title. */
582.open-app {
583 --control: 24px;
584 display: inline-flex;
585 align-items: center;
586 gap: 5px;
587 height: 24px;
588 padding: 0 6px 0 4px;
589 border: 1px solid var(--line);
590 border-radius: 99px;
591 background: var(--surface);
592 color: var(--text-2);
593 font-size: 12px;
594 font-weight: 500;
595 white-space: nowrap;
596 transition: border-color 150ms, background-color 150ms, color 150ms;
597}
598.open-app:hover { border-color: var(--axis); background: var(--hover); color: var(--text); }
599.open-app :is(img, .monogram) { width: 16px; height: 16px; }
600.open-app .monogram { font-size: 9px; border-radius: 4px; }
601.open-app .arrow { color: var(--muted); }
602.open-app:hover .arrow { color: var(--text); }
603
604.monogram {
605 display: grid;
606 place-items: center;
607 border-radius: 22%;
608 font-weight: 700;
609 color: #fff;
610}
611
612.service-row .monogram { width: 16px; height: 16px; font-size: 9px; border-radius: 4px; }
613.service-row img { width: 16px; height: 16px; }
614picture { display: flex; }
615
616/* charts ------------------------------------------------------------------ */
617
618.chart { position: relative; }
619.chart .u-legend { font-size: 12px; color: var(--text-2); text-align: left; margin-top: 4px; }
620.chart .u-legend th { font-weight: 400; }
621.chart .u-legend .u-marker { width: 10px; height: 4px; border-radius: 2px; }
622.chart .u-legend .u-series > * { padding: 1px 6px 1px 0; }
623.chart .u-legend .u-value { font-variant-numeric: tabular-nums; color: var(--text); }
624.chart .u-legend .u-series:first-child { display: none; }
625.chart .u-cursor-x { border-right: 1px dashed var(--axis); }
626.chart .u-cursor-y { display: none; }
627
628.donut-row { display: flex; gap: 20px; align-items: center; }
629.legend { display: grid; gap: 6px; font-size: 13px; }
630.legend-item { display: grid; grid-template-columns: 10px 1fr auto; gap: 8px; align-items: center; color: var(--text-2); }
631.legend-item .swatch { width: 10px; height: 10px; border-radius: 3px; }
632.legend-item .v { color: var(--text); font-variant-numeric: tabular-nums; }
633
634/* tables & logs ----------------------------------------------------------- */
635
636table.data { width: 100%; border-collapse: collapse; font-size: 13px; }
637table.data th { text-align: left; font-weight: 500; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); }
638table.data th.num { text-align: right; }
639table.data td { padding: 6px 8px; border-bottom: 1px solid var(--grid); font-variant-numeric: tabular-nums; }
640table.data tbody tr { transition: background-color 150ms; }
641
642/* The shared text input and select. */
643.search {
644 width: 100%;
645 height: var(--control);
646 padding: 0 10px;
647 border-radius: var(--radius);
648 border: 2px solid var(--line);
649 background: var(--well);
650 color: var(--text);
651 font: inherit;
652 transition: border-color 150ms, box-shadow 150ms;
653}
654
655.search::placeholder { color: var(--muted); }
656/* A search field with a leading icon; the label is the box. */
657.search-box {
658 display: flex;
659 align-items: center;
660 gap: 8px;
661 height: var(--control);
662 padding: 0 10px;
663 border: 2px solid var(--line);
664 border-radius: var(--radius);
665 background: var(--well);
666 color: var(--muted);
667 transition: border-color 150ms, box-shadow 150ms;
668}
669.search-box:hover { border-color: var(--axis); }
670.search-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
671.search-box input { flex: 1; min-width: 0; height: 100%; border: 0; padding: 0; background: none; color: var(--text); font: inherit; outline: none; }
672.search-box input::placeholder { color: var(--muted); font-family: var(--sans); }
673.search:hover { border-color: var(--axis); }
674.search:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
675
676.logs {
677 font: 12px/1.55 var(--mono);
678 background: var(--surface);
679 border: 1px solid var(--line);
680 border-radius: var(--radius-lg);
681 box-shadow: var(--shadow);
682 padding: 8px 0;
683 margin-top: 8px;
684 max-height: 70vh;
685 overflow: auto;
686}
687
688.log { display: grid; grid-template-columns: 76px 1fr; gap: 12px; padding: 0 12px; white-space: pre-wrap; word-break: break-word; }
689.log:hover { background: var(--hover); }
690.log time { color: var(--muted); }
691.log.warn { background: color-mix(in srgb, var(--warning) 7%, transparent); }
692.log.error { background: color-mix(in srgb, var(--critical) 10%, transparent); color: inherit; }
693.log mark { background: var(--accent-wash); color: var(--accent); border-radius: 3px; }
694
695.empty { color: var(--muted); padding: 24px 0; text-align: center; }
696.empty.failed { display: grid; justify-items: center; gap: 10px; padding: 24px 12px; color: var(--text-2); }
697.empty.failed p { margin: 0; max-width: 60ch; }
698.empty.unconnected { display: grid; justify-items: center; gap: 8px; padding: 24px 12px; }
699.empty.unconnected svg { width: 20px; height: 20px; }
700.empty.unconnected p { margin: 0; max-width: 60ch; }
701.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); }
702.metric .empty { height: 196px; display: grid; place-content: center; }
703.card.metric { padding: 10px 10px 4px 12px; }
704
705/* shared pieces ----------------------------------------------------------- */
706
707.spacer { flex: 1; }
708.muted { color: var(--muted); }
709.error { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
710.nowrap { white-space: nowrap; }
711.tile .warning { color: color-mix(in srgb, var(--warning) 70%, var(--text)); }
712.inline { display: inline-flex; align-items: center; gap: 6px; }
713.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
714/* Live numbers the stream stopped updating. */
715.tile.stale, .stale .spark { opacity: 0.45; transition: opacity 300ms; }
716.small { --control: 26px; }
717.reveal {
718 display: grid;
719 flex: none;
720 opacity: 0;
721 transition: grid-template-columns 200ms var(--ease-out), grid-template-rows 200ms var(--ease-out), opacity 200ms;
722}
723.reveal.x { grid-template-columns: 0fr; }
724.reveal.y { grid-template-rows: 0fr; }
725.reveal.shown { opacity: 1; }
726.reveal.x.shown { grid-template-columns: 1fr; }
727.reveal.y.shown { grid-template-rows: 1fr; }
728/* The clip margin keeps focus rings and press effects from being cut off. */
729.reveal > div { min-width: 0; min-height: 0; overflow: clip; overflow-clip-margin: 4px; }
730
731.sort {
732 display: inline-flex;
733 align-items: center;
734 gap: 4px;
735 border: 0;
736 background: none;
737 padding: 0;
738 color: inherit;
739 font-weight: inherit;
740 white-space: nowrap;
741 cursor: pointer;
742}
743.sort:hover { color: var(--text); }
744
745.meter { display: flex; gap: 2px; height: 6px; min-width: 80px; border-radius: 3px; background: var(--raised); overflow: hidden; }
746.meter span { background: var(--series-1); transition: width 300ms var(--ease-out); }
747.meter span.failed { background: var(--critical); }
748
749.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; font-size: 13px; }
750.kv dt { color: var(--muted); }
751.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
752.mono { font-family: var(--mono); font-size: 12px; }
753.chip {
754 display: inline-block;
755 padding: 0 8px;
756 border: 0;
757 border-radius: 99px;
758 background: var(--raised);
759 color: inherit;
760 font: 12px/20px var(--sans);
761 white-space: nowrap;
762 transition: background-color 150ms, color 150ms;
763}
764button.chip { cursor: pointer; }
765:is(a, button).chip:hover { background: var(--accent-wash); color: var(--accent); }
766.chip[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }
767.chip.warn { background: color-mix(in srgb, var(--warning) 16%, transparent); color: color-mix(in srgb, var(--warning) 65%, var(--text)); }
768.chips { display: flex; flex-wrap: wrap; gap: 4px; }
769
770/* Headline numbers as small pills beside the page title; what each one is lives in its tooltip. */
771.stats { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
772.stat {
773 display: inline-flex;
774 align-items: center;
775 gap: 5px;
776 height: 24px;
777 padding: 0 10px 0 8px;
778 border: 1px solid var(--line);
779 border-radius: 99px;
780 background: var(--surface);
781 color: var(--muted);
782 font-size: 12px;
783 white-space: nowrap;
784 font-variant-numeric: tabular-nums;
785}
786.stat b { color: var(--text); font-weight: 600; }
787.stat svg { flex: none; }
788.stat .status-label { gap: 5px; color: var(--text-2); }
789a.stat { transition: border-color 150ms, background-color 150ms; }
790a.stat:hover { border-color: var(--axis); background: var(--hover); }
791.stat .button { --control: 20px; margin-right: -8px; padding: 0 6px; border: 0; border-radius: 99px; font-size: 12px; }
792.stat .button.icon-only { width: 20px; padding: 0; }
793.stat-skeleton { height: 24px; border-radius: 99px; }
794
795.check input { accent-color: var(--accent); margin: 0; }
796.check:has(input:disabled) { cursor: progress; }
797
798/* Checkbox.tsx: the real input sits invisibly under the drawn box, which takes its states. */
799label.checkbox { position: relative; display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-2); cursor: pointer; }
800.checkbox input { position: absolute; left: 0; width: 15px; height: 15px; margin: 0; opacity: 0; pointer-events: none; }
801.checkbox .box {
802 flex: none;
803 display: grid;
804 place-items: center;
805 width: 15px;
806 height: 15px;
807 border: 1px solid var(--axis);
808 border-radius: 4px;
809 background: var(--surface);
810 color: var(--on-accent);
811 transition: background-color 150ms, border-color 150ms;
812}
813.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); }
814.checkbox:hover .box { border-color: var(--muted); }
815.checkbox input:is(:checked, :indeterminate) + .box { border-color: var(--accent); background: var(--accent); }
816.checkbox input:checked:not(:indeterminate) + .box .tick, .checkbox input:indeterminate + .box .dash { opacity: 1; transform: none; }
817.checkbox input:focus-visible + .box { outline: 2px solid var(--focus); outline-offset: 2px; }
818.checkbox:has(input:disabled) { color: var(--muted); cursor: default; }
819.checkbox input:disabled + .box { opacity: 0.45; }
820
821/* Row buttons fill the row's height; the cell's 1px height lets the inner div resolve 100%. */
822td.actions { width: 1%; height: 1px; padding: 0; }
823td.actions > div { display: flex; justify-content: flex-end; height: 100%; }
824td.actions :is(button, a) {
825 display: inline-flex;
826 text-decoration: none;
827 align-items: center;
828 gap: 6px;
829 min-height: 36px;
830 padding: 0 14px;
831 border: 0;
832 background: none;
833 color: var(--accent);
834 font-size: 13px;
835 font-weight: 500;
836 white-space: nowrap;
837 cursor: pointer;
838 transition: background-color 150ms, color 150ms, opacity 150ms;
839}
840td.actions :is(button, a):hover:not(:disabled) { background: var(--raised); }
841td.actions :is(button, a).danger { color: color-mix(in srgb, var(--critical) 80%, var(--text)); }
842td.actions :is(button, a).danger:hover:not(:disabled) { background: color-mix(in srgb, var(--critical) 12%, transparent); }
843td.actions button:disabled { opacity: 0.45; cursor: default; }
844td.actions button[aria-busy="true"] { opacity: 1; cursor: progress; }
845td.actions button[aria-busy="true"]::before {
846 content: "";
847 width: 12px;
848 height: 12px;
849 border: 1.5px solid currentColor;
850 border-right-color: transparent;
851 border-radius: 50%;
852 animation: spin 700ms linear infinite;
853}
854td.actions :is(button, a):focus-visible { outline-offset: -2px; }
855.service-icon { display: grid; }
856.service-icon img, .service-icon .monogram { width: 32px; height: 32px; font-size: 16px; }
857
858.copy {
859 border: 0;
860 border-radius: 4px;
861 margin: 0 -3px;
862 padding: 0 3px;
863 background: none;
864 font: inherit;
865 text-align: inherit;
866 cursor: copy;
867 transition: background-color 150ms;
868}
869.copy:hover { background: var(--accent-wash); }
870
871.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
872
873.skeleton {
874 display: block;
875 min-height: 12px;
876 border-radius: var(--radius);
877 background: linear-gradient(100deg, var(--raised) 40%, var(--shimmer) 50%, var(--raised) 60%) 0 0 / 300% 100%;
878 opacity: 0.6;
879 animation: shimmer 1.6s ease-in-out infinite;
880}
881.row-skeleton { height: 14px; margin: 11px var(--gutter); }
882@keyframes shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
883
884/* overlays ---------------------------------------------------------------- */
885
886.dialog {
887 width: min(440px, calc(100% - 32px));
888 padding: 0;
889 border: 1px solid var(--line);
890 border-radius: var(--radius-lg);
891 background: var(--surface);
892 color: var(--text);
893 box-shadow: 0 24px 64px #0007;
894 animation: dialog-in 260ms var(--ease-overshoot);
895}
896.dialog::backdrop { background: #06061280; animation: fade-in 200ms; }
897.dialog.closing { animation: dialog-out 140ms ease-in forwards; }
898.dialog.closing::backdrop { animation: fade-in 140ms reverse forwards; }
899.dialog form { display: grid; gap: 12px; padding: 18px 20px 16px; }
900.dialog h2 { margin: 0; color: var(--text); font-size: 16px; }
901.dialog .description { min-width: 0; color: var(--text-2); font-size: 13.5px; }
902.dialog .description p { margin: 0 0 6px; }
903.dialog .description p:last-child { margin-bottom: 0; }
904.dialog .field { display: grid; gap: 5px; color: var(--text-2); font-size: 12px; }
905.dialog .field .hint { color: var(--muted); }
906.dialog .segmented { justify-self: start; }
907.dialog .row { display: grid; grid-auto-columns: 1fr; grid-auto-flow: column; gap: 10px; }
908.dialog .field select.search { padding-right: 4px; }
909.dialog .field .search:user-invalid { border-color: color-mix(in srgb, var(--critical) 60%, var(--line)); }
910.dialog .error { margin: 0; font-size: 13px; }
911.dialog .actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
912@keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.95); } }
913@keyframes dialog-out { to { opacity: 0; transform: scale(0.97); } }
914@keyframes fade-in { from { opacity: 0; } }
915
916.tooltip {
917 position: fixed;
918 inset: auto;
919 margin: 0;
920 border: 0;
921 overflow: visible;
922 pointer-events: none;
923 box-sizing: content-box;
924 width: max-content;
925 max-width: 300px;
926 padding: 5px 9px;
927 border-radius: 6px;
928 background: var(--text);
929 color: var(--page);
930 font-size: 12px;
931 line-height: 1.35;
932 text-align: center;
933 text-wrap: balance;
934 overflow-wrap: anywhere;
935 animation: tip-in 240ms var(--ease-overshoot);
936}
937.tooltip::after {
938 content: "";
939 position: absolute;
940 top: 100%;
941 left: var(--arrow);
942 width: 9px;
943 height: 9px;
944 border-radius: 2px;
945 background: inherit;
946 transform: translate(-50%, -60%) rotate(45deg);
947 z-index: -1;
948}
949.tooltip[data-side="bottom"] { animation-name: tip-in-below; }
950.tooltip[data-side="bottom"]::after { top: 0; transform: translate(-50%, -40%) rotate(45deg); }
951@keyframes tip-in { from { opacity: 0; transform: translateY(5px) scale(0.94); } }
952@keyframes tip-in-below { from { opacity: 0; transform: translateY(-5px) scale(0.94); } }
953
954.toasts {
955 position: fixed;
956 z-index: 50;
957 left: var(--sidebar);
958 right: 0;
959 bottom: 20px;
960 display: grid;
961 justify-items: center;
962 pointer-events: none;
963}
964.toast {
965 display: flex;
966 align-items: center;
967 gap: 12px;
968 max-width: min(640px, calc(100% - 32px));
969 padding: 6px 6px 6px 14px;
970 border: 1px solid var(--line);
971 border-radius: var(--radius);
972 background: var(--raised);
973 box-shadow: 0 8px 28px #0005;
974 font-size: 13px;
975 pointer-events: auto;
976 animation: toast-in 300ms var(--ease-overshoot);
977}
978.toast .close { display: grid; place-items: center; width: 24px; height: 24px; border: 0; border-radius: var(--radius); background: none; color: var(--muted); cursor: pointer; }
979.toast .close:hover { background: var(--hover); color: var(--text); }
980@keyframes toast-in { from { opacity: 0; transform: translateY(12px) scale(0.96); } }
981
982@media (prefers-reduced-motion: reduce) {
983 *, ::before, ::after {
984 animation-duration: 1ms !important;
985 animation-iteration-count: 1 !important;
986 transition-duration: 1ms !important;
987 }
988}
989
990/* TimeChart `inline`: values floating inside the plot instead of a legend. */
991.chart .values { position: absolute; inset: 0; pointer-events: none; font-size: 12px; font-variant-numeric: tabular-nums; }
992.chart .values > span {
993 position: absolute;
994 right: 4px;
995 display: flex;
996 align-items: center;
997 gap: 5px;
998 height: 16px;
999 padding: 0 5px;
1000 border-radius: 4px;
1001 background: color-mix(in srgb, var(--page) 78%, transparent);
1002 color: var(--text-2);
1003 white-space: nowrap;
1004 transform: translateY(-50%);
1005}
1006.chart .values > span::before { content: ""; width: 8px; height: 2px; border-radius: 1px; background: var(--key); }
1007.chart .values b { color: var(--text); font-weight: 600; }
1008.chart .values.hover { inset: 0 auto 0 auto; }
1009.chart .values.hover > span { right: auto; left: 8px; }
1010.chart .values.hover.flip > span { left: auto; right: 8px; }
1011.chart .values time {
1012 position: absolute;
1013 top: 100%;
1014 left: 0;
1015 margin-top: 3px;
1016 padding: 1px 6px;
1017 border-radius: 4px;
1018 background: var(--raised);
1019 color: var(--text);
1020 font-size: 11px;
1021 white-space: nowrap;
1022 transform: translateX(-50%);
1023}
1024
1025/* Charts stacked flush on one time axis and crosshair; each title sits in a gap in its divider. */
1026.strip-head { display: flex; align-items: center; gap: 8px; height: 24px; }
1027.strip-head::before, .strip-head .rule { content: ""; height: 0; border-top: 1px solid var(--line); }
1028.strip-head::before { width: 10px; }
1029.strip-head .rule { flex: 1; }
1030.strip-head h3 { margin: 0; color: var(--text-2); font-size: 13px; font-weight: 650; }
1031.strip-head .segmented { --control: 24px; font-size: 12px; }
1032.strip-head .now {
1033 display: inline-flex;
1034 align-items: center;
1035 gap: 3px;
1036 color: var(--accent);
1037 font-size: 18px;
1038 font-weight: 650;
1039 white-space: nowrap;
1040 transition: opacity 300ms;
1041}
1042.strip-head .now svg { color: var(--muted); }
1043.strip-head .now svg:not(:first-child) { margin-left: 8px; }
1044.strip-head .now.stale { opacity: 0.4; }
1045.strip-head .now .status-label { font-size: 14px; }
1046.strip .empty.failed { min-height: 64px; padding: 8px 0; }
1047