Files
terdut-server/internal/web/static/app.css
T
Niklas Ye 8b2789b9b2 Let the filter chips wrap in the desktop incident list
The list pane is 340-420px wide and its chip row scrolled sideways with the
scrollbar hidden. That works by swipe on a phone, but a mouse has nothing to
grab, so Archived (the last chip) could not be reached on a wide screen. In
the desktop layout the row now wraps instead, and the divider between the
status and team chips is hidden there, since it would sit mid-line.

Phones keep the sideways scroll: the rule is inside the min-width: 900px
block.

Claude-Session: https://claude.ai/code/session_01MMados3BD1oSjevHxbmVqU
2026-09-25 17:25:17 +02:00

916 lines
38 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;
/* Two hues that mean nothing on their own. The rota needs six colours to
tell six people apart and the palette above only has four that are not
already an alarm. */
--teal: #0f7d8c;
--teal-soft: #e3f4f6;
--pink: #b3427e;
--pink-soft: #fbe8f2;
--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;
/* No bottom tab bar on any breakpoint any more — mobile uses the hamburger
menu in the topbar, desktop the sidebar — so this stays 0. Kept as a
variable rather than deleted since .view, .toast and .nav's own height
calc still read it. */
--tabbar-h: 0px;
--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;
--teal: #4fc2d4;
--teal-soft: #0f2e33;
--pink: #f07fb8;
--pink-soft: #3a1c2d;
--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-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
.topbar-title {
font-size: 18px; font-weight: 700; letter-spacing: -0.01em;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
#menu-btn { position: relative; }
.nav-badge.menu-btn-badge { top: 2px; left: auto; right: 2px; }
.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); }
/* Hidden on phones — mobile navigates through the hamburger menu in the
topbar instead (see #menu-btn / openNavMenu in app.js). Reappears as the
left sidebar from 900px, where the desktop block below redeclares display. */
.nav {
display: none;
position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
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;
/* min-width lets a column shrink below its label's natural width, which is
what stops six tabs widening the bar past the screen. */
min-width: 0; padding: 0 2px;
}
.nav-label {
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-link svg { width: 24px; height: 24px; flex: none; 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; }
.note {
margin-top: 6px; padding: 10px 12px;
background: var(--surface-2); border-radius: var(--radius-sm);
white-space: pre-wrap; overflow-wrap: anywhere;
}
.note-fix { background: var(--ok-soft); border-left: 3px solid var(--ok); }
.similar { list-style: none; margin: 0; padding: 0; }
.similar-item { padding: 10px 0; }
.similar-item + .similar-item { border-top: 1px solid var(--border, var(--surface-2)); }
.check { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--muted); }
.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) {
.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; }
/* The pane is 340-420px wide and a mouse cannot scroll a row whose scrollbar
is hidden, so the chips wrap here instead: Archived stays reachable. */
.pane-list .chips { flex-wrap: wrap; overflow-x: visible; }
.pane-list .chip-sep { display: none; }
.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; }
/* A name in an admin table is the way to that row's own page -- a person's or
a team's. */
.row-link { color: var(--text); font-weight: 650; text-decoration: none; }
.row-link:hover { color: var(--accent); text-decoration: underline; }
.invite-block { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 12px; }
.invite-block h3 { margin: 0 0 4px; font-size: 14px; }
/* The link is shown once and never stored, so it has to be selectable and
wrap rather than scroll off the side of a phone. */
.invite-out { margin-top: 12px; font-size: 13px; }
.invite-link {
display: block; margin-top: 6px; padding: 8px; border-radius: var(--radius-sm);
background: var(--surface-2); font-family: var(--mono); font-size: 12px;
word-break: break-all; user-select: all;
}
/* --- one user, one team --------------------------------------------------
Both subject pages share this: .user-head and .user-facts are generic
despite the names, and a team fills them with its own facts. */
.back-link {
display: inline-flex; align-items: center; gap: 2px; margin-bottom: 12px;
color: var(--muted); font-size: 14px; text-decoration: none;
}
.back-link:hover { color: var(--text); }
.back-link svg { width: 18px; height: 18px; }
.user-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.user-head h2 { margin: 0; }
.user-facts {
display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px;
margin: 12px 0 0; font-size: 14px;
}
.user-facts dt { color: var(--muted); }
.user-facts dd { margin: 0; overflow-wrap: anywhere; }
.row-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.admin-table .row-actions { margin-top: 0; gap: 6px; }
/* --- 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%; }
/* The rota, a month at a time. A name is too wide to print thirty times and
too alike down a column to read, so a day carries an initial in that
person's colour and the legend underneath says whose. A shift is then a run
of one colour, which is the shape the question actually has. */
.rota-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; padding: 10px; }
.rota-wd {
padding-bottom: 4px; text-align: center;
color: var(--muted); font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.04em;
}
.rota-day {
display: flex; flex-direction: column; align-items: center; gap: 4px;
min-height: 52px; padding: 6px 0 8px;
border: 0; border-radius: var(--radius-sm); background: none;
font: inherit; color: inherit;
}
button.rota-day { cursor: pointer; }
button.rota-day:hover { background: var(--surface-2); }
.rota-num { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.rota-day.today { background: var(--accent-soft); }
.rota-day.today .rota-num { color: var(--accent); font-weight: 700; }
.rota-day.past { opacity: 0.55; }
/* The days either side of the month are real days and are drawn, but they
belong to the month you are not looking at. */
.rota-day.outside { opacity: 0.35; }
.rota-chip {
display: grid; place-items: center;
width: 26px; height: 26px; border-radius: 50%;
font-size: 12px; font-weight: 750; text-transform: uppercase;
}
/* An empty day is a dot rather than a hole, and keeps the chip's box so the
rows stay on one baseline. */
.rota-chip.none { width: 8px; height: 8px; margin: 9px; background: var(--border-strong); }
/* Six colours, then they repeat; the initial inside still tells two people
apart. Deliberately not the severity palette — nothing here is critical. */
.rc1 { background: var(--accent-soft); color: var(--accent); }
.rc2 { background: var(--ok-soft); color: var(--ok); }
.rc3 { background: var(--snooze-soft); color: var(--snooze); }
.rc4 { background: var(--warn-soft); color: var(--warn); }
.rc5 { background: var(--teal-soft); color: var(--teal); }
.rc6 { background: var(--pink-soft); color: var(--pink); }
.rota-foot { padding: 12px 14px; border-top: 1px solid var(--border); }
.rota-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; font-size: 14px; }
.rota-key { display: inline-flex; align-items: center; gap: 6px; }
.rota-key .rota-chip { width: 22px; height: 22px; font-size: 11px; }
.rota-note { margin: 10px 0 0; color: var(--muted); font-size: 13px; }
.rota-note:first-child { margin-top: 0; }
.rota-bulk { padding: 12px 14px; border-top: 1px solid var(--border); }
.rota-bulk .stacked-form { margin-top: 4px; }
.sheet-pick { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.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; }
/* --- onboarding checklist ------------------------------------------------
Sits above the queue until it is finished or hidden. Deliberately plain:
it is a list of things to do, not a celebration. */
.onboarding { border-left: 3px solid var(--accent); }
.onboarding-head { display: flex; align-items: center; gap: 10px; }
.onboarding-head h2 { flex: 1; margin: 0; }
.checklist { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.checklist .step { display: flex; gap: 10px; align-items: flex-start; }
.checklist .step p { margin: 2px 0 0; }
.step-mark {
flex: none; width: 20px; height: 20px; border-radius: 50%;
border: 1px solid var(--border-strong); color: var(--accent);
display: flex; align-items: center; justify-content: center; font-size: 13px;
}
.step.done .step-mark { border-color: var(--accent); }
.step.done > div > strong { color: var(--muted); text-decoration: line-through; }
.step-actions { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.signup-intro { margin: 0 0 4px; font-size: 14px; color: var(--muted); }
/* --- sub-navigation -------------------------------------------------------
A strip of links across the top of every Admin and every Team page, one per
sub-section. Deliberately not .chip: chips filter what a page already shows,
here and in the queue, and these go somewhere. Same aria-current convention
as the tab bar, so the state lives on the attribute rather than in a class.
Six entries do not fit a phone's width, which is what the horizontal scroll
below is for -- the Team tab's strip is the one that needs it. */
.subnav {
display: flex; gap: 2px;
margin: 12px auto 0;
border-bottom: 1px solid var(--border);
overflow-x: auto; scrollbar-width: none;
}
.subnav::-webkit-scrollbar { display: none; }
.subnav-link {
flex: none;
padding: 8px 12px; margin-bottom: -1px;
border-bottom: 2px solid transparent;
color: var(--muted); font-size: 14px; font-weight: 600; white-space: nowrap;
}
.subnav-link:hover { color: var(--text); }
.subnav-link[aria-current="page"] { color: var(--accent); border-bottom-color: var(--accent); }
/* The overview a tab opens on, at /admin and at /team. The strip above already
links to the sections, so these carry the counts, which is the part a menu
cannot say. Not named for either tab: both use it, and the one that renamed
.user-link to .row-link is the same rename for the same reason. */
.overview-menu { display: grid; gap: 10px; margin-top: 16px; }
/* The grid's gap is the spacing here, so .card + .card must not add its own. */
.overview-menu .card + .card { margin-top: 0; }
.overview-item { display: block; padding: 14px; }
.overview-item:hover { background: var(--surface-hover); }
.overview-head { display: flex; align-items: baseline; gap: 8px; }
.overview-count { margin-left: auto; color: var(--muted); font-size: 18px; font-weight: 700; }
.overview-item p { margin: 4px 0 0; }
/* ---------- stats page ---------- */
#view-stats .chips { padding-left: 0; padding-right: 0; }
.stats { display: grid; gap: 14px; padding-bottom: 16px; }
.stat-tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.stat-tile {
--sev: var(--border-strong);
background: var(--surface); border: 1px solid var(--border);
border-left: 3px solid var(--sev); border-radius: var(--radius);
box-shadow: var(--shadow); padding: 12px;
}
.stat-tile.st-triggered { --sev: var(--crit); }
.stat-tile.st-acknowledged { --sev: var(--warn); }
.stat-tile.st-resolved { --sev: var(--ok); }
.stat-value { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-label { margin-top: 2px; font-size: 12px; color: var(--muted); }
.chart-card + .chart-card { margin-top: 0; }
.chart-title {
margin-bottom: 10px; font-size: 13px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted);
}
.hbars { list-style: none; display: grid; gap: 6px; }
.hbar { display: grid; grid-template-columns: minmax(80px, 34%) 1fr auto; align-items: center; gap: 8px; font-size: 13px; }
.hbar-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hbar-track { height: 10px; border-radius: 5px; background: var(--surface-2); overflow: hidden; }
.hbar-fill { display: block; height: 100%; border-radius: 5px; background: var(--ok); }
.hbar-count { min-width: 2ch; text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
.columns { display: block; width: 100%; height: auto; }
.columns .axis { stroke: var(--border-strong); stroke-width: 1; }
.columns .col-hit { fill: transparent; }
.columns .col-bar { fill: var(--accent); }
.columns .col:hover .col-bar { opacity: 0.75; }
.columns .col-label { fill: var(--faint); font-size: 10px; font-family: var(--font); }
@media (min-width: 900px) {
.stat-tiles { grid-template-columns: repeat(3, 1fr); }
}