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

3825 lines
86 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
2026-08-18 08:41:48 +12:00
derived: the library is teal, a person violet, a television blue, on every page.
Beside the accent is one quieter green, `idle`, which is not a seventh meaning but the
accent's meaning held at lower confidence: good, but not this minute. It exists so that
"fine and not connected right now" can stop borrowing amber, which is reserved for
something an operator should look at. Nothing derives it either. */
2026-08-14 09:40:03 +12:00
: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);
2026-08-18 08:41:48 +12:00
/* The quiet green: fine, but not this minute. Same hue as the accent and deliberately
duller, because it is the accent's own meaning at lower confidence rather than a
verdict of its own — a television in ordinary use that happens to be switched off is
not something an operator has to do anything about, and amber said it was. */
--idle: #4a8f60;
--idle-ink: #86c79a;
--idle-wash: rgba(74, 143, 96, 0.13);
2026-08-14 09:40:03 +12:00
--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 {
2026-08-16 21:20:55 +12:00
display: grid;
place-items: center;
width: 38px;
height: 38px;
flex: 0 0 38px;
padding: 0;
border: 1px solid transparent;
2026-08-14 09:40:03 +12:00
border-radius: var(--radius-sm);
background: transparent;
color: inherit;
cursor: pointer;
2026-08-16 12:13:51 +12:00
transition: background .16s ease, border-color .16s ease, color .16s ease;
2026-08-14 09:40:03 +12:00
}
.topbar-status:hover:not(:disabled),
.topbar-status:focus-visible {
background: var(--surface-lift);
}
.topbar-status:disabled {
2026-08-16 21:20:55 +12:00
cursor: default;
opacity: 1;
2026-08-14 09:40:03 +12:00
}
.topbar-status .dot {
2026-08-16 21:20:55 +12:00
width: 9px;
height: 9px;
2026-08-14 09:40:03 +12:00
border-radius: 50%;
background: var(--quiet);
2026-08-16 21:20:55 +12:00
box-shadow: 0 0 0 3px rgba(125, 133, 144, .12);
2026-08-14 09:40:03 +12:00
}
.topbar-status[data-tone="ok"] .dot {
2026-08-16 21:20:55 +12:00
background: var(--accent-ink);
box-shadow: 0 0 0 3px var(--accent-wash), 0 0 9px rgba(86, 211, 100, .28);
2026-08-14 09:40:03 +12:00
}
.topbar-status[data-tone="bad"] .dot {
background: var(--danger);
2026-08-16 21:20:55 +12:00
box-shadow: 0 0 0 3px var(--danger-wash);
2026-08-14 09:40:03 +12:00
}
2026-08-16 12:13:51 +12:00
/* The account is an identity first and an exit second. Keeping sign-out in this menu
prevents an unexplained door icon from competing with the operational controls. */
.account-menu {
position: relative;
2026-08-16 21:20:55 +12:00
display: flex;
align-items: center;
height: 38px;
2026-08-16 12:13:51 +12:00
flex: 0 0 auto;
}
.account-trigger {
max-width: 190px;
height: 38px;
padding: 0 9px 0 5px;
border-color: var(--line);
background: var(--surface);
color: var(--muted);
}
.account-trigger:hover:not(:disabled),
.account-menu[data-open="true"] .account-trigger {
border-color: rgba(86, 211, 100, .5);
background: var(--surface-hi);
color: var(--text);
}
.account-avatar {
display: grid;
place-items: center;
width: 27px;
height: 27px;
flex: 0 0 27px;
border: 1px solid rgba(86, 211, 100, .36);
border-radius: 50%;
background: var(--accent-wash);
color: var(--accent-ink);
font-size: 11px;
font-weight: 750;
}
.account-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12.5px;
}
.account-caret {
width: 12px;
height: 12px;
transition: transform .16s ease;
}
.account-menu[data-open="true"] .account-caret {
transform: rotate(180deg);
}
.account-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: min(280px, calc(100vw - 24px));
padding: 7px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--surface);
box-shadow: 0 18px 44px rgba(0, 0, 0, .55);
z-index: 40;
}
.account-identity {
display: flex;
align-items: center;
gap: 11px;
min-width: 0;
padding: 9px 10px 12px;
border-bottom: 1px solid var(--line-soft);
}
.account-avatar-large {
width: 34px;
height: 34px;
flex-basis: 34px;
font-size: 13px;
}
.account-identity span:last-child {
min-width: 0;
}
.account-identity small,
.account-identity b {
display: block;
}
.account-identity small {
color: var(--quiet);
font-size: 11px;
}
.account-identity b {
overflow: hidden;
text-overflow: ellipsis;
font-size: 13.5px;
}
2026-08-17 19:09:17 +12:00
/* A link and a submit button sit in this menu side by side, and they have to be one row
shape: the operator reads them as two entries of the same list, not as a link above a
form. */
.account-panel > a,
.account-panel form button {
display: flex;
align-items: center;
gap: 9px;
justify-content: flex-start;
width: 100%;
height: 38px;
padding: 0 10px;
border-radius: 8px;
color: var(--muted);
font-size: 13px;
text-decoration: none;
}
.account-panel > a:hover {
background: var(--surface-hi);
color: var(--text);
}
.account-panel > a {
2026-08-16 12:13:51 +12:00
margin-top: 6px;
}
2026-08-17 19:09:17 +12:00
.account-panel form {
margin-top: 2px;
}
2026-08-16 12:13:51 +12:00
.account-panel form button {
justify-content: flex-start;
width: 100%;
height: 38px;
border-color: transparent;
background: transparent;
color: var(--muted);
}
.account-panel form button:hover:not(:disabled) {
background: var(--surface-hi);
color: var(--text);
}
2026-08-14 09:40:03 +12:00
@media (max-width: 900px) {
2026-08-16 21:20:55 +12:00
.topbar-version {
2026-08-14 09:40:03 +12:00
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);
2026-08-16 21:20:55 +12:00
right: auto;
left: 0;
width: 100%;
max-height: min(50vh, 420px);
2026-08-14 09:40:03 +12:00
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;
2026-08-16 21:20:55 +12:00
display: flex;
align-items: center;
height: 38px;
2026-08-14 09:40:03 +12:00
}
.bell-button {
position: relative;
display: grid;
place-items: center;
2026-08-16 21:20:55 +12:00
width: 38px;
height: 38px;
2026-08-14 09:40:03 +12:00
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);
2026-08-16 12:13:51 +12:00
min-width: 0;
2026-08-14 09:40:03 +12:00
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);
}
2026-08-18 08:41:48 +12:00
.glyph[data-tone="idle"] {
background: var(--idle-wash);
color: var(--idle-ink);
}
2026-08-14 09:40:03 +12:00
.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);
}
2026-08-18 08:41:48 +12:00
.tag[data-tone="idle"] {
border-color: rgba(74, 143, 96, 0.35);
background: var(--idle-wash);
color: var(--idle-ink);
}
2026-08-14 09:40:03 +12:00
.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);
}
2026-08-18 08:41:48 +12:00
.chip[data-tone="idle"] {
color: var(--idle-ink);
}
2026-08-14 09:40:03 +12:00
.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-key,
2026-08-16 21:20:55 +12:00
.topbar-version {
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 {
2026-08-16 21:20:55 +12:00
width: 40px;
flex-basis: 40px;
2026-08-15 09:23:26 +12:00
min-width: 40px;
height: 40px;
border: 1px solid var(--line);
background: var(--surface);
2026-08-14 09:40:03 +12:00
}
2026-08-16 21:20:55 +12:00
.omni-input,
.bell,
.bell-button,
.account-menu,
.account-trigger {
height: 40px;
2026-08-16 12:13:51 +12:00
}
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
2026-08-16 12:13:51 +12:00
@media (max-width: 1100px) {
.account-trigger {
width: 40px;
padding: 0;
}
.account-name,
.account-caret {
display: none;
}
}
2026-08-15 09:23:26 +12:00
@media (max-width: 820px) {
2026-08-16 21:20:55 +12:00
.brand-word {
2026-08-15 09:23:26 +12:00
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-16 12:13:51 +12:00
2026-08-16 21:20:55 +12:00
/* A phone keeps every global control on one row. Search is the only flexible item: the
navigation and identity controls retain useful targets while the field gives up width
before the header can wrap. Its relative wrapper still anchors the results directly
beneath the field at exactly the width available here. */
2026-08-16 12:13:51 +12:00
@media (max-width: 680px) {
:root {
2026-08-16 21:20:55 +12:00
--top: calc(60px + var(--safe-top));
2026-08-16 12:13:51 +12:00
}
.topbar {
align-items: center;
2026-08-16 21:20:55 +12:00
flex-wrap: nowrap;
gap: clamp(4px, 1.5vw, 8px);
padding: calc(var(--safe-top) + 8px) max(8px, var(--safe-right)) 8px max(8px, var(--safe-left));
2026-08-16 12:13:51 +12:00
}
.rail-toggle { order: 1; }
2026-08-16 21:20:55 +12:00
.topbar-brand {
order: 2;
height: 40px;
flex: 0 0 auto;
2026-08-16 12:13:51 +12:00
}
.topbar .omni {
2026-08-16 21:20:55 +12:00
order: 3;
flex: 1 1 96px;
width: 0;
min-width: 0;
2026-08-16 12:13:51 +12:00
max-width: none;
}
2026-08-16 21:20:55 +12:00
.topbar-status {
order: 4;
margin-left: 0;
2026-08-16 12:13:51 +12:00
}
2026-08-16 21:20:55 +12:00
.bell { order: 5; }
.account-menu { order: 6; }
.brand-mark {
width: 30px;
height: 30px;
flex-basis: 30px;
2026-08-16 12:13:51 +12:00
}
.account-panel,
.bell-panel {
position: fixed;
top: calc(var(--top) + 8px);
}
.account-panel {
right: max(12px, var(--safe-right));
}
}
@media (max-width: 370px) {
.tiles {
grid-template-columns: minmax(0, 1fr);
}
}
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 ---------- */
2026-08-18 08:41:48 +12:00
/* The log table.
Six columns, in the order the questions are asked: when, how bad, which part of the
server, what happened, did it work, how long did it take. The columns are the whole
design — an operator learns where to look once and then reads down a column rather than
across a line, which is what makes thirty seconds of watching this page worth anything.
Monospace is spent deliberately rather than applied to the row. The time, the method,
the status and the duration are values to be compared down a column and are set in it;
the summary is a sentence and is set in the sans face, because a sentence in monospace
is slower to read and looks like a dump of something. */
.logbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-bottom: 10px;
}
.logbar-filters {
display: flex;
flex: 1 1 520px;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-width: 0;
}
.logbar-filters select {
width: auto;
min-width: 0;
height: 30px;
padding: 0 26px 0 9px;
font-size: 12px;
}
/* The actions are secondary to the filters and are marked so by weight, not by distance:
the previous bar gave Pause and Export the same prominence as the controls that decide
what the table contains. */
.logbar-actions {
display: flex;
flex: 0 0 auto;
gap: 6px;
margin-left: auto;
}
.logsearch {
position: relative;
display: flex;
flex: 1 1 260px;
align-items: center;
min-width: 200px;
}
.logsearch svg {
position: absolute;
left: 9px;
width: 14px;
height: 14px;
color: var(--quiet);
pointer-events: none;
}
.logsearch input {
width: 100%;
height: 30px;
padding-left: 28px;
font-size: 12px;
}
/* One chip per narrowing in force. They are the only place a filter can be removed one at
a time, which is what makes drilling in by clicking a value in the table reversible. */
.logchips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.logchip {
display: inline-flex;
align-items: center;
gap: 5px;
height: 24px;
padding: 0 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface-lift);
color: var(--muted);
font: 500 11px/1 var(--sans);
}
.logchip svg {
width: 11px;
height: 11px;
opacity: .7;
}
.logchip:hover:not(:disabled) {
border-color: var(--danger);
background: var(--danger-wash);
color: var(--danger-ink);
}
.logchip-clear {
border-style: dashed;
color: var(--quiet);
}
.logshell {
position: relative;
}
2026-08-14 09:40:03 +12:00
.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;
2026-08-18 08:41:48 +12:00
font: 12px/1.5 var(--sans);
2026-08-15 09:23:26 +12:00
contain: layout paint style;
2026-08-14 09:40:03 +12:00
}
2026-08-19 06:57:59 +12:00
/* Every track but the event is a fixed width, and that is the whole of why the columns
line up. A row is its own grid container — there are twenty thousand of them and no
shared table — so a `minmax()` track sized from its own content gave each row a slightly
different Service and Result column, and the Result column moved as the widest verdict
on screen changed. Fixed tracks cannot move; content inside them truncates instead.
Vertical rhythm is set here too: cells start at the top of the row rather than being
centred in it, so the Time, Level, Service and Result of a two-line event sit on the
same line as its summary instead of dropping half a line to the middle of the pair. */
2026-08-18 08:41:48 +12:00
.loghead,
.logrow {
display: grid;
2026-08-19 06:57:59 +12:00
grid-template-columns: 92px 52px 190px minmax(240px, 1fr) 150px 68px;
2026-08-18 08:41:48 +12:00
gap: 12px;
padding: 0 12px;
}
2026-08-14 11:47:32 +12:00
.loghead {
position: sticky;
top: 0;
2026-08-18 08:41:48 +12:00
z-index: 2;
align-items: center;
height: 31px;
2026-08-14 11:47:32 +12:00
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-18 08:41:48 +12:00
min-width: 900px;
}
.loghead > :last-child {
text-align: right;
2026-08-15 09:23:26 +12:00
}
.logbody {
position: relative;
2026-08-18 08:41:48 +12:00
min-width: 900px;
2026-08-14 11:47:32 +12:00
}
2026-08-18 08:41:48 +12:00
/* The date is a divider between days rather than a column repeated on every row. A log
watched live is almost always one day; printing the date 20,000 times to cover the
handful of rows where it changes is the trade the compact timestamp exists to avoid. */
.logday {
2026-08-15 09:23:26 +12:00
position: absolute;
top: 0;
right: 0;
left: 0;
2026-08-18 08:41:48 +12:00
display: flex;
2026-08-15 09:23:26 +12:00
align-items: center;
2026-08-18 08:41:48 +12:00
height: 26px;
padding: 0 12px;
background: linear-gradient(to bottom, rgba(255, 255, 255, .03), transparent);
color: var(--quiet);
font: 700 10px/1 var(--sans);
letter-spacing: .08em;
text-transform: uppercase;
}
.logday span {
padding-right: 10px;
background: #080a0e;
}
.logday::after {
content: '';
flex: 1;
height: 1px;
background: var(--line-soft);
}
2026-08-19 06:57:59 +12:00
/* The figures below are the twin of the row geometry in `pages/Logs.tsx`: 7px of padding,
a 16px first line, a 2px gap and a 15px second line, over a 1px hairline. The row's
height is set inline from those constants, so a cell drawn taller than its share here is
text sliced through the middle rather than a row that grows. Every first-line cell is
therefore given `line-height: 16px` explicitly, whatever its font size. */
2026-08-18 08:41:48 +12:00
.logrow {
position: absolute;
top: 0;
right: 0;
left: 0;
2026-08-19 06:57:59 +12:00
align-items: start;
padding-top: 7px;
padding-bottom: 7px;
2026-08-18 08:41:48 +12:00
border-bottom: 1px solid rgba(255, 255, 255, .03);
2026-08-15 09:23:26 +12:00
contain: strict;
}
2026-08-19 06:57:59 +12:00
.logrow > *,
.logrow-line {
line-height: 16px;
}
2026-08-18 08:41:48 +12:00
.logrow:hover {
background: rgba(255, 255, 255, .035);
2026-08-14 09:40:03 +12:00
}
2026-08-18 08:41:48 +12:00
.logrow[data-selected] {
background: var(--accent-wash);
}
/* A failure is marked on the row and not only in a column, because the thing an operator
is scanning for is a row, and a hairline at the start of one is found faster than a word
two thirds of the way along it. INFO is deliberately unmarked: most of the table is INFO
and a mark every row carries is a mark that says nothing. */
.logrow[data-level="ERROR"] {
box-shadow: inset 2px 0 0 var(--danger);
background: rgba(229, 83, 75, .05);
}
.logrow[data-level="WARN"] {
box-shadow: inset 2px 0 0 var(--warn);
}
.logrow-time {
2026-08-14 09:40:03 +12:00
color: var(--quiet);
2026-08-19 06:57:59 +12:00
font: 11px/16px var(--mono);
2026-08-18 08:41:48 +12:00
font-variant-numeric: tabular-nums;
2026-08-14 09:40:03 +12:00
}
2026-08-18 08:41:48 +12:00
/* Every structured value in the table is also the control that filters to it. They are
buttons rather than links because nothing navigates: the table narrows in place. */
.logfacet {
height: auto;
2026-08-14 09:40:03 +12:00
min-width: 0;
2026-08-15 09:23:26 +12:00
padding: 0;
overflow: hidden;
border: 0;
2026-08-18 08:41:48 +12:00
background: none;
color: inherit;
2026-08-15 09:23:26 +12:00
font: inherit;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
2026-08-18 08:41:48 +12:00
.logfacet:hover:not(:disabled) {
2026-08-15 09:23:26 +12:00
border: 0;
2026-08-18 08:41:48 +12:00
background: none;
text-decoration: underline;
text-underline-offset: 3px;
2026-08-15 09:23:26 +12:00
}
2026-08-18 08:41:48 +12:00
.logfacet:focus-visible {
2026-08-15 09:23:26 +12:00
outline: 1px solid var(--accent);
outline-offset: 2px;
}
2026-08-18 08:41:48 +12:00
.logrow-level {
color: var(--quiet);
2026-08-19 06:57:59 +12:00
font: 700 10px/16px var(--sans);
2026-08-18 08:41:48 +12:00
letter-spacing: .06em;
}
.logrow-level[data-level="ERROR"] { color: var(--danger-ink); }
.logrow-level[data-level="WARN"] { color: var(--warn-ink); }
.logrow-level[data-level="INFO"] { color: var(--muted); }
.logrow-level[data-level="DEBUG"],
.logrow-level[data-level="TRACE"] { color: var(--quiet); }
/* Service and component. The service is the recognisable half — one word, upper case,
always the same colour for the same subsystem — and the component sits beside it in the
ordinary text colour, so the pair reads as a place rather than as two tags. The tones
are the console's secondary palette only: green, amber and red mean good, look and wrong
on every other page and must not start meaning "playback" on this one. */
.logrow-place {
display: flex;
2026-08-19 06:57:59 +12:00
align-items: center;
2026-08-18 08:41:48 +12:00
gap: 5px;
min-width: 0;
2026-08-19 06:57:59 +12:00
height: 16px;
overflow: hidden;
2026-08-18 08:41:48 +12:00
}
.logrow-service {
flex: 0 0 auto;
max-width: 96px;
overflow: hidden;
color: var(--quiet);
2026-08-19 06:57:59 +12:00
font: 700 10px/16px var(--sans);
2026-08-18 08:41:48 +12:00
letter-spacing: .07em;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.logrow-service[data-tone="info"] { color: var(--info-ink); }
.logrow-service[data-tone="note"] { color: var(--note-ink); }
.logrow-service[data-tone="data"] { color: var(--data-ink); }
.logrow-service[data-tone="idle"] { color: var(--idle-ink); }
.logrow-service[data-tone="quiet"] { color: var(--muted); }
.logrow-sep {
flex: 0 0 auto;
color: var(--line);
}
.logrow-component {
flex: 1 1 auto;
color: var(--quiet);
font-size: 11.5px;
2026-08-19 06:57:59 +12:00
line-height: 16px;
2026-08-18 08:41:48 +12:00
}
/* The summary is the row. It is a button because selecting the row is what opens the
record, and the whole width of the column should be the target. */
.logrow-summary {
display: flex;
flex-direction: column;
gap: 2px;
2026-08-19 06:57:59 +12:00
align-items: stretch;
justify-content: flex-start;
2026-08-18 08:41:48 +12:00
width: 100%;
min-width: 0;
padding: 0;
overflow: hidden;
border: 0;
background: none;
font: inherit;
text-align: left;
}
.logrow-summary:hover:not(:disabled) {
border: 0;
background: none;
}
.logrow-summary:focus-visible {
outline: 1px solid var(--accent);
outline-offset: -1px;
}
.logrow-line {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
2026-08-19 06:57:59 +12:00
height: 16px;
2026-08-18 08:41:48 +12:00
}
.logrow-action {
flex: 0 0 auto;
color: var(--muted);
2026-08-19 06:57:59 +12:00
font: 600 10.5px/16px var(--mono);
2026-08-18 08:41:48 +12:00
letter-spacing: .04em;
}
.logrow-action[data-method="POST"],
.logrow-action[data-method="PUT"],
.logrow-action[data-method="PATCH"] { color: var(--info-ink); }
.logrow-action[data-method="DELETE"] { color: var(--danger-ink); }
.logrow-text {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
color: var(--text);
text-overflow: ellipsis;
white-space: nowrap;
}
2026-08-19 06:57:59 +12:00
/* Identity on a request row, kept on the summary's own line. It gives way before the path
does — knowing which route was called matters more than which television called it, and
the drawer holds both either way. */
.logrow-trail {
flex: 0 1 auto;
min-width: 0;
max-width: 38%;
2026-08-18 08:41:48 +12:00
overflow: hidden;
color: var(--quiet);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
2026-08-19 06:57:59 +12:00
/* The second line: the reason a failure needs no drawer, or the person and position that
make an application event mean something. Exactly one line tall, 15px, which is the
figure the row was measured with — it can truncate but it can never wrap, because a wrap
is a row overflowing into the one below it. */
.logrow-second {
height: 15px;
overflow: hidden;
color: var(--quiet);
font-size: 11px;
line-height: 15px;
text-overflow: ellipsis;
white-space: nowrap;
}
.logrow-second[data-tone="error"] {
color: var(--danger-ink);
}
2026-08-18 08:41:48 +12:00
/* A result is a word, not a badge. A success is understated to the point of being ignorable
— which is the correct amount of attention for the four hundredth 200 in a row — and only
a failure is given a fill. */
.logrow-result {
min-width: 0;
2026-08-19 06:57:59 +12:00
height: 16px;
overflow: hidden;
2026-08-18 08:41:48 +12:00
}
.logrow-verdict {
display: inline-block;
max-width: 100%;
color: var(--quiet);
2026-08-19 06:57:59 +12:00
font: 500 11px/14px var(--mono);
2026-08-18 08:41:48 +12:00
}
.logrow-verdict[data-tone="ok"] { color: var(--muted); }
.logrow-verdict[data-tone="info"] { color: var(--info-ink); }
.logrow-verdict[data-tone="data"] { color: var(--data-ink); }
2026-08-19 06:57:59 +12:00
/* A fill still has to fit the row's line box: 14px of text and 1px either side is the
16px every other cell on the line occupies. */
2026-08-18 08:41:48 +12:00
.logrow-verdict[data-tone="warn"],
.logrow-verdict[data-tone="bad"] {
padding: 1px 6px;
border-radius: var(--radius-xs);
font-weight: 600;
}
.logrow-verdict[data-tone="warn"] {
background: var(--warn-wash);
color: var(--warn-ink);
}
.logrow-verdict[data-tone="bad"] {
background: var(--danger-wash);
color: var(--danger-ink);
}
.logrow-duration {
color: var(--quiet);
2026-08-19 06:57:59 +12:00
font: 11px/16px var(--mono);
2026-08-18 08:41:48 +12:00
font-variant-numeric: tabular-nums;
text-align: right;
}
.logrow-duration[data-tone="warn"] { color: var(--warn-ink); }
.logrow-duration[data-tone="bad"] {
color: var(--danger-ink);
font-weight: 600;
}
/* Offered only once the operator has left the tail, which is the only time following the
log automatically would be taking the page away from them. */
.logtail {
position: absolute;
right: 18px;
bottom: 14px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 6px;
height: 28px;
padding: 0 12px;
border: 1px solid var(--accent);
border-radius: 999px;
background: var(--surface-lift);
color: var(--accent-ink);
font: 600 11px/1 var(--sans);
box-shadow: 0 6px 16px rgba(0, 0, 0, .5);
}
.logtail svg {
width: 12px;
height: 12px;
transform: rotate(90deg);
}
.logtail:hover:not(:disabled) {
border-color: var(--accent);
background: var(--accent-wash);
color: var(--accent-ink);
}
/* The drawer answers "exactly how and why". Sections rather than one alphabetical list,
and a field with no value is omitted rather than printed as `unknown`. */
.logdrawer {
2026-08-15 09:23:26 +12:00
margin-top: 12px;
2026-08-18 08:41:48 +12:00
padding: 14px;
2026-08-15 09:23:26 +12:00
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
}
2026-08-18 08:41:48 +12:00
.logdrawer-head {
2026-08-15 09:23:26 +12:00
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
2026-08-18 08:41:48 +12:00
padding-bottom: 12px;
border-bottom: 1px solid var(--line);
2026-08-15 09:23:26 +12:00
}
2026-08-18 08:41:48 +12:00
.logdrawer-head > div:first-child {
2026-08-15 09:23:26 +12:00
min-width: 0;
}
2026-08-18 08:41:48 +12:00
.logdrawer-head b {
2026-08-15 09:23:26 +12:00
display: block;
2026-08-18 08:41:48 +12:00
margin-top: 4px;
font-size: 14px;
2026-08-15 09:23:26 +12:00
overflow-wrap: anywhere;
}
2026-08-18 08:41:48 +12:00
.logdrawer-place {
display: flex;
align-items: baseline;
gap: 5px;
margin: 0;
2026-08-15 09:23:26 +12:00
color: var(--quiet);
2026-08-18 08:41:48 +12:00
font-size: 11px;
2026-08-15 09:23:26 +12:00
}
2026-08-18 08:41:48 +12:00
.logdrawer-error {
margin: 6px 0 0;
color: var(--danger-ink);
font: 12px/1.5 var(--mono);
overflow-wrap: anywhere;
}
.logdrawer-actions {
display: flex;
flex: 0 0 auto;
gap: 6px;
}
.logdrawer-grid {
2026-08-15 09:23:26 +12:00
display: grid;
2026-08-18 08:41:48 +12:00
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px 24px;
margin-top: 12px;
}
.logdrawer-section h4 {
margin: 0 0 6px;
color: var(--quiet);
font: 700 10px/1 var(--sans);
letter-spacing: .08em;
text-transform: uppercase;
}
.logdrawer-section dl {
display: grid;
grid-template-columns: minmax(88px, max-content) minmax(0, 1fr);
gap: 4px 12px;
margin: 0;
2026-08-15 09:23:26 +12:00
font: 12px/1.5 var(--mono);
}
2026-08-18 08:41:48 +12:00
.logdrawer-section dt { color: var(--muted); }
.logdrawer-section dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
.logdrawer-raw {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.logdrawer-raw summary {
cursor: pointer;
color: var(--quiet);
font: 700 10px/1 var(--sans);
letter-spacing: .08em;
text-transform: uppercase;
}
.logdrawer-raw pre {
max-height: 320px;
margin: 10px 0 0;
overflow: auto;
padding: 10px;
border: 1px solid var(--line);
border-radius: var(--radius-xs);
background: #080a0e;
font: 11px/1.6 var(--mono);
}
/* Columns are given up in the order they are least missed: the duration first, since it is
the only one of the six whose absence costs nothing to understanding what happened, and
the timestamp after it. Level, service and the summary are never dropped — a log that
cannot say how bad, where, or what is not a log. */
@media (max-width: 1180px) {
.loghead,
.logrow {
2026-08-19 06:57:59 +12:00
grid-template-columns: 84px 46px 170px minmax(200px, 1fr) 130px;
2026-08-18 08:41:48 +12:00
gap: 10px;
}
.loghead > :last-child,
.logrow-duration {
display: none;
}
.loghead,
.logbody {
2026-08-19 06:57:59 +12:00
min-width: 680px;
2026-08-18 08:41:48 +12:00
}
}
@media (max-width: 900px) {
.loghead,
.logrow {
2026-08-19 06:57:59 +12:00
grid-template-columns: 46px 140px minmax(180px, 1fr) 110px;
2026-08-18 08:41:48 +12:00
gap: 8px;
}
.loghead > :first-child,
.logrow-time {
display: none;
}
.loghead,
.logbody {
min-width: 520px;
}
}
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;
}
2026-08-18 08:41:48 +12:00
/* The presence dot: four-valued, and the tone is passed rather than derived. The two
greens are the point — one set is connected now and the other was an hour ago, and both
are fine, so they read as the same answer at two strengths rather than as two answers.
The dot is 7px and colour alone is never the whole signal here: every one of these
carries the wording in its title. */
2026-08-14 09:40:03 +12:00
.dot-state {
width: 7px;
height: 7px;
flex: 0 0 7px;
border-radius: 50%;
background: var(--quiet);
}
.dot-state[data-tone="ok"] {
background: var(--accent);
}
2026-08-18 08:41:48 +12:00
.dot-state[data-tone="idle"] {
background: var(--idle);
}
2026-08-14 09:40:03 +12:00
.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. */
2026-08-16 12:13:51 +12:00
@media (pointer: coarse) and (min-width: 821px) and (max-width: 1400px) {
2026-08-15 09:23:26 +12:00
.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;
}
2026-08-18 08:41:48 +12:00
/* The column collapse itself is width-driven and lives beside the table's own rules;
what a touch device needs on top of it is room to scroll and controls it can reach. */
2026-08-15 09:23:26 +12:00
.logview {
height: min(66dvh, 720px);
}
2026-08-18 08:41:48 +12:00
.logbar-actions {
margin-left: 0;
}
2026-08-15 09:23:26 +12:00
}
@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%;
2026-08-16 12:13:51 +12:00
margin-left: 0;
2026-08-15 09:23:26 +12:00
}
.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,
2026-08-16 12:13:51 +12:00
.topbar .account-trigger {
2026-08-15 09:23:26 +12:00
width: 44px;
height: 44px;
padding: 0;
}
2026-08-16 21:20:55 +12:00
.topbar .bell,
.topbar .account-menu {
height: 44px;
}
.topbar .omni-input {
height: 44px;
2026-08-15 09:23:26 +12:00
}
.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;
}
}
2026-08-16 12:13:51 +12:00
/* Short laptop screens benefit from vertical density, while retaining the same readable
widths and full-size pointer targets. */
@media (pointer: fine) and (min-width: 821px) and (max-height: 800px) {
.page {
padding-top: 20px;
padding-bottom: 52px;
}
.page-head {
margin-bottom: 16px;
}
.card {
padding-top: 14px;
padding-bottom: 14px;
}
.card-head {
margin-bottom: 11px;
}
}
2026-08-15 09:23:26 +12:00
@media (hover: none) {
tbody tr:hover td,
.list-row:hover,
.bell-item:hover,
a.tile:hover {
background: inherit;
}
}
2026-08-19 06:57:59 +12:00
/* The notification history's detail row.
*
* The drawer is a row inside the table rather than a panel beside it, so the record stays
* under the row it belongs to while an operator reads down a filtered list. It borrows the
* log viewer's drawer vocabulary wholesale — the two answer the same shape of question and
* a second look for it would be a second thing to keep in step. */
.detail-row > td {
padding: 0 0 12px;
background: var(--surface-sunken, transparent);
}
.logdrawer-raw h4 {
margin: 0 0 6px;
color: var(--quiet);
font: 700 10px/1 var(--sans);
letter-spacing: .08em;
text-transform: uppercase;
}
/* The message is the one thing on the page rendered as prose rather than as a field: it is
the sentence a viewer actually read, and setting it in the mono field type would make it
look like a value rather than like the notification it is. */
.notification-body {
max-width: 70ch;
margin: 0;
color: var(--text);
font: 13px/1.6 var(--sans);
overflow-wrap: anywhere;
}
2026-08-19 14:25:44 +12:00
/* ---------- runtime ----------
The verdict block and the section headings the runtime pages are built from. The verdict
is deliberately not a `.banner`: a banner is something that has gone wrong and can be
dismissed, and this is a standing answer that is usually good news. */
.subhead {
display: flex;
align-items: baseline;
gap: 8px;
margin: 6px 0 0;
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--muted);
}
.subhead small {
font-size: 11px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
color: var(--quiet);
}
.verdict {
margin: 0 0 16px;
padding: 12px 14px;
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
border-radius: var(--radius-sm);
background: var(--surface-lift);
}
.verdict[data-tone="warn"] {
border-left-color: var(--warn);
}
.verdict[data-tone="bad"] {
border-left-color: var(--danger);
}
.verdict-head {
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
font-size: 13px;
color: var(--muted);
}
.verdict-head svg {
width: 17px;
height: 17px;
color: var(--accent);
}
.verdict[data-tone="warn"] .verdict-head svg {
color: var(--warn);
}
.verdict[data-tone="bad"] .verdict-head svg {
color: var(--danger);
}
.verdict-head b {
color: var(--text);
font-size: 13px;
}
.verdict-notes {
margin: 11px 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.verdict-notes li {
display: flex;
align-items: baseline;
gap: 9px;
font-size: 12.5px;
color: var(--muted);
}
2026-08-22 08:26:10 +12:00
/* ---------- metadata hero ---------- */
.metadata-hero-layout {
display: grid;
grid-template-columns: minmax(320px, .82fr) minmax(460px, 1.35fr);
gap: 16px;
align-items: start;
}
.metadata-hero-order {
display: grid;
gap: 8px;
}
.metadata-hero-order-row {
display: grid;
grid-template-columns: 30px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 10px;
border: 1px solid var(--line-soft);
border-radius: var(--radius-sm);
background: var(--surface-lift);
opacity: .62;
}
.metadata-hero-order-row[data-enabled] { opacity: 1; }
.metadata-hero-order-number {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--surface);
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.metadata-hero-order-actions { display: flex; gap: 5px; }
.metadata-hero-order-actions button { min-width: 34px; padding-inline: 8px; }
2026-08-22 11:32:50 +12:00
.metadata-hero-colour {
display: grid;
gap: 7px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--line-soft);
}
.metadata-hero-colour > b { font-size: 13.5px; }
.metadata-hero-colour > p { margin: 0; color: var(--muted); font-size: 12.5px; }
.metadata-hero-colour .segments { width: fit-content; }
2026-08-22 08:26:10 +12:00
.metadata-hero-preview {
position: relative;
isolation: isolate;
overflow: hidden;
aspect-ratio: 16 / 7.2;
min-height: 280px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: #080b0e;
}
.metadata-hero-preview::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(90deg, #080b0e 0 34%, rgba(8,11,14,.9) 48%, rgba(8,11,14,.2) 76%, transparent);
}
.metadata-hero-preview-art {
position: absolute;
inset: 0 0 0 36%;
z-index: -2;
background:
radial-gradient(circle at 67% 24%, rgba(211,242,255,.8) 0 2%, transparent 3%),
radial-gradient(ellipse at 76% 78%, rgba(71,107,92,.8), transparent 45%),
linear-gradient(145deg, #17303d 0%, #456273 42%, #101a1f 100%);
}
.metadata-hero-preview-copy {
display: flex;
flex-direction: column;
justify-content: center;
gap: 10px;
width: 54%;
min-height: 100%;
padding: 26px 0 24px 28px;
}
.metadata-preview-title {
max-width: 330px;
color: #fff;
font-family: Georgia, serif;
font-size: clamp(24px, 3vw, 42px);
line-height: .95;
letter-spacing: .03em;
}
.metadata-preview-ratings { display: flex; gap: 8px; }
.metadata-preview-ratings b {
padding: 5px 8px;
border-radius: 4px;
background: rgba(255,255,255,.11);
color: #e8edf0;
font-size: 11px;
}
.metadata-preview-facts {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: #bdc7ce;
font-size: 11px;
line-height: 16px;
white-space: nowrap;
}
.metadata-preview-facts span {
overflow: hidden;
text-overflow: ellipsis;
}
.metadata-preview-facts b {
flex: none;
padding: 1px 5px;
border: 1px solid rgba(255,255,255,.28);
border-radius: 3px;
background: rgba(0,0,0,.22);
color: #e8edf0;
font-size: inherit;
line-height: inherit;
}
.metadata-preview-reason {
overflow: hidden;
color: var(--accent);
font-size: 11px;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.metadata-preview-progress {
display: flex;
align-items: center;
gap: 10px;
color: #bdc7ce;
font-size: 11px;
line-height: 16px;
white-space: nowrap;
}
.metadata-preview-progress > span {
display: block;
overflow: hidden;
width: 112px;
height: 3px;
border-radius: 2px;
background: rgba(255,255,255,.22);
}
.metadata-preview-progress i {
display: block;
width: 59%;
height: 100%;
background: var(--accent);
}
2026-08-22 11:32:50 +12:00
.metadata-preview-progress b { color: var(--accent); font-size: inherit; line-height: inherit; }
.metadata-preview-progress[data-colour="white"] b { color: #fff; }
2026-08-22 08:26:10 +12:00
.metadata-preview-summary { margin: 0; color: #cbd2d7; font-size: 12px; line-height: 1.45; }
@media (max-width: 980px) {
.metadata-hero-layout { grid-template-columns: 1fr; }
}