Files
memby/admin-ui/src/styles.css
T

2896 lines
61 KiB
CSS
Raw Normal View History

2026-08-14 09:40:03 +12:00
/* One vocabulary for the whole console.
Every screen is built from the components below — card, tile, field, tag, table, list.
A page that needs a look of its own is a missing component, not a licence for a style
attribute: there are no inline styles in the page components, on purpose, and no CSS
modules either. One stylesheet, read top to bottom, is what a strict no-dependency
console has instead of a design system nobody can see the whole of.
The palette takes its cues from GitHub's dark interface: near-black chrome, a cool
charcoal work area, restrained borders and one deliberate green for live or primary
actions. An operator can scan dense operational data without the interface competing
for attention.
The console is darker than the television app rather than lighter. What makes a card
hard to find is not the darkness of the page behind it but the absence of anything
*between* them — so the page drops to near-black and the separation is done properly,
by a card that sits a measurable step above it and a hairline visible on a bad monitor.
Four steps, and they are the whole scale: page, card, raised (a control), high (a
control under the pointer). A fifth value is a decision that has not been made yet.
The accent is spent only on what is live, selected or primary. Beside it are three
secondary tones with no verdict attached — info (blue), note (violet), data (teal) —
which is what lets a page distinguish one kind of thing from another without every
coloured element reading as a warning. Green means good, amber means look, red means
wrong; the other three mean nothing at all, which is the point. A tone is passed, never
derived: the library is teal, a person violet, a television blue, on every page. */
:root {
color-scheme: dark;
2026-08-15 09:23:26 +12:00
--oled: #000;
--bg: #080b10;
--rail-bg: var(--oled);
2026-08-14 09:40:03 +12:00
--top-bg: rgba(1, 4, 9, 0.94);
--surface: #0d1117;
--surface-lift: #161b22;
--surface-hi: #21262d;
--line: #30363d;
--line-soft: #21262d;
--text: #f0f6fc;
--muted: #b1bac4;
--quiet: #7d8590;
--accent: #2ea043;
--accent-ink: #56d364;
--accent-wash: rgba(46, 160, 67, 0.16);
--danger: #e5534b;
--danger-ink: #ff9b94;
--danger-wash: rgba(229, 83, 75, 0.13);
--warn: #e0ad4e;
--warn-ink: #f2cb78;
--warn-wash: rgba(239, 196, 107, 0.13);
--info: #4f9cd8;
--info-ink: #93cbef;
--info-wash: rgba(79, 156, 216, 0.14);
--note: #a07ce8;
--note-ink: #bda1f5;
--note-wash: rgba(160, 124, 232, 0.14);
--data: #3fc2b6;
--data-ink: #6fdcd0;
--data-wash: rgba(63, 194, 182, 0.13);
--rail: 236px;
2026-08-15 09:23:26 +12:00
--safe-top: env(safe-area-inset-top, 0px);
--safe-right: env(safe-area-inset-right, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
--safe-left: env(safe-area-inset-left, 0px);
--top: calc(58px + var(--safe-top));
2026-08-14 09:40:03 +12:00
--radius: 8px;
--radius-sm: 6px;
--radius-xs: 4px;
--sans: "Inter Variable", Inter, "Segoe UI", sans-serif;
--mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
* {
box-sizing: border-box;
}
[hidden] {
display: none !important;
}
body {
margin: 0;
2026-08-15 09:23:26 +12:00
background:
radial-gradient(circle at 72% -20%, rgba(63, 194, 182, 0.045), transparent 38rem),
var(--bg);
2026-08-14 09:40:03 +12:00
color: var(--text);
font: 16.5px/1.55 var(--sans);
font-weight: 450;
letter-spacing: 0;
-webkit-font-smoothing: antialiased;
2026-08-15 09:23:26 +12:00
min-width: 320px;
min-height: 100dvh;
background: var(--bg);
-webkit-tap-highlight-color: transparent;
2026-08-14 09:40:03 +12:00
}
a {
color: var(--accent-ink);
}
/* Minimal animation, and none of it decorative. The only movement in the console is a
thing appearing (a toast, a panel) and the one spinner that says a request is in
flight. Anybody honouring the platform's reduce-motion setting gets neither. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
.skip {
position: absolute;
left: -9999px;
top: 8px;
z-index: 60;
padding: 8px 14px;
border-radius: var(--radius-sm);
background: var(--surface-lift);
color: var(--text);
text-decoration: none;
}
.skip:focus {
left: 8px;
}
/* Focus is visible everywhere. The console is driven from a keyboard as much as a mouse,
and a focus ring that only some controls have is worse than none. */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--radius-xs);
}
/* ---------- top bar ---------- */
/* The bar spans the whole width above both the rail and the page, and carries the things
that are true of the console rather than of any one screen: which gateway this is,
whether it is answering, the search, the notification bell and the way out. */
.topbar {
position: fixed;
inset: 0 0 auto 0;
z-index: 30;
height: var(--top);
display: flex;
align-items: center;
gap: 12px;
2026-08-15 09:23:26 +12:00
padding: var(--safe-top) max(clamp(14px, 2vw, 22px), var(--safe-right)) 0 var(--safe-left);
2026-08-14 09:40:03 +12:00
background: var(--top-bg);
border-bottom: 1px solid var(--line);
box-shadow: 0 1px 0 rgba(240, 246, 252, 0.02), 0 8px 24px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(14px);
}
.topbar-brand {
display: flex;
align-items: center;
gap: 11px;
flex: 0 0 var(--rail);
/* 22px is the rail's own padding plus a link's, so the mark sits in the same column as
the navigation icons under it rather than a few pixels to their left. */
height: 100%;
padding: 0 22px;
color: var(--text);
text-decoration: none;
}
/* Round, because the wordmark beside it is the only other thing in this corner and two
rounded rectangles side by side read as a control rather than as a signature. */
.brand-mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
flex: 0 0 30px;
border-radius: var(--radius-sm);
background: var(--accent);
color: #fff;
font-size: 16px;
font-weight: 800;
letter-spacing: -0.06em;
}
.brand-word {
font-size: 17px;
font-weight: 650;
letter-spacing: -0.01em;
white-space: nowrap;
}
.topbar-spacer {
flex: 1 1 auto;
min-width: 0;
}
.topbar-tools {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.topbar .omni {
position: absolute;
left: 50%;
transform: translateX(-50%);
2026-08-15 09:23:26 +12:00
width: min(820px, calc(100vw - var(--rail) - 380px));
2026-08-14 09:40:03 +12:00
}
/* The version is set in the console's monospace because it is read character by character
when somebody is working out which tree a container was deployed from. */
.topbar-version {
font: 11.5px/1 var(--mono);
color: var(--quiet);
white-space: nowrap;
}
.topbar-status {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 4px 6px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
text-align: left;
}
.topbar-status:hover:not(:disabled),
.topbar-status:focus-visible {
background: var(--surface-lift);
}
.topbar-status:disabled {
cursor: wait;
opacity: .65;
}
.topbar-status .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--quiet);
flex: 0 0 8px;
}
.topbar-status[data-tone="ok"] .dot {
background: var(--accent);
}
.topbar-status[data-tone="bad"] .dot {
background: var(--danger);
}
.topbar-status b {
font-size: 12.5px;
font-weight: 600;
}
.topbar-status span {
font-size: 11.5px;
color: var(--quiet);
white-space: nowrap;
}
@media (max-width: 900px) {
.topbar-version,
.topbar-status span {
display: none;
}
}
/* ---------- omni search ---------- */
.omni {
position: relative;
width: 100%;
}
.omni-input {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
height: 38px;
padding: 0 10px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
}
.omni[data-open="true"] .omni-input {
border-color: var(--accent);
}
2026-08-15 09:23:26 +12:00
.omni-input input[type="search"] {
2026-08-14 09:40:03 +12:00
flex: 1 1 auto;
min-width: 0;
2026-08-15 09:23:26 +12:00
height: 100%;
padding: 0;
2026-08-14 09:40:03 +12:00
border: 0;
2026-08-15 09:23:26 +12:00
border-radius: 0;
2026-08-14 09:40:03 +12:00
background: none;
color: var(--text);
font: inherit;
font-size: 14px;
outline: none;
}
.omni-input input::placeholder {
color: var(--quiet);
}
.omni-input .ico {
color: var(--quiet);
flex: 0 0 15px;
}
/* The shortcut is printed on the control because it is the only thing on the page saying
the console can be driven from the keyboard. */
.omni-key {
flex: 0 0 auto;
padding: 2px 6px;
border: 1px solid var(--line);
border-radius: 4px;
font: 10.5px/1.3 var(--mono);
color: var(--quiet);
}
.omni-panel {
position: absolute;
top: calc(100% + 6px);
right: 0;
width: min(520px, 94vw);
max-height: min(60vh, 520px);
overflow-y: auto;
padding: 6px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
z-index: 40;
}
.omni-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius-sm);
color: var(--text);
text-decoration: none;
}
.omni-item .ico {
color: var(--quiet);
flex: 0 0 16px;
}
.omni-item.on,
.omni-item:hover {
background: var(--surface-hi);
}
.omni-item b {
font-size: 13.5px;
font-weight: 600;
}
.omni-item small {
display: block;
color: var(--quiet);
font-size: 11.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.omni-item span {
flex: 1 1 auto;
min-width: 0;
}
.omni-group {
flex: 0 0 auto;
color: var(--quiet);
font-size: 10.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* ---------- notification bell ---------- */
.bell {
position: relative;
}
.bell-button {
position: relative;
display: grid;
place-items: center;
width: 34px;
height: 34px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--muted);
cursor: pointer;
}
.bell-button:hover {
background: var(--surface-hi);
color: var(--text);
}
.bell[data-open="true"] .bell-button {
border-color: var(--accent);
color: var(--text);
}
/* The badge counts unread events. It is a count rather than a dot because "there is news"
and "there are fourteen things you have not read" are different amounts of urgency. */
.bell-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 17px;
height: 17px;
padding: 0 4px;
border-radius: 9px;
background: var(--danger);
color: #fff;
font: 700 10.5px/17px var(--sans);
text-align: center;
}
.bell-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: min(420px, 92vw);
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
z-index: 40;
overflow: hidden;
}
.bell-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 11px 14px;
border-bottom: 1px solid var(--line);
}
.bell-head b {
font-size: 13px;
}
.bell-list {
max-height: min(58vh, 460px);
overflow-y: auto;
}
.bell-item {
display: flex;
gap: 11px;
padding: 11px 14px;
border-bottom: 1px solid var(--line-soft);
color: var(--text);
text-decoration: none;
}
.bell-item:last-child {
border-bottom: 0;
}
.bell-item:hover {
background: var(--surface-lift);
}
/* An unread row wears a bar down its left edge rather than a different background: the
list is read by scanning, and a row that changes colour when it is read makes the whole
panel reflow visually the moment it is opened. */
.bell-item[data-unread="true"] {
box-shadow: inset 2px 0 0 var(--accent);
}
.bell-item .glyph {
flex: 0 0 auto;
}
.bell-body {
flex: 1 1 auto;
min-width: 0;
}
.bell-body b {
display: block;
font-size: 13px;
font-weight: 600;
}
.bell-body p {
margin: 2px 0 0;
color: var(--muted);
font-size: 12.5px;
overflow-wrap: anywhere;
}
.bell-body time {
display: block;
margin-top: 3px;
color: var(--quiet);
font-size: 11px;
}
.bell-foot {
padding: 9px 14px;
border-top: 1px solid var(--line);
text-align: center;
}
.bell-foot a {
font-size: 12.5px;
text-decoration: none;
}
/* ---------- rail ---------- */
.rail {
position: fixed;
top: var(--top);
bottom: 0;
left: 0;
width: var(--rail);
2026-08-15 09:23:26 +12:00
padding: 12px 12px max(20px, var(--safe-bottom));
2026-08-14 09:40:03 +12:00
overflow-y: auto;
background: var(--rail-bg);
border-right: 1px solid var(--line);
z-index: 20;
}
.rail-group + .rail-group {
margin-top: 4px;
}
2026-08-15 09:23:26 +12:00
.rail-group:first-child {
margin-bottom: 9px;
padding-bottom: 9px;
border-bottom: 1px solid var(--line-soft);
}
2026-08-14 09:40:03 +12:00
.rail-head {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 9px 10px 5px;
border: 0;
background: none;
color: var(--quiet);
font: 600 10.5px/1 var(--sans);
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
}
.rail-head .caret {
margin-left: auto;
transition: transform 0.12s ease;
}
.rail-head[aria-expanded="false"] .caret {
transform: rotate(-90deg);
}
2026-08-15 09:23:26 +12:00
.rail-head-static {
cursor: default;
color: var(--muted);
}
2026-08-14 09:40:03 +12:00
.rail a {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: var(--radius-sm);
color: var(--muted);
font-size: 13.5px;
text-decoration: none;
}
.rail a:hover {
background: var(--surface);
color: var(--text);
}
.rail a[aria-current="page"] {
background: var(--accent-wash);
color: var(--accent-ink);
font-weight: 600;
}
.rail a .ico {
flex: 0 0 17px;
opacity: 0.85;
}
.rail a[aria-current="page"] .ico {
opacity: 1;
}
/* A rail item may carry the same badge the bell does, so news is visible without the
panel being open. */
.rail-badge {
margin-left: auto;
min-width: 18px;
padding: 0 5px;
border-radius: 9px;
background: var(--danger);
color: #fff;
font: 700 10.5px/17px var(--sans);
text-align: center;
}
/* ---------- page ---------- */
.page {
width: min(calc(100vw - var(--rail)), 1920px);
margin: var(--top) 0 0 calc(var(--rail) + max(0px, (100vw - var(--rail) - 1920px) / 2));
padding: clamp(24px, 2.4vw, 44px) clamp(22px, 3vw, 56px) 80px;
}
.page-head {
margin-bottom: 20px;
}
.page-head h1 {
margin: 0;
font-size: 22px;
font-weight: 650;
letter-spacing: -0.02em;
}
.page-head p {
margin: 5px 0 0;
max-width: 68ch;
color: var(--muted);
font-size: 13.5px;
}
.page-head-row {
display: flex;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
2026-08-15 09:23:26 +12:00
.page-head-title {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
.page-head-icon {
display: grid;
place-items: center;
flex: 0 0 40px;
width: 40px;
height: 40px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--accent-wash);
color: var(--accent-ink);
}
.page-head-icon .ico {
width: 20px;
height: 20px;
}
.page-head-text {
min-width: 0;
}
2026-08-14 09:40:03 +12:00
.page-head-row .page-head-actions {
margin-left: auto;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.crumbs {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
color: var(--quiet);
font-size: 12px;
}
.crumbs a {
color: var(--muted);
text-decoration: none;
}
.crumbs a:hover {
color: var(--text);
}
2026-08-15 09:23:26 +12:00
@media (max-width: 1400px) {
2026-08-14 09:40:03 +12:00
:root {
--rail: 0px;
}
.rail {
transform: translateX(-100%);
2026-08-15 09:23:26 +12:00
transition: transform 0.2s cubic-bezier(.22, 1, .36, 1);
width: min(320px, calc(100vw - 72px));
padding-right: 14px;
padding-left: max(14px, var(--safe-left));
background: rgba(8, 12, 18, .98);
box-shadow: none;
2026-08-14 09:40:03 +12:00
}
.rail[data-open="true"] {
transform: none;
2026-08-15 09:23:26 +12:00
box-shadow: 24px 0 80px rgba(0, 0, 0, 0.58);
2026-08-14 09:40:03 +12:00
}
.topbar-brand {
flex: 0 0 auto;
2026-08-15 09:23:26 +12:00
padding: 0 4px 0 12px;
2026-08-14 09:40:03 +12:00
}
.page {
margin-left: 0;
}
}
2026-08-15 09:23:26 +12:00
.rail-scrim {
display: none;
}
@media (max-width: 1400px) {
.rail-scrim {
position: fixed;
inset: var(--top) 0 0 0;
z-index: 19;
display: block;
width: auto;
height: auto;
padding: 0;
border: 0;
border-radius: 0;
background: rgba(1, 4, 9, .58);
backdrop-filter: blur(5px);
animation: rail-scrim-in .16s ease-out;
}
.rail-scrim:hover:not(:disabled) {
border: 0;
background: rgba(1, 4, 9, .58);
}
}
@keyframes rail-scrim-in {
from { opacity: 0; }
}
2026-08-14 09:40:03 +12:00
.rail-toggle {
display: none;
width: 34px;
height: 34px;
margin-left: 8px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--muted);
cursor: pointer;
}
2026-08-15 09:23:26 +12:00
@media (max-width: 1400px) {
2026-08-14 09:40:03 +12:00
.rail-toggle {
display: grid;
place-items: center;
}
}
/* ---------- error banner ---------- */
.banner {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
padding: 11px 14px;
border: 1px solid var(--danger);
border-radius: var(--radius-sm);
background: var(--danger-wash);
color: var(--danger-ink);
font-size: 13.5px;
}
.banner button {
margin-left: auto;
border: 0;
background: none;
color: inherit;
cursor: pointer;
font: inherit;
}
/* ---------- cards ---------- */
.card {
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.card + .card,
.grid + .card,
.card + .grid,
.grid + .grid {
margin-top: 16px;
}
.card-head {
display: flex;
align-items: center;
gap: 11px;
margin-bottom: 14px;
}
.card-head h2 {
margin: 0;
font-size: 14.5px;
font-weight: 620;
letter-spacing: -0.01em;
}
.card-head p {
margin: 2px 0 0;
color: var(--muted);
font-size: 12.5px;
}
.card-head-text {
min-width: 0;
}
.card-head-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.card-foot {
margin-top: 14px;
padding-top: 13px;
border-top: 1px solid var(--line-soft);
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
.grid[data-cols="2"] {
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
}
.grid[data-cols="wide"] {
grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr);
}
@media (max-width: 1100px) {
.grid[data-cols="wide"] {
grid-template-columns: 1fr;
}
}
/* ---------- tiles ---------- */
.tiles {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
margin-bottom: 16px;
}
.tile {
padding: 13px 15px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.tile .glyph {
2026-08-14 11:47:32 +12:00
/* Summary cards share tile padding, so their marker always starts at the same top-left
inset. The SVG itself remains centred by .glyph's grid treatment. */
margin: 0 0 9px;
2026-08-14 09:40:03 +12:00
}
.tile b {
display: block;
font-size: 24px;
font-weight: 650;
letter-spacing: -0.03em;
line-height: 1.15;
}
.tile b.small {
font-size: 14.5px;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.4;
}
.tile span {
display: block;
margin-top: 3px;
color: var(--muted);
font-size: 12px;
}
/* A tile that links somewhere behaves like one; a tile that does not must not look like
it does. */
a.tile {
color: inherit;
text-decoration: none;
}
a.tile:hover {
border-color: var(--accent);
background: var(--surface-lift);
}
/* ---------- glyphs, tags, chips ---------- */
.ico {
display: block;
width: 1em;
height: 1em;
font-size: 17px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
flex: 0 0 auto;
}
.glyph {
display: grid;
place-items: center;
width: 30px;
height: 30px;
flex: 0 0 30px;
border-radius: var(--radius-sm);
background: var(--surface-hi);
color: var(--muted);
}
.tile .glyph .ico { width: 18px; height: 18px; margin: auto; }
.loading-page { display: grid; gap: 20px; }
.loading-heading { display: grid; gap: 10px; max-width: 48rem; }
.loading-heading .skeleton:first-child { height: 34px; width: 28%; }
.loading-heading .skeleton:last-child { height: 18px; width: 72%; }
.loading-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
.loading-tiles .skeleton { height: 132px; }
.loading-card { display: grid; gap: 12px; min-height: 180px; padding: 22px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.loading-card .skeleton:first-child { height: 20px; width: 32%; }
.loading-card .skeleton:not(:first-child) { height: 14px; }
@media (max-width: 820px) { .loading-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.glyph .ico {
display: block;
}
.glyph[data-tone="ok"] {
background: var(--accent-wash);
color: var(--accent-ink);
}
.glyph[data-tone="warn"] {
background: var(--warn-wash);
color: var(--warn-ink);
}
.glyph[data-tone="bad"] {
background: var(--danger-wash);
color: var(--danger-ink);
}
.glyph[data-tone="info"] {
background: var(--info-wash);
color: var(--info-ink);
}
.glyph[data-tone="note"] {
background: var(--note-wash);
color: var(--note-ink);
}
.glyph[data-tone="data"] {
background: var(--data-wash);
color: var(--data-ink);
}
.tag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface-lift);
color: var(--muted);
font-size: 11.5px;
font-weight: 600;
white-space: nowrap;
}
/* A verdict tag carries a dot in its own colour: tone alone is no signal to somebody who
cannot separate the green from the amber. */
.tag[data-tone]::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.tag[data-tone="ok"] {
border-color: rgba(82, 181, 75, 0.35);
background: var(--accent-wash);
color: var(--accent-ink);
}
.tag[data-tone="warn"] {
border-color: rgba(239, 196, 107, 0.35);
background: var(--warn-wash);
color: var(--warn-ink);
}
.tag[data-tone="bad"] {
border-color: rgba(229, 83, 75, 0.35);
background: var(--danger-wash);
color: var(--danger-ink);
}
.tag[data-tone="info"] {
border-color: rgba(79, 156, 216, 0.35);
background: var(--info-wash);
color: var(--info-ink);
}
.tag[data-tone="note"] {
border-color: rgba(160, 124, 232, 0.35);
background: var(--note-wash);
color: var(--note-ink);
}
.tag[data-tone="data"] {
border-color: rgba(63, 194, 182, 0.35);
background: var(--data-wash);
color: var(--data-ink);
}
.chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 9px;
border: 1px solid var(--line);
border-radius: var(--radius-xs);
background: var(--surface-lift);
color: var(--muted);
font: 11.5px/1.5 var(--mono);
white-space: nowrap;
}
.chip[data-tone="ok"] {
color: var(--accent-ink);
}
.chip[data-tone="warn"] {
color: var(--warn-ink);
}
.chip[data-tone="bad"] {
color: var(--danger-ink);
}
.chip[data-tone="info"] {
color: var(--info-ink);
}
.chip[data-tone="note"] {
color: var(--note-ink);
}
.chip[data-tone="data"] {
color: var(--data-ink);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
/* ---------- tables ---------- */
/* Dense but readable, which is most of what this console is for. The wrapper is what
scrolls, so a wide table never makes the page scroll sideways. */
.table-wrap {
overflow-x: auto;
overscroll-behavior-inline: contain;
-webkit-overflow-scrolling: touch;
margin: 0 -18px -16px;
padding: 0 18px 16px;
}
table {
width: 100%;
min-width: max-content;
border-collapse: collapse;
font-size: 13px;
}
2026-08-15 09:23:26 +12:00
/* Tablets get a drawer shell rather than a squeezed desktop rail. The controls remain in
one calm bar in both orientations; only the least useful labels disappear as space gets
tighter, while the live status itself always remains visible. */
@media (max-width: 1400px) {
2026-08-14 09:40:03 +12:00
.topbar {
display: flex;
2026-08-15 09:23:26 +12:00
gap: 10px;
2026-08-14 09:40:03 +12:00
height: var(--top);
2026-08-15 09:23:26 +12:00
padding: var(--safe-top) max(16px, var(--safe-right)) 0 max(16px, var(--safe-left));
2026-08-14 09:40:03 +12:00
}
.topbar-brand {
padding: 0;
}
.rail-toggle {
margin-left: 0;
}
.topbar-spacer {
display: none;
}
.topbar .omni {
position: relative;
left: auto;
transform: none;
flex: 1 1 auto;
2026-08-15 09:23:26 +12:00
width: auto;
max-width: 640px;
2026-08-14 09:40:03 +12:00
min-width: 0;
}
.omni-input {
height: 36px;
}
.omni-panel {
position: fixed;
top: calc(var(--top) + 6px);
right: 12px;
left: 12px;
width: auto;
max-height: calc(100dvh - var(--top) - 18px);
}
.omni-key,
.topbar-version,
2026-08-15 09:23:26 +12:00
.topbar-status span {
2026-08-14 09:40:03 +12:00
display: none;
}
.topbar-tools {
display: contents;
}
.bell-button,
.rail-toggle {
2026-08-15 09:23:26 +12:00
width: 40px;
height: 40px;
}
.topbar-status {
min-width: 40px;
height: 40px;
padding: 0 10px;
border: 1px solid var(--line);
background: var(--surface);
2026-08-14 09:40:03 +12:00
}
.page {
2026-08-15 09:23:26 +12:00
width: 100%;
padding: clamp(24px, 3vw, 34px) max(24px, calc(var(--safe-right) + 10px)) max(64px, calc(var(--safe-bottom) + 36px)) max(24px, calc(var(--safe-left) + 10px));
2026-08-14 09:40:03 +12:00
}
.table-wrap {
2026-08-15 09:23:26 +12:00
margin: 0 -18px -16px;
padding: 0 18px 18px;
scrollbar-gutter: stable;
2026-08-14 09:40:03 +12:00
}
table {
font-size: 13px;
}
th,
td {
padding-right: 16px;
}
}
2026-08-15 09:23:26 +12:00
@media (max-width: 820px) {
.brand-word,
.topbar-status b {
display: none;
}
.topbar-status {
display: grid;
place-items: center;
width: 40px;
padding: 0;
}
.page {
padding: 22px max(16px, var(--safe-right)) max(56px, calc(var(--safe-bottom) + 28px)) max(16px, var(--safe-left));
}
.grid,
.grid[data-cols="2"],
.grid[data-cols="wide"],
.grid.two {
grid-template-columns: minmax(0, 1fr);
}
.tiles {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.table-wrap {
margin-right: -16px;
margin-left: -16px;
padding-right: 16px;
padding-left: 16px;
}
}
2026-08-14 09:40:03 +12:00
th {
padding: 0 12px 8px 0;
border-bottom: 1px solid var(--line);
color: var(--quiet);
font-weight: 600;
font-size: 11px;
letter-spacing: 0.07em;
text-transform: uppercase;
text-align: left;
white-space: nowrap;
}
td {
padding: 9px 12px 9px 0;
border-bottom: 1px solid var(--line-soft);
vertical-align: middle;
}
tr:last-child td {
border-bottom: 0;
}
tbody tr:hover td {
background: var(--surface-lift);
}
th:last-child,
td:last-child {
padding-right: 0;
}
td.num,
th.num {
text-align: right;
font-variant-numeric: tabular-nums;
}
td.mono {
font: 12px/1.5 var(--mono);
color: var(--muted);
}
td.nowrap,
th.nowrap {
white-space: nowrap;
}
td.muted {
color: var(--muted);
}
/* A sortable header is a button, so it is reachable from a keyboard. */
th button {
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
padding: 0;
background: none;
color: inherit;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
}
th button:hover {
color: var(--text);
}
th[aria-sort] button {
color: var(--accent-ink);
}
.table-row-link {
color: var(--text);
text-decoration: none;
font-weight: 600;
}
.table-row-link:hover {
color: var(--accent-ink);
}
/* ---------- lists ---------- */
.list {
display: flex;
flex-direction: column;
gap: 1px;
}
.list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid var(--line-soft);
}
.list-item:last-child {
border-bottom: 0;
}
.list-item .list-body {
flex: 1 1 auto;
min-width: 0;
}
.list-item b {
display: block;
font-size: 13.5px;
font-weight: 600;
}
.list-item p {
margin: 2px 0 0;
color: var(--muted);
font-size: 12.5px;
}
.list-item .list-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* ---------- controls ---------- */
button,
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
height: 32px;
padding: 0 13px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface-lift);
color: var(--text);
font: 600 13px/1 var(--sans);
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
button:hover:not(:disabled),
.btn:hover {
background: var(--surface-hi);
border-color: #2c3542;
}
button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
button[data-variant="primary"] {
border-color: var(--accent);
background: var(--accent);
color: #06240a;
}
button[data-variant="primary"]:hover:not(:disabled) {
background: var(--accent-ink);
border-color: var(--accent-ink);
}
button[data-variant="danger"] {
border-color: rgba(229, 83, 75, 0.4);
background: var(--danger-wash);
color: var(--danger-ink);
}
button[data-variant="danger"]:hover:not(:disabled) {
background: rgba(229, 83, 75, 0.22);
}
button[data-variant="quiet"] {
border-color: transparent;
background: none;
color: var(--muted);
}
button[data-variant="quiet"]:hover:not(:disabled) {
background: var(--surface-lift);
color: var(--text);
}
button[data-size="sm"] {
height: 26px;
padding: 0 9px;
font-size: 12px;
}
.field {
display: block;
min-width: 0;
}
.field > span {
display: block;
margin-bottom: 5px;
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
.field > small {
display: block;
margin-top: 5px;
color: var(--quiet);
font-size: 11.5px;
}
input[type="text"],
input[type="password"],
input[type="search"],
input[type="number"],
input[type="date"],
2026-08-15 09:23:26 +12:00
input[type="time"],
input[type="datetime-local"],
2026-08-14 09:40:03 +12:00
input[type="url"],
select,
textarea {
width: 100%;
height: 32px;
padding: 0 10px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text);
font: inherit;
font-size: 13px;
}
textarea {
height: auto;
min-height: 74px;
padding: 8px 10px;
resize: vertical;
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--accent);
outline: none;
}
input::placeholder,
textarea::placeholder {
color: var(--quiet);
}
select {
appearance: none;
padding-right: 26px;
background-image: linear-gradient(45deg, transparent 50%, var(--quiet) 50%),
linear-gradient(135deg, var(--quiet) 50%, transparent 50%);
background-position: calc(100% - 14px) 14px, calc(100% - 9px) 14px;
background-size: 5px 5px;
background-repeat: no-repeat;
}
.fields {
display: grid;
gap: 13px;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.field-row {
display: flex;
align-items: flex-end;
gap: 10px;
flex-wrap: wrap;
}
/* A switch, not a checkbox: the console is full of on/off policy and a control that says
which way it is pointing from across the room is worth the extra markup. */
.check {
display: flex;
align-items: flex-start;
gap: 11px;
padding: 10px 0;
cursor: pointer;
}
.check input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.check .switch {
position: relative;
width: 34px;
height: 20px;
flex: 0 0 34px;
margin-top: 1px;
border-radius: 999px;
background: var(--surface-hi);
border: 1px solid var(--line);
transition: background 0.12s ease, border-color 0.12s ease;
}
.check .switch::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.12s ease, background 0.12s ease;
}
.check input:checked + .switch {
background: var(--accent-wash);
border-color: var(--accent);
}
.check input:checked + .switch::after {
transform: translateX(14px);
background: var(--accent);
}
.check input:focus-visible + .switch {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.check input:disabled + .switch {
opacity: 0.45;
}
.check-body b {
display: block;
font-size: 13.5px;
font-weight: 600;
}
.check-body p {
margin: 2px 0 0;
color: var(--muted);
font-size: 12.5px;
}
/* A row of exclusive choices — a date window, a severity — where a select would hide the
options behind a click. */
.segments {
display: inline-flex;
padding: 2px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
}
.segments button {
height: 26px;
border: 0;
background: none;
color: var(--muted);
font-size: 12.5px;
font-weight: 600;
}
.segments button[aria-pressed="true"] {
background: var(--surface-hi);
color: var(--text);
}
/* ---------- filter bar ---------- */
/* Fast filtering is most of what makes a table useful, so the controls sit above it in a
row of their own rather than behind a "filters" disclosure nobody opens. */
.filters {
display: flex;
align-items: flex-end;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 14px;
padding: 13px 15px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.filters .field {
flex: 1 1 160px;
max-width: 260px;
}
.filters .field.grow {
flex: 2 1 240px;
max-width: none;
}
.filters .filter-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
}
.filter-summary {
color: var(--quiet);
font-size: 12px;
white-space: nowrap;
}
/* ---------- charts ---------- */
/* One bar chart, drawn with divs. A charting library would be the single largest
dependency in the console for the sake of four bar charts. */
.bars {
display: flex;
align-items: flex-end;
gap: 3px;
height: 92px;
padding-top: 6px;
}
.bar {
position: relative;
flex: 1 1 0;
min-width: 3px;
border-radius: 2px 2px 0 0;
background: var(--accent-wash);
border-top: 2px solid var(--accent);
min-height: 2px;
}
.bar[data-tone="bad"] {
background: var(--danger-wash);
border-top-color: var(--danger);
}
.bar[data-empty="true"] {
background: var(--line-soft);
border-top-color: var(--line);
}
.bars-axis {
display: flex;
justify-content: space-between;
margin-top: 6px;
color: var(--quiet);
font-size: 11px;
}
/* A proportion bar, for one number against a total. */
.meter {
height: 6px;
border-radius: 3px;
background: var(--surface-hi);
overflow: hidden;
}
.meter > div {
height: 100%;
background: var(--accent);
}
.meter[data-tone="warn"] > div {
background: var(--warn);
}
.meter[data-tone="bad"] > div {
background: var(--danger);
}
/* ---------- log / code ---------- */
.logview {
2026-08-15 09:23:26 +12:00
height: 62vh;
min-height: 320px;
2026-08-14 09:40:03 +12:00
overflow: auto;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: #080a0e;
font: 12px/1.6 var(--mono);
2026-08-15 09:23:26 +12:00
contain: layout paint style;
2026-08-14 09:40:03 +12:00
}
2026-08-14 11:47:32 +12:00
.loghead {
position: sticky;
top: 0;
z-index: 1;
display: grid;
2026-08-15 09:23:26 +12:00
grid-template-columns: 150px 46px minmax(180px, 1fr) minmax(240px, 2fr);
2026-08-14 11:47:32 +12:00
gap: 12px;
padding: 7px 12px;
border-bottom: 1px solid var(--line);
background: #10131a;
color: var(--quiet);
font-size: 10px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
2026-08-15 09:23:26 +12:00
min-width: 760px;
}
.logbody {
position: relative;
min-width: 760px;
2026-08-14 11:47:32 +12:00
}
2026-08-14 09:40:03 +12:00
.logline {
display: flex;
gap: 12px;
padding: 3px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
2026-08-15 09:23:26 +12:00
.logline[data-virtual="true"] {
position: absolute;
top: 0;
right: 0;
left: 0;
display: grid;
grid-template-columns: 150px 46px minmax(180px, 1fr) minmax(240px, 2fr);
align-items: center;
height: 48px;
padding-top: 4px;
padding-bottom: 4px;
white-space: nowrap;
contain: strict;
}
2026-08-14 09:40:03 +12:00
.logline:hover {
background: rgba(255, 255, 255, 0.03);
}
.logline time {
flex: 0 0 auto;
color: var(--quiet);
}
2026-08-14 11:47:32 +12:00
.logline time small {
display: block;
color: var(--muted);
font-size: 10px;
}
2026-08-14 09:40:03 +12:00
.logline .lvl {
flex: 0 0 46px;
font-weight: 700;
}
.logline[data-level="ERROR"] .lvl {
color: var(--danger-ink);
}
.logline[data-level="WARN"] .lvl {
color: var(--warn-ink);
}
.logline[data-level="INFO"] .lvl {
color: var(--info-ink);
}
.logline[data-level="DEBUG"] .lvl {
color: var(--quiet);
}
.logline .msg {
flex: 1 1 auto;
min-width: 0;
2026-08-15 09:23:26 +12:00
overflow: hidden;
text-overflow: ellipsis;
2026-08-14 09:40:03 +12:00
}
.logline .attrs {
color: var(--quiet);
}
2026-08-14 11:47:32 +12:00
@media (min-width: 900px) {
.logline {
display: grid;
2026-08-15 09:23:26 +12:00
grid-template-columns: 150px 46px minmax(180px, 1fr) minmax(240px, 2fr);
2026-08-14 11:47:32 +12:00
}
}
2026-08-14 09:40:03 +12:00
.logline .attrs b {
color: var(--muted);
font-weight: 400;
}
2026-08-15 09:23:26 +12:00
.logattrs-button {
display: block;
width: 100%;
min-width: 0;
height: 28px;
padding: 0;
overflow: hidden;
border: 0;
background: transparent;
color: var(--quiet);
font: inherit;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.logattrs-button:hover:not(:disabled) {
border: 0;
background: transparent;
color: var(--text);
}
.logattrs-button:focus-visible {
outline: 1px solid var(--accent);
outline-offset: 2px;
}
.log-inspector {
margin-top: 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
}
.log-inspector-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.log-inspector-head > div {
min-width: 0;
}
.log-inspector-head b,
.log-inspector-head span {
display: block;
overflow-wrap: anywhere;
}
.log-inspector-head span {
margin-top: 3px;
color: var(--quiet);
font: 11px/1.5 var(--mono);
}
.log-inspector dl {
display: grid;
grid-template-columns: minmax(130px, max-content) minmax(0, 1fr);
gap: 5px 14px;
margin: 12px 0 0;
padding-top: 10px;
border-top: 1px solid var(--line);
font: 12px/1.5 var(--mono);
}
.log-inspector dt { color: var(--muted); }
.log-inspector dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
2026-08-14 11:47:32 +12:00
.logdetails {
min-width: 0;
}
.logdetails summary {
cursor: pointer;
list-style: none;
overflow-wrap: anywhere;
}
.logdetails summary::-webkit-details-marker { display: none; }
.logdetails summary::before {
content: '';
display: inline-block;
margin-right: 6px;
color: var(--accent);
}
.logdetails[open] summary::before { transform: rotate(90deg); }
.logdetails dl {
display: grid;
grid-template-columns: minmax(130px, max-content) minmax(0, 1fr);
gap: 4px 12px;
margin: 7px 0 2px;
padding: 8px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, .025);
}
.logdetails dt { color: var(--muted); }
.logdetails dd { min-width: 0; margin: 0; color: var(--text); overflow-wrap: anywhere; }
2026-08-14 09:40:03 +12:00
pre.code {
margin: 0;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: #080a0e;
color: var(--muted);
font: 12px/1.6 var(--mono);
overflow-x: auto;
}
/* ---------- misc ---------- */
.empty {
margin: 0;
padding: 22px 0;
color: var(--quiet);
font-size: 13px;
text-align: center;
}
.muted {
color: var(--muted);
}
.quiet {
color: var(--quiet);
}
.mono {
font-family: var(--mono);
font-size: 12px;
}
.note {
margin: 0;
padding: 10px 13px;
border: 1px solid var(--line);
border-left: 2px solid var(--info);
border-radius: var(--radius-xs);
background: var(--surface-lift);
color: var(--muted);
font-size: 12.5px;
}
.note[data-tone="warn"] {
border-left-color: var(--warn);
}
.note[data-tone="bad"] {
border-left-color: var(--danger);
}
.note[data-tone="ok"] {
border-left-color: var(--accent);
}
.stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.row.tight {
gap: 6px;
}
.spacer {
flex: 1 1 auto;
}
/* One spinner, and it is the only thing in the console that moves on its own. */
.spinner {
width: 14px;
height: 14px;
border: 2px solid var(--line);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* The page's own loading state: a shape rather than a spinner, so the layout does not
jump when the data lands. */
.skeleton {
border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--surface) 25%, var(--surface-lift) 50%, var(--surface) 75%);
background-size: 300% 100%;
animation: shimmer 1.3s ease-in-out infinite;
}
@keyframes shimmer {
to {
background-position: -300% 0;
}
}
/* ---------- toasts ---------- */
/* A mutation says what happened here rather than in the error banner, which is reserved
for the console failing to talk to the gateway at all. */
.toasts {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 50;
display: flex;
flex-direction: column;
gap: 8px;
max-width: min(420px, 90vw);
}
.toast {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 11px 14px;
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
border-radius: var(--radius-sm);
background: var(--surface-lift);
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
font-size: 13px;
animation: toast-in 0.14s ease;
}
.toast[data-tone="bad"] {
border-left-color: var(--danger);
}
.toast[data-tone="warn"] {
border-left-color: var(--warn);
}
.toast button {
margin-left: auto;
height: auto;
padding: 0;
border: 0;
background: none;
color: var(--quiet);
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(6px);
}
}
/* ---------- dialog ---------- */
.scrim {
position: fixed;
inset: 0;
z-index: 45;
display: grid;
place-items: center;
padding: 20px;
background: rgba(4, 6, 9, 0.7);
backdrop-filter: blur(2px);
}
.dialog {
width: min(520px, 100%);
max-height: 86vh;
overflow-y: auto;
padding: 20px 22px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}
.dialog h2 {
margin: 0 0 6px;
font-size: 16px;
}
.dialog p {
margin: 0 0 16px;
color: var(--muted);
font-size: 13.5px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
}
/* ---------- key/value rows ---------- */
/* A label on the left, a verdict on the right. The console's densest way of stating a
handful of facts about one subject, and the shape the overview is built from. */
.kv {
display: flex;
flex-direction: column;
}
.kv-row {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 0;
border-bottom: 1px solid var(--line-soft);
font-size: 13.5px;
}
.kv-row:last-child {
border-bottom: 0;
}
.kv-row > span:first-child {
color: var(--muted);
}
.kv-row > span:last-child {
margin-left: auto;
text-align: right;
min-width: 0;
}
/* A tile strip with no card around it, for a group of numbers inside one. */
.tiles.plain {
margin-bottom: 12px;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.tiles.plain .tile {
padding: 10px 12px;
border: 0;
background: var(--surface-lift);
}
.tiles.plain .tile b {
font-size: 18px;
}
.hint {
margin: 0;
color: var(--quiet);
font-size: 12px;
}
/* A two-column card grid that does not collapse as eagerly as the default. */
.grid.two {
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
}
@media (max-width: 980px) {
.grid.two {
grid-template-columns: 1fr;
}
}
/* ---------- linkable list rows ---------- */
/* A row that is a destination. The whole row is the target rather than a link inside it,
because a list of people is read by scanning names and clicking one. */
.list-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 18px;
border-bottom: 1px solid var(--line-soft);
color: var(--text);
text-decoration: none;
}
.list-row:last-child {
border-bottom: 0;
}
.list-row:hover {
background: var(--surface-lift);
}
.list-main {
display: flex;
align-items: center;
gap: 12px;
flex: 1 1 auto;
min-width: 0;
}
.list-title {
display: flex;
align-items: center;
gap: 7px;
font-size: 14px;
font-weight: 600;
}
.list-meta {
display: block;
margin-top: 2px;
color: var(--muted);
font-size: 12.5px;
}
.list-row .list-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
/* A card that holds nothing but a list: the rows carry their own padding, so the card
must not add a second inset around them. */
.card.flush {
padding: 0;
}
/* Initials behind a name. Emby has no avatar for most households, and a row of identical
grey circles says nothing — the letters are what makes one row distinguishable from the
next at a glance. */
.avatar {
display: grid;
place-items: center;
width: 34px;
height: 34px;
flex: 0 0 34px;
border-radius: 50%;
background: var(--note-wash);
color: var(--note-ink);
font-size: 12.5px;
font-weight: 700;
letter-spacing: 0.02em;
}
/* The presence dot: three-valued, and the tone is passed rather than derived. */
.dot-state {
width: 7px;
height: 7px;
flex: 0 0 7px;
border-radius: 50%;
background: var(--quiet);
}
.dot-state[data-tone="ok"] {
background: var(--accent);
}
.dot-state[data-tone="warn"] {
background: var(--warn);
}
.dot-state[data-tone="bad"] {
background: var(--danger);
}
/* A column of switches, for a list of people or of options. */
.checks.columns {
display: grid;
gap: 0 24px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.crumb {
color: var(--muted);
font-size: 12.5px;
text-decoration: none;
white-space: nowrap;
}
.crumb:hover {
color: var(--accent-ink);
}
/* A run of small chips inside a table cell — a device's build history. */
.versions {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* ---------- journey visits ---------- */
/* One visit read from its first step to its last. A timeline rather than a table because
the shape of a visit — how many steps, and how far apart — is most of what is being
read, and a table of five rows hides it. */
.visit {
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface-lift);
}
.visit + .visit {
margin-top: 10px;
}
.visit header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.visit header b {
font-size: 13.5px;
}
.visit header span {
display: block;
color: var(--quiet);
font-size: 12px;
}
.visit header .tag {
margin-left: auto;
}
.journey-answers {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin: 0 0 15px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius-xs);
background: var(--line);
}
.journey-answer {
position: relative;
min-width: 0;
padding: 11px 12px 11px 38px;
background: var(--surface);
}
.journey-answer .ico {
position: absolute;
top: 13px;
left: 12px;
width: 16px;
height: 16px;
color: var(--quiet);
}
.journey-answer[data-kind="entry"] .ico,
.journey-answer[data-kind="outcome"] .ico { color: var(--accent); }
.journey-answer[data-kind="selection"] .ico { color: var(--note-ink); }
.journey-answer span,
.journey-answer b {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.journey-answer span {
margin-bottom: 3px;
color: var(--quiet);
font-size: 11px;
font-weight: 650;
letter-spacing: .04em;
text-transform: uppercase;
}
.journey-answer b {
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.journey-timeline {
margin: 0;
padding: 0 0 0 14px;
list-style: none;
border-left: 1px solid var(--line);
}
.journey-timeline li {
position: relative;
display: flex;
align-items: baseline;
gap: 10px;
padding: 7px 0 7px 5px;
font-size: 12.5px;
}
.timeline-dot {
position: absolute;
left: -18px;
top: 12px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
}
.timeline-dot[data-action="select"],
.timeline-dot[data-action="open"] { background: var(--note-ink); }
.timeline-dot[data-action="close"] { background: var(--quiet); }
.journey-timeline li > div { min-width: 0; }
.journey-timeline li b {
display: inline;
font-weight: 600;
}
.journey-timeline li div span {
display: block;
color: var(--muted);
}
.journey-timeline li time {
margin-left: auto;
padding-left: 10px;
color: var(--quiet);
white-space: nowrap;
}
.visits {
max-height: 60vh;
overflow-y: auto;
}
@media (max-width: 640px) {
.journey-answers { grid-template-columns: 1fr; }
.journey-timeline li { align-items: flex-start; flex-wrap: wrap; }
.journey-timeline li time { width: 100%; margin: 2px 0 0; padding-left: 0; }
}
/* A headline number over a proportion bar, for one figure that has a denominator. */
.summary {
padding: 13px 15px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface-lift);
}
.summary-head {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 9px;
}
.summary-head b {
font-size: 13px;
}
.summary-head strong {
margin-left: auto;
font-size: 22px;
font-weight: 650;
letter-spacing: -0.03em;
}
.summary p {
margin: 9px 0 0;
color: var(--muted);
font-size: 12.5px;
}
.summary-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
margin-top: 14px;
}
.table-sub {
display: block;
color: var(--quiet);
font-size: 11.5px;
}
.route-arrow {
color: var(--quiet);
}
details summary {
cursor: pointer;
font-size: 12.5px;
padding: 6px 0;
}
/* ---------- theme swatch ---------- */
/* A palette rendered as its own three colours. The custom properties are the only inline
style in the console, and they are values rather than layout: what a colour scheme looks
like cannot be described in a class name. */
.swatch {
position: relative;
width: 34px;
height: 22px;
flex: 0 0 34px;
margin-top: 1px;
border-radius: var(--radius-xs);
background: var(--swatch-surface, var(--surface-hi));
border: 1px solid var(--swatch-hairline, var(--line));
overflow: hidden;
}
.swatch i {
position: absolute;
inset: auto 0 0 0;
height: 7px;
background: var(--swatch-accent, var(--accent));
}
/* A caption inside a card, above a group of controls that belong together. Quieter than a
heading, because the card already has one. */
.group-label {
margin: 4px 0 2px;
color: var(--quiet);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.group + .group {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--line-soft);
}
2026-08-15 09:23:26 +12:00
/* ---------- hero scheduler ---------- */
.hero-pins {
display: grid;
gap: 8px;
margin-bottom: 16px;
}
.hero-pin {
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto auto;
align-items: center;
gap: 10px;
min-height: 52px;
padding: 8px 10px;
border: 1px solid var(--line-soft);
border-radius: var(--radius-sm);
background: var(--surface-lift);
}
.hero-pin-order {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--note-wash);
color: var(--note-ink);
font-size: 12px;
font-weight: 750;
}
.hero-pin > span:nth-child(2) {
min-width: 0;
}
.hero-pin b,
.hero-pin small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hero-pin b { font-size: 13.5px; }
.hero-pin small { margin-top: 2px; color: var(--quiet); font-size: 11.5px; }
.hero-schedule-list {
display: grid;
gap: 10px;
}
.hero-schedule {
position: relative;
display: grid;
grid-template-columns: 100px minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
padding: 15px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 11px;
background: linear-gradient(135deg, var(--surface-lift), rgba(13, 17, 23, .74));
}
.hero-schedule::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: var(--quiet);
opacity: .4;
}
.hero-schedule[data-enabled="true"]::before {
background: var(--info);
opacity: 1;
}
.hero-schedule-time {
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
padding-right: 14px;
border-right: 1px solid var(--line-soft);
}
.hero-schedule-time b {
color: var(--info-ink);
font-size: 11px;
letter-spacing: .08em;
text-transform: uppercase;
}
.hero-schedule-time span {
margin-top: 4px;
color: var(--muted);
font: 12px/1.4 var(--mono);
}
.hero-schedule-main { min-width: 0; }
.hero-schedule-title {
display: flex;
align-items: center;
gap: 8px;
}
.hero-schedule-title h3 {
min-width: 0;
margin: 0;
overflow: hidden;
color: var(--text);
font-size: 14px;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.hero-schedule-main > p {
margin: 4px 0 8px;
color: var(--muted);
font-size: 12.5px;
}
.hero-schedule-actions {
display: flex;
align-items: center;
gap: 6px;
}
.hero-schedule-dialog {
width: min(760px, 100%);
padding: 0;
overflow-x: hidden;
}
.hero-schedule-dialog-head {
padding: 24px 26px 20px;
border-bottom: 1px solid var(--line);
background:
radial-gradient(circle at 90% -40%, var(--info-wash), transparent 240px),
var(--surface-lift);
}
.hero-schedule-kicker {
color: var(--info-ink);
font-size: 10.5px;
font-weight: 750;
letter-spacing: .1em;
text-transform: uppercase;
}
.hero-schedule-dialog-head h2 {
margin-top: 4px;
font-size: 21px;
letter-spacing: -.025em;
}
.hero-schedule-dialog-head p { margin-bottom: 0; }
.hero-schedule-dialog > .fields,
.hero-schedule-dialog > .schedule-days,
.hero-schedule-dialog > .schedule-options,
.hero-schedule-dialog > .check {
margin-right: 26px;
margin-left: 26px;
}
.schedule-frequency {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin: 20px 26px;
}
.schedule-frequency button {
display: block;
height: auto;
min-height: 68px;
padding: 11px 13px;
text-align: left;
white-space: normal;
}
.schedule-frequency button b,
.schedule-frequency button span {
display: block;
}
.schedule-frequency button span {
margin-top: 4px;
color: var(--quiet);
font-size: 11.5px;
font-weight: 500;
}
.schedule-frequency button[aria-pressed="true"] {
border-color: var(--info);
background: var(--info-wash);
color: var(--info-ink);
}
.schedule-frequency button[aria-pressed="true"] span { color: var(--muted); }
.schedule-days {
margin-top: 18px;
padding: 16px;
border: 1px solid var(--line-soft);
border-radius: 10px;
background: var(--surface-lift);
}
.schedule-days-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.schedule-days-head > b { font-size: 12.5px; }
.schedule-days-head > div {
display: flex;
gap: 4px;
margin-left: auto;
}
.schedule-days-head button {
height: 28px;
padding: 0 8px;
border-color: transparent;
background: none;
color: var(--muted);
font-size: 11.5px;
}
.schedule-day-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 6px;
}
.schedule-day-grid button {
width: 100%;
height: 38px;
padding: 0;
color: var(--muted);
}
.schedule-day-grid button[aria-pressed="true"] {
border-color: var(--accent);
background: var(--accent-wash);
color: var(--accent-ink);
}
.schedule-options {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(140px, 1fr);
align-items: start;
gap: 22px;
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--line-soft);
}
.schedule-option-label {
display: block;
margin-bottom: 3px;
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
.schedule-placement-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px 14px;
}
.hero-schedule-dialog > .check { margin-top: 8px; }
.hero-schedule-dialog > .dialog-actions {
margin-top: 8px;
padding: 18px 26px;
border-top: 1px solid var(--line);
background: var(--surface-lift);
}
@media (max-width: 820px) {
.hero-pin {
grid-template-columns: 30px minmax(0, 1fr) auto;
}
.hero-pin > button:last-child { grid-column: 3; }
.hero-schedule {
grid-template-columns: 78px minmax(0, 1fr);
gap: 12px;
}
.hero-schedule-actions {
grid-column: 1 / -1;
padding-top: 10px;
border-top: 1px solid var(--line-soft);
}
.hero-schedule-actions button { flex: 1 1 0; }
.hero-schedule-dialog-head { padding: 22px 20px 18px; }
.hero-schedule-dialog > .fields,
.hero-schedule-dialog > .schedule-days,
.hero-schedule-dialog > .schedule-options,
.hero-schedule-dialog > .check {
margin-right: 20px;
margin-left: 20px;
}
.schedule-frequency {
gap: 6px;
margin: 16px 20px;
}
.schedule-frequency button {
min-width: 0;
min-height: 62px;
padding: 9px;
}
.schedule-frequency button span { display: none; }
.schedule-days-head { align-items: flex-start; }
.schedule-days-head > div { flex-wrap: wrap; justify-content: flex-end; }
.schedule-day-grid { gap: 4px; }
.schedule-day-grid button { min-width: 0; padding: 0 3px; font-size: 11px; }
.schedule-options { grid-template-columns: 1fr; }
.schedule-placement-grid { grid-template-columns: 1fr; }
.hero-schedule-dialog > .dialog-actions { padding: 16px 20px max(16px, var(--safe-bottom)); }
}
/* ---------- tablet and touch refinement ---------- */
/* iPad is an operating surface, not a large phone. Cards gain enough separation to be
parsed at arm's length, but data remains dense and two-column where portrait width can
genuinely support it. */
@media (min-width: 821px) and (max-width: 1400px) {
.page-head {
margin-bottom: 24px;
}
.page-head h1 {
font-size: 26px;
line-height: 1.2;
}
.page-head p {
max-width: 72ch;
font-size: 14px;
}
.card {
padding: 20px 22px;
border-color: rgba(99, 110, 123, .42);
border-radius: 12px;
box-shadow: 0 1px 0 rgba(255, 255, 255, .025), 0 14px 40px rgba(0, 0, 0, .14);
}
.card + .card,
.grid + .card,
.card + .grid,
.grid + .grid {
margin-top: 20px;
}
.grid {
gap: 20px;
}
.card-head {
align-items: flex-start;
margin-bottom: 18px;
}
.card-head h2 {
font-size: 15.5px;
}
.card-head p {
margin-top: 4px;
font-size: 13px;
}
.card-foot {
margin-top: 18px;
padding-top: 16px;
}
.tiles {
gap: 14px;
}
.tile {
min-height: 112px;
padding: 16px 17px;
border-radius: 12px;
background: linear-gradient(145deg, var(--surface-lift), var(--surface));
}
.tile b {
font-size: 27px;
}
.filters {
gap: 12px;
padding: 16px;
border-radius: 12px;
}
.filters .field {
flex: 1 1 calc(50% - 6px);
max-width: none;
}
.filters .field.grow {
flex-basis: 100%;
}
.list-row {
min-height: 64px;
padding: 14px 20px;
}
.list-item {
min-height: 60px;
padding: 13px 0;
}
.loghead,
.logbody {
min-width: 680px;
}
.loghead,
.logline[data-virtual="true"] {
grid-template-columns: 128px 44px minmax(160px, 1fr) minmax(220px, 1.4fr);
gap: 10px;
}
.logview {
height: min(66dvh, 720px);
}
}
@media (max-width: 820px) {
.page-head-row {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 14px;
}
.page-head-row .page-head-actions {
width: 100%;
margin-left: 0;
}
.card {
padding: 17px 18px;
border-radius: 11px;
}
.card-head {
align-items: flex-start;
flex-wrap: wrap;
}
.card-head-actions {
width: 100%;
margin-left: 41px;
}
.card-foot > button,
.card-foot > .btn {
flex: 1 1 auto;
}
.tile {
min-width: 0;
min-height: 104px;
padding: 14px;
background: linear-gradient(145deg, var(--surface-lift), var(--surface));
}
.tile b {
font-size: 23px;
}
.tile span {
overflow-wrap: anywhere;
}
.filters .field,
.filters .field.grow {
flex: 1 1 100%;
max-width: none;
}
.filters .filter-actions {
width: 100%;
margin-left: 0;
}
.list-item,
.list-row,
.list-main {
align-items: flex-start;
}
.list-item,
.list-row {
flex-wrap: wrap;
}
.list-item .list-actions,
.list-row .list-actions {
width: 100%;
justify-content: flex-start;
padding-left: 46px;
}
.kv-row {
align-items: flex-start;
}
.dialog {
width: 100%;
max-height: min(88dvh, 720px);
padding: 22px;
border-radius: 16px;
}
.scrim {
align-items: end;
padding: 12px max(12px, var(--safe-right)) max(12px, var(--safe-bottom)) max(12px, var(--safe-left));
}
.dialog-actions > button {
flex: 1 1 0;
}
.bell-panel {
position: fixed;
top: calc(var(--top) + 8px);
right: max(12px, var(--safe-right));
left: max(12px, var(--safe-left));
width: auto;
max-height: calc(100dvh - var(--top) - var(--safe-bottom) - 20px);
border-radius: 14px;
}
.bell-list {
max-height: calc(100dvh - var(--top) - var(--safe-bottom) - 120px);
}
.toasts {
right: max(12px, var(--safe-right));
bottom: max(12px, var(--safe-bottom));
left: max(12px, var(--safe-left));
max-width: none;
}
}
/* Coarse pointers need a target, not merely larger text. This is deliberately scoped to
tablet widths so a desktop browser with touch support keeps the compact operations UI. */
@media (pointer: coarse) and (max-width: 1400px) {
button,
.btn {
min-width: 44px;
height: 44px;
padding-right: 15px;
padding-left: 15px;
}
button[data-size="sm"] {
min-width: 38px;
height: 38px;
padding-right: 11px;
padding-left: 11px;
}
.topbar .rail-toggle,
.topbar .bell-button,
.topbar .topbar-status,
.topbar form button {
width: 44px;
height: 44px;
padding: 0;
}
.topbar-status b {
margin-left: 1px;
}
.rail a {
min-height: 46px;
padding: 10px 12px;
border-radius: 9px;
font-size: 14px;
}
.rail-head {
min-height: 42px;
padding: 12px;
}
input[type="text"],
input[type="password"],
input[type="search"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="url"],
select {
height: 44px;
font-size: 16px;
}
select {
background-position: calc(100% - 16px) 20px, calc(100% - 11px) 20px;
}
textarea {
min-height: 104px;
font-size: 16px;
}
.check {
min-height: 52px;
padding-top: 14px;
padding-bottom: 14px;
}
.check .switch {
width: 40px;
height: 24px;
flex-basis: 40px;
}
.check .switch::after {
width: 18px;
height: 18px;
}
.check input:checked + .switch::after {
transform: translateX(16px);
}
.segments {
max-width: 100%;
overflow-x: auto;
}
.segments button {
height: 38px;
}
.crumb,
.crumbs a,
.table-row-link {
display: inline-flex;
align-items: center;
min-height: 36px;
}
}
@media (hover: none) {
tbody tr:hover td,
.list-row:hover,
.bell-item:hover,
a.tile:hover {
background: inherit;
}
}