Files
terdut-server/internal/web/static/app.css
T
Niklas Ye dc3879eca6 Serve a web UI for the incident queue, built for phones
Whoever is on call gets paged on a phone, and until now the only ways to
act on a page were the notification's Acknowledge button or a terminal.
Tapping the notification itself opened /api/incidents/{id}, which a
browser can only answer with a 401 in JSON. The server now serves a web
UI at / covering the incident queue, each incident's alerts and timeline
with every action on it, who is on call, the alert feed, and changing
your own password. The notification link now points at /incidents/{id}
in that UI.

It is embedded in the binary and has no build step: plain HTML, CSS and
ES modules under internal/web/static, served with an ETag per file and a
CSP that allows nothing from any other origin. That is how rd-web is
built. It avoids adding a node toolchain to the Dockerfile and the
pipeline for a page this size, and it keeps the page on the same origin
as the API, so no CORS is needed and nothing else has to be deployed.
Paths without a file extension fall back to index.html, so a deep link
survives a reload. An unknown path under /api/ still gets a JSON 404
rather than the page.

Signing in uses a username and password, because pasting a 64-character
API key into a phone at 3am is not a sign-in flow. Users have no
password until one is set through PUT /api/users/{id}/password, or
optionally at bootstrap. A user without a password is exactly where they
were before this commit and can only use API keys. A login sets an
HttpOnly, SameSite=Lax session cookie. It lasts 30 days and slides
forward while in use, so an on-call phone does not sign itself out.
Only the token's hash is stored, as for API keys.

The cookie needs a CSRF guard where a bearer header does not, because
browsers attach cookies to requests other sites make. So cookie-
authenticated requests go through Go 1.25's http.CrossOriginProtection,
and bearer requests do not. A request carrying an Authorization header
is judged on that header alone and never falls back to the cookie.
Changing a password ends every other session of that user. Changing
your own requires the current password, so a phone left signed in
cannot be used to take the account over.

Failed logins are counted per username and per client address. Ten
failures for one username in 15 minutes refuse that username for the
rest of the window, even with the right password. That makes locking
somebody out possible for anyone who knows their username. It was
accepted because the alternative is unlimited guessing, and during a
lockout the notification's Acknowledge button and API keys keep
working. The address limit reads the first X-Forwarded-For hop, since
behind the gateway RemoteAddr is Envoy. It is looser, because a whole
office behind one NAT shares it.

The Secure flag follows TERDUT_PUBLIC_URL, since TLS terminates at the
gateway and the server itself only ever sees plain HTTP. The chart
already defaults that variable to https://<hostname>.

Schedule editing, statistics and user management stay in terdut-tui for
now. The API they use is unchanged, and bearer authentication behaves
exactly as before.
2026-09-19 17:48:21 +02:00

623 lines
24 KiB
CSS

/* terdut web UI.
*
* Mobile first: one column, a top bar and a bottom tab bar. From 900px the tab
* bar becomes a sidebar and the queue shows list and detail side by side.
* Colour is reserved for severity and status; everything else is neutral.
*/
:root {
--bg: #f5f6f8;
--surface: #ffffff;
--surface-2: #eff1f4;
--surface-hover: #f7f8fa;
--border: #e2e5ea;
--border-strong: #cfd3da;
--text: #16181d;
--muted: #5b626e;
--faint: #8a909b;
--accent: #2f5bd3;
--accent-text: #ffffff;
--accent-soft: #e8eefc;
--crit: #d0342c;
--crit-soft: #fdecea;
--warn: #b86e00;
--warn-soft: #fdf3e1;
--info: #2f6fdf;
--info-soft: #e9f0fd;
--ok: #1d7f4c;
--ok-soft: #e6f5ec;
--snooze: #6b5bd2;
--snooze-soft: #efedfb;
--radius: 10px;
--radius-sm: 6px;
--shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 8%);
--shadow-lg: 0 12px 32px rgb(16 24 40 / 18%);
--font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--topbar-h: 52px;
--tabbar-h: 58px;
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1115;
--surface: #171a20;
--surface-2: #1f232b;
--surface-hover: #1c2027;
--border: #2a2f38;
--border-strong: #394050;
--text: #e7e9ed;
--muted: #a0a7b3;
--faint: #737a87;
--accent: #6d8ff0;
--accent-text: #0b0d12;
--accent-soft: #1d2640;
--crit: #ff6b61;
--crit-soft: #3a1c1b;
--warn: #f0b140;
--warn-soft: #362a14;
--info: #74a3ff;
--info-soft: #1a2640;
--ok: #4cc488;
--ok-soft: #15301f;
--snooze: #a89bff;
--snooze-soft: #262245;
--shadow: 0 1px 2px rgb(0 0 0 / 40%);
--shadow-lg: 0 16px 40px rgb(0 0 0 / 55%);
}
}
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 15px/1.45 var(--font);
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
}
a { color: inherit; text-decoration: none; }
button, input, textarea, select { font: inherit; color: inherit; }
code { font-family: var(--mono); font-size: 0.9em; }
h1, h2, h3 { margin: 0; line-height: 1.25; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ---------- boot + login ---------- */
.boot { display: grid; place-items: center; min-height: 100dvh; }
.spinner {
width: 26px; height: 26px; border-radius: 50%;
border: 3px solid var(--border); border-top-color: var(--accent);
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.login {
min-height: 100dvh;
display: grid; place-items: center;
padding: calc(24px + var(--safe-top)) 16px calc(24px + var(--safe-bottom));
}
.login-card {
width: 100%; max-width: 360px;
display: grid; gap: 14px;
}
.login-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.login-brand h1 { font-size: 24px; letter-spacing: -0.01em; }
.login-hint { color: var(--faint); font-size: 13px; margin: 4px 0 0; }
label { display: grid; gap: 6px; }
label > span { font-size: 13px; font-weight: 600; color: var(--muted); }
input, textarea, select {
width: 100%;
min-height: 44px;
padding: 10px 12px;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
font-size: 16px; /* 16px stops iOS zooming into the field */
}
textarea { min-height: 110px; resize: vertical; line-height: 1.45; }
input:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.form-error {
margin: 0; padding: 10px 12px;
background: var(--crit-soft); color: var(--crit);
border-radius: var(--radius-sm); font-size: 14px;
}
/* ---------- buttons ---------- */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
min-height: 44px; padding: 0 16px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
font-weight: 600; font-size: 15px;
cursor: pointer;
white-space: nowrap;
transition: background 0.12s, border-color 0.12s, opacity 0.12s;
}
.btn:hover { background: var(--surface-hover); }
.btn:disabled { opacity: 0.55; cursor: default; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
.btn-primary:hover { background: var(--accent); filter: brightness(1.06); }
.btn-danger { background: var(--crit); border-color: var(--crit); color: #fff; }
.btn-danger:hover { background: var(--crit); filter: brightness(1.06); }
.btn-ghost { background: transparent; border-color: transparent; }
.btn-ghost:hover { background: var(--surface-2); }
.btn-block { width: 100%; }
.btn-icon { width: 44px; padding: 0; }
.btn-sm { min-height: 32px; padding: 0 8px; font-size: 13px; }
.btn-sm svg { width: 16px; height: 16px; }
.btn svg, .icon { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* ---------- app frame ---------- */
.app { min-height: 100dvh; }
.topbar {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; justify-content: space-between; gap: 12px;
height: calc(var(--topbar-h) + var(--safe-top));
padding: var(--safe-top) 16px 0;
background: color-mix(in srgb, var(--bg) 88%, transparent);
backdrop-filter: saturate(1.4) blur(12px);
-webkit-backdrop-filter: saturate(1.4) blur(12px);
border-bottom: 1px solid var(--border);
}
.topbar-title { font-size: 18px; font-weight: 700; letter-spacing: -0.01em; }
.open-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 10px; border-radius: 999px;
font-size: 13px; font-weight: 600;
background: var(--surface-2); color: var(--muted);
}
.open-pill::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
.open-pill.has-triggered { background: var(--crit-soft); color: var(--crit); }
.open-pill.has-triggered::before { background: var(--crit); }
.open-pill.all-acked::before { background: var(--warn); }
/* Bottom tab bar on phones. */
.nav {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
display: grid; grid-template-columns: repeat(4, 1fr);
height: calc(var(--tabbar-h) + var(--safe-bottom));
padding-bottom: var(--safe-bottom);
background: color-mix(in srgb, var(--surface) 92%, transparent);
backdrop-filter: saturate(1.4) blur(12px);
-webkit-backdrop-filter: saturate(1.4) blur(12px);
border-top: 1px solid var(--border);
}
.nav-brand { display: none; }
.nav-link {
position: relative;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
color: var(--faint); font-size: 11px; font-weight: 600;
}
.nav-link svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.nav-link[aria-current="page"] { color: var(--accent); }
.nav-badge {
position: absolute; top: 6px; left: calc(50% + 6px);
min-width: 18px; height: 18px; padding: 0 5px;
border-radius: 999px; background: var(--crit); color: #fff;
font-size: 11px; font-weight: 700; line-height: 18px; text-align: center;
}
.view { padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom)); }
.view-page { padding-left: 16px; padding-right: 16px; }
.view-page > * { max-width: 760px; margin-left: auto; margin-right: auto; }
/* Phone detail: the incident takes the whole screen with its own action bar,
so the tab bar and top bar step aside. */
.app.detail-open .nav,
.app.detail-open .topbar { display: none; }
.app.detail-open .pane-list { display: none; }
.app.detail-open .view-queue { padding-bottom: 0; }
.view-queue:not(.has-detail) .pane-detail { display: none; }
/* ---------- chips ---------- */
.chips {
display: flex; gap: 6px;
padding: 12px 16px 8px;
overflow-x: auto; scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
flex: none;
min-height: 34px; padding: 0 12px;
border: 1px solid var(--border-strong); border-radius: 999px;
background: var(--surface); color: var(--muted);
font-size: 13px; font-weight: 600; cursor: pointer;
}
.chip[aria-selected="true"] { background: var(--text); border-color: var(--text); color: var(--bg); }
.chip .count { margin-left: 4px; opacity: 0.7; }
/* ---------- lists ---------- */
.list { padding: 0 16px 16px; display: grid; gap: 8px; }
.row {
position: relative;
display: grid; grid-template-columns: 1fr auto; gap: 4px 12px;
padding: 11px 14px 11px 18px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
cursor: pointer;
overflow: hidden;
}
.row:hover { background: var(--surface-hover); }
.row[aria-current="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.row.kbd-focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.row::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
background: var(--sev, var(--border-strong));
}
.row-title {
font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.row-age { color: var(--faint); font-size: 13px; text-align: right; white-space: nowrap; }
.row-meta {
grid-column: 1 / -1;
display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px;
color: var(--muted); font-size: 13px;
min-width: 0;
}
.row-meta .labels { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; color: var(--faint); }
.row.resolved .row-title { color: var(--muted); }
.sev-critical { --sev: var(--crit); }
.sev-warning { --sev: var(--warn); }
.sev-info { --sev: var(--info); }
.empty {
padding: 48px 16px; text-align: center; color: var(--muted);
}
.empty strong { display: block; color: var(--text); font-size: 16px; margin-bottom: 4px; }
.empty .icon { width: 36px; height: 36px; color: var(--ok); margin-bottom: 8px; }
.load-error {
margin: 12px 16px; padding: 10px 12px;
background: var(--crit-soft); color: var(--crit);
border-radius: var(--radius-sm); font-size: 14px;
}
/* ---------- badges ---------- */
.badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 1px 8px; border-radius: 999px;
font-size: 12px; font-weight: 700; letter-spacing: 0.01em;
background: var(--surface-2); color: var(--muted);
white-space: nowrap;
}
.badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.badge.plain::before { display: none; }
.badge.st-triggered, .badge.st-firing { background: var(--crit-soft); color: var(--crit); }
.badge.st-acknowledged { background: var(--warn-soft); color: var(--warn); }
.badge.st-snoozed { background: var(--snooze-soft); color: var(--snooze); }
.badge.st-resolved { background: var(--ok-soft); color: var(--ok); }
.badge.sev-critical { background: var(--crit-soft); color: var(--crit); }
.badge.sev-warning { background: var(--warn-soft); color: var(--warn); }
.badge.sev-info { background: var(--info-soft); color: var(--info); }
/* ---------- incident detail ---------- */
.detail { padding: 0 16px calc(96px + var(--safe-bottom)); }
.detail-head {
position: sticky; top: 0; z-index: 5;
display: flex; align-items: center; gap: 4px;
height: calc(var(--topbar-h) + var(--safe-top));
margin: 0 -16px; padding: var(--safe-top) 8px 0;
background: color-mix(in srgb, var(--bg) 88%, transparent);
backdrop-filter: saturate(1.4) blur(12px);
-webkit-backdrop-filter: saturate(1.4) blur(12px);
border-bottom: 1px solid var(--border);
}
.detail-head .crumb { font-weight: 600; color: var(--muted); font-size: 14px; }
.detail-title { font-size: 21px; font-weight: 750; letter-spacing: -0.01em; margin: 16px 0 8px; overflow-wrap: anywhere; }
.detail-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.card + .card, .section + .section { margin-top: 14px; }
.card-pad { padding: 14px; }
.facts { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0; padding: 14px; font-size: 14px; }
.facts dt { color: var(--muted); }
.facts dd { margin: 0; overflow-wrap: anywhere; }
.facts .sub { color: var(--faint); }
.section { margin-top: 22px; }
.section-title {
display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--muted); margin: 0 2px 8px;
}
.labels-wrap { display: flex; flex-wrap: wrap; gap: 6px; }
.label {
display: inline-flex; max-width: 100%;
font-family: var(--mono); font-size: 12px;
border: 1px solid var(--border); border-radius: var(--radius-sm);
overflow: hidden;
}
.label > span { padding: 2px 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.label > span:first-child { background: var(--surface-2); color: var(--muted); }
.alert-item { padding: 12px 14px; display: grid; gap: 6px; }
.alert-item + .alert-item { border-top: 1px solid var(--border); }
.alert-item-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.alert-item-name { font-weight: 650; overflow-wrap: anywhere; }
.alert-item-summary { color: var(--muted); font-size: 14px; overflow-wrap: anywhere; }
.alert-item-foot { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 13px; color: var(--faint); }
.alert-item-foot a { color: var(--accent); font-weight: 600; }
details > summary { cursor: pointer; color: var(--muted); font-size: 13px; font-weight: 600; list-style: none; }
details > summary::-webkit-details-marker { display: none; }
details > summary::before { content: "▸ "; }
details[open] > summary::before { content: "▾ "; }
details[open] > summary { margin-bottom: 8px; }
.timeline { list-style: none; margin: 0; padding: 4px 0; }
.tl-item {
position: relative;
display: grid; grid-template-columns: 20px 1fr; gap: 10px;
padding: 8px 14px;
}
.tl-item::before {
content: ""; position: absolute; left: 23px; top: 0; bottom: 0; width: 2px; background: var(--border);
}
.tl-item:first-child::before { top: 16px; }
.tl-item:last-child::before { bottom: calc(100% - 16px); }
.tl-dot {
position: relative; z-index: 1;
width: 10px; height: 10px; margin: 5px 0 0 5px; border-radius: 50%;
background: var(--surface); border: 2px solid var(--faint);
}
.tl-triggered .tl-dot, .tl-notify_failed .tl-dot, .tl-deadman_silent .tl-dot { border-color: var(--crit); background: var(--crit); }
.tl-acknowledged .tl-dot { border-color: var(--warn); background: var(--warn); }
.tl-resolved .tl-dot { border-color: var(--ok); background: var(--ok); }
.tl-snoozed .tl-dot { border-color: var(--snooze); }
.tl-note .tl-dot { border-color: var(--accent); background: var(--accent); }
.tl-body { min-width: 0; font-size: 14px; }
.tl-text { overflow-wrap: anywhere; }
.tl-text .who { font-weight: 650; }
.tl-time { color: var(--faint); font-size: 12px; }
.tl-note .note {
margin-top: 6px; padding: 10px 12px;
background: var(--surface-2); border-radius: var(--radius-sm);
white-space: pre-wrap; overflow-wrap: anywhere;
}
.note-actions { display: flex; justify-content: flex-end; }
.note-actions .btn { color: var(--muted); }
/* The action bar sits at the bottom of the screen on a phone, and at the
bottom of the detail pane on desktop. */
.actionbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 15;
display: flex; gap: 8px;
padding: 10px 16px calc(10px + var(--safe-bottom));
background: var(--surface);
border-top: 1px solid var(--border);
}
.actionbar .btn { min-height: 48px; }
.actionbar .btn-primary { flex: 1; font-size: 16px; }
.detail-placeholder {
display: grid; place-items: center; height: 100%;
color: var(--faint); text-align: center; padding: 32px;
}
/* ---------- sheet (dialog) ---------- */
.sheet {
width: 100%; max-width: 100%;
max-height: 88dvh;
margin: auto 0 0; padding: 0;
border: 0; border-radius: 16px 16px 0 0;
background: var(--surface); color: var(--text);
box-shadow: var(--shadow-lg);
overflow: auto;
}
.sheet::backdrop { background: rgb(0 0 0 / 40%); }
.sheet[open] { animation: sheet-up 0.18s ease-out; }
@keyframes sheet-up { from { transform: translateY(24px); opacity: 0.6; } }
.sheet-inner { padding: 8px 16px calc(16px + var(--safe-bottom)); }
.sheet-grab { width: 40px; height: 4px; margin: 0 auto 12px; border-radius: 2px; background: var(--border-strong); }
.sheet-title { font-size: 17px; font-weight: 700; margin: 0 0 4px; }
.sheet-text { color: var(--muted); margin: 0 0 14px; font-size: 14px; }
.sheet-form { display: grid; gap: 12px; }
.sheet-actions { display: flex; gap: 8px; margin-top: 16px; }
.sheet-actions .btn { flex: 1; }
.menu { list-style: none; margin: 0 -4px; padding: 0; }
.menu-item {
display: flex; align-items: center; gap: 12px;
width: 100%; min-height: 50px; padding: 0 12px;
background: none; border: 0; border-radius: var(--radius-sm);
text-align: left; font-size: 16px; cursor: pointer;
}
.menu-item:hover { background: var(--surface-2); }
.menu-item .icon { color: var(--muted); flex: none; }
.menu-item .menu-sub { margin-left: auto; color: var(--faint); font-size: 13px; }
.menu-item.danger, .menu-item.danger .icon { color: var(--crit); }
.menu-item[aria-checked="true"] { font-weight: 700; }
.menu-item[aria-checked="true"]::after { content: "✓"; margin-left: 8px; color: var(--accent); }
.menu-sep { height: 1px; background: var(--border); margin: 6px 12px; }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; z-index: 50;
bottom: calc(var(--tabbar-h) + var(--safe-bottom) + 12px);
transform: translateX(-50%);
max-width: calc(100% - 32px);
padding: 10px 16px; border-radius: var(--radius);
background: var(--text); color: var(--bg);
font-size: 14px; font-weight: 600;
box-shadow: var(--shadow-lg);
}
.toast.error { background: var(--crit); color: #fff; }
.app.detail-open ~ .toast { bottom: calc(80px + var(--safe-bottom)); }
/* ---------- on-call ---------- */
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 16px auto 12px; }
.page-head h2 { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.now-card { display: flex; align-items: center; gap: 14px; padding: 16px; margin-top: 16px; }
.avatar {
flex: none; display: grid; place-items: center;
width: 44px; height: 44px; border-radius: 50%;
background: var(--accent-soft); color: var(--accent);
font-weight: 750; font-size: 17px; text-transform: uppercase;
}
.avatar.none { background: var(--surface-2); color: var(--faint); }
.now-label { color: var(--muted); font-size: 13px; font-weight: 600; }
.now-name { font-size: 20px; font-weight: 750; }
.you { color: var(--accent); font-weight: 650; font-size: 13px; margin-left: 6px; }
.week-nav { display: flex; align-items: center; gap: 4px; }
.week-nav .label { font-size: 14px; font-weight: 650; min-width: 9em; text-align: center; }
.days { list-style: none; margin: 0; padding: 0; }
.day { display: grid; grid-template-columns: 3.2em 4.2em 1fr; align-items: center; gap: 8px; min-height: 50px; padding: 0 14px; }
.day + .day { border-top: 1px solid var(--border); }
.day-name { font-weight: 650; }
.day-date { color: var(--faint); font-size: 13px; }
.day-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.day-who.nobody { color: var(--faint); font-style: italic; }
.day.today { background: var(--accent-soft); }
.day.today:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.day.today:last-child { border-radius: 0 0 var(--radius) var(--radius); }
.day.today .day-name { color: var(--accent); }
.day.past { opacity: 0.6; }
.shift-list { list-style: none; margin: 0; padding: 0; }
.shift-list li { display: flex; justify-content: space-between; padding: 12px 14px; }
.shift-list li + li { border-top: 1px solid var(--border); }
.shift-list .muted { color: var(--faint); }
/* ---------- alerts page ---------- */
.view-alerts .chips { padding-left: 0; padding-right: 0; }
.view-alerts .list { padding-left: 0; padding-right: 0; }
.row.st-firing { --sev: var(--crit); }
.row.st-resolved { --sev: var(--ok); }
.row.no-link { cursor: default; }
/* ---------- account ---------- */
.account-card { display: flex; align-items: center; gap: 14px; padding: 16px; margin-top: 16px; }
.account-name { font-size: 18px; font-weight: 750; }
.account-email { color: var(--muted); font-size: 14px; overflow-wrap: anywhere; }
.pw-form { display: grid; gap: 12px; padding: 16px; }
.form-ok {
margin: 0; padding: 10px 12px;
background: var(--ok-soft); color: var(--ok);
border-radius: var(--radius-sm); font-size: 14px;
}
.kbd-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.kbd-table td { padding: 8px 14px; border-top: 1px solid var(--border); }
.kbd-table tr:first-child td { border-top: 0; }
kbd {
display: inline-block; min-width: 1.6em; padding: 1px 6px;
font-family: var(--mono); font-size: 12px; text-align: center;
background: var(--surface-2); border: 1px solid var(--border-strong); border-bottom-width: 2px;
border-radius: 4px;
}
.only-desktop { display: none; }
.foot-note { color: var(--faint); font-size: 13px; text-align: center; margin: 24px auto; }
/* ---------- desktop ---------- */
@media (min-width: 900px) {
:root { --tabbar-h: 0px; }
.app { display: grid; grid-template-columns: 220px 1fr; height: 100dvh; }
.nav {
position: static; grid-row: 1 / span 2;
display: flex; flex-direction: column; gap: 2px;
height: auto; padding: 16px 12px;
background: var(--surface);
border-top: 0; border-right: 1px solid var(--border);
backdrop-filter: none;
}
.nav-brand {
display: flex; align-items: center; gap: 10px;
padding: 4px 10px 18px; font-size: 18px; font-weight: 750; letter-spacing: -0.01em;
}
.nav-link {
flex-direction: row; justify-content: flex-start; gap: 12px;
min-height: 40px; padding: 0 10px; border-radius: var(--radius-sm);
color: var(--muted); font-size: 14px;
}
.nav-link:hover { background: var(--surface-2); }
.nav-link[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }
.nav-link svg { width: 20px; height: 20px; }
.nav-badge { position: static; margin-left: auto; }
.topbar { display: none; }
.view { padding-bottom: 0; overflow: auto; height: 100dvh; }
.view-page { padding: 8px 32px 32px; }
.view-queue {
display: grid; grid-template-columns: minmax(340px, 420px) 1fr;
overflow: hidden;
}
.view-queue .pane { overflow: auto; height: 100dvh; }
.pane-list { border-right: 1px solid var(--border); }
.pane-list .chips { position: sticky; top: 0; z-index: 2; background: var(--bg); padding-top: 16px; }
.view-queue:not(.has-detail) .pane-detail { display: block; }
/* On desktop the list stays visible next to the detail. */
.app.detail-open .nav { display: flex; }
.app.detail-open .pane-list { display: block; }
.detail-head .back { display: none; }
.detail-head { padding-left: 16px; }
.pane-detail { position: relative; display: flex; flex-direction: column; }
.detail { flex: 1; padding: 0 32px 24px; max-width: 900px; width: 100%; }
.detail-head { margin: 0 -32px; padding-left: 32px; }
.actionbar {
position: sticky; bottom: 0;
padding: 12px 32px;
}
.actionbar .btn-primary { flex: 0 1 240px; }
.sheet {
width: min(440px, calc(100% - 32px));
margin: auto; border-radius: 14px;
}
.sheet-grab { display: none; }
.sheet-inner { padding: 20px; }
.toast, .app.detail-open ~ .toast { bottom: 24px; }
.only-desktop { display: block; }
}