Files
terdut-server/internal/web/static/app.css
T
Niklas Ye d728af53b1
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 13s
CI / test (pull_request) Successful in 2m19s
Put a team's own settings in the web UI
Closes #17. Everything a team owner configures was API-only: escalation,
integrations, dead man's switches, membership, and the rota -- which the
on-call view still described as the TUI's job, and the TUI has been broken
against this server since teams landed. Setting up the feature this whole
line of work exists for meant using curl.

A Team tab now holds all of it, one team at a time, with a picker for
somebody in more than one. An owner edits; a member sees the same page
without the controls, because the server refuses their writes anyway --
hiding a button is a courtesy to the reader, not the thing enforcing
anything.

The escalation editor holds a draft and sends the whole ladder, because
the API replaces it wholesale: the levels are an order, and patching one
rung leaves the numbering of the others undecided. Adding a level
defaults to five minutes and the rota, which is the shape almost every
ladder starts as.

An integration key is returned exactly once, so creating one opens a
panel that says so, shows the URL large with a copy button, and renders
the Alertmanager receiver snippet with the URL already in it -- the next
thing anybody does with that key is paste it into a config. The panel
stays until it is dismissed rather than disappearing on the next
re-render.

The incident view gains where an incident is on the ladder and when the
next page is due, which is the question somebody looking at an
unacknowledged incident actually has. The API carries it: the incident
payload now includes escalation_level and escalation_due_at, the latter
computed in the incident SELECT by joining the level's timeout, so a list
costs no extra queries.

Verified against a live server by making every call the page makes,
including the writes: the six reads the Team tab issues, a two-level
ladder saved and read back, an integration created and its key returned
once, three days of rota assigned, switches set, and an incident showing
level 1 with a due time five minutes out.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
2026-09-20 21:22:24 +02:00

690 lines
27 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); }
/* Which team's queue a row came from. Only rendered for somebody in more than
one team, so it never repeats the same word down the whole list. */
/* Separates the status chips from the team chips in the queue's filter row. */
.chip-sep { width: 1px; align-self: stretch; background: var(--border); margin: 0 2px; }
.row-team {
padding: 1px 6px; border-radius: 4px;
background: var(--surface-2); border: 1px solid var(--border);
color: var(--muted); font-size: 12px; white-space: nowrap;
}
.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; }
}
/* --- admin ---------------------------------------------------------------
The admin page is three tables of things you act on, so it needs table
styling the rest of the app never did: the queue is a list of links and the
account page is a form. */
.admin-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.admin-table th {
text-align: left; font-weight: 600; color: var(--muted); font-size: 12px;
text-transform: uppercase; letter-spacing: 0.04em;
padding: 4px 8px 4px 0; border-bottom: 1px solid var(--border);
}
.admin-table td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
.admin-table tr:last-child td { border-bottom: none; }
.admin-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.admin-table td .btn-sm + .btn-sm { margin-left: 6px; }
/* A disabled account stays readable — it is still the name on old
acknowledgements — but should not look like a working one. */
.disabled-row td { opacity: 0.55; }
.btn-sm.danger { color: var(--crit); border-color: var(--crit-soft); }
.inline-form { display: flex; gap: 8px; margin-top: 12px; }
.inline-form input { flex: 1; min-width: 0; }
.admin-settings .setting-value { width: 5.5em; margin-right: 6px; }
.admin-settings .setting-unit { max-width: 8em; }
.admin-settings button[type="submit"] { margin-top: 12px; }
.small { font-size: 13px; }
/* --- team settings -------------------------------------------------------
Forms with a label above each control, rather than the queue's rows of
links. The escalation ladder is the only nested structure in the app, so it
gets a little indentation to make the levels read as an order. */
.stacked-form { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; align-items: flex-start; }
.stacked-form label { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 14px; }
.stacked-form label.checkbox { gap: 8px; }
.stacked-form input.wide { min-width: min(420px, 100%); }
.team-picker { margin-top: 8px; max-width: 100%; }
.ladder-level {
border-left: 3px solid var(--border-strong);
padding: 8px 0 8px 12px; margin: 12px 0;
}
.ladder-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.ladder-targets { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.target-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* An integration key is shown exactly once, so it should look like something
to act on rather than another row of text. */
.key-panel {
margin-top: 12px; padding: 12px;
border: 1px solid var(--accent); border-radius: 8px; background: var(--accent-soft);
}
.key-panel pre {
overflow-x: auto; background: var(--surface); border: 1px solid var(--border);
border-radius: 6px; padding: 8px; font-size: 12px;
}
.key-url code { word-break: break-all; }