b0a02c010b
Closes #5. Three of the server's tunables were environment variables, which meant changing how long an incident waits before being paged again required editing a chart, merging it and waiting for a reconcile. They are behaviour rather than infrastructure, and the difference is who needs to change them and how often. The split is by who owns the value. What stays in the environment is where the server is plugged in: the listen address, the DSN, the ntfy URL and token, the public URL. Those are needed before the database is open and two of them are credentials -- the settings endpoint reports that ntfy is configured and that a token is set, and never what either is. What moves is how it behaves: the notify repeat interval, the stale window and the archive window. The environment variable becomes the seed rather than the setting, written once on first start and never overwritten, so a redeploy cannot put a chart's default back over an administrator's edit -- the rule the per-team dead man's switches already follow. The loops read the current value per tick, so a change at 02:00 is obeyed at 02:00. Key/value rather than a column per knob: #6 and #7 will both add settings, and a table shaped one-column-per-setting needs a migration for each. The cost is that values are text and the accessor has to say what type it wanted, which settings.go does in one place. Unknown keys are refused rather than stored -- a typo that wrote notify_repeat_second would otherwise sit in the table looking like configuration and doing nothing -- and each value has bounds loose enough to catch a slipped decimal point without having an opinion about anybody's rota. Disabling an account is new, and is not deleting one. Deleting a user nulls acknowledged_by and assigned_to, which quietly rewrites who did what during an incident months after the fact. A disabled user cannot authenticate by either credential, loses their sessions immediately, and stays the name on every acknowledgement they made. The check is part of the lookup in serveAs rather than a test afterwards, so there is no path where the row is loaded and the flag is then forgotten. The page itself is a fourth tab, shown only to an administrator and only as a courtesy: every endpoint under it is refused with 403 regardless, so somebody who types /admin gets an explanation rather than a blank screen. It lists teams with their size and open-incident count, users with their flags, and the settings with their bounds -- plus the environment half, read-only, so somebody hunting for the ntfy URL learns where it lives instead of concluding the server has none. Delete is disabled rather than offered-and-refused for a team with open incidents, and neither admin action is offered on your own account, since the server refuses both. Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
660 lines
25 KiB
CSS
660 lines
25 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; }
|