7b9d309d13
The rota put an initial on every day, so one person covering a week was seven identical chips and a legend to decode them. A shift is now one bar across its days with the person's name on it; a flat end with a chevron means it carries on across the row break. Days with nobody on call are a hatched amber bar while they can still be fixed and a quiet grey one once they are history, and the footer is a green banner when the month is covered or an amber one when it is not. Tapping a day still opens it: the bars ignore the pointer, so the tap reaches the cell underneath. The month title was monospace because its button borrowed .label, which belongs to label chips; it has its own class now, and a Today button sits beside the arrows. The escalation card was a five-column table. It is now the ladder it describes: a numbered step per level with its status and who it pages, the wait before the next level on its own line, and what happens after the last one at the bottom. With no fallback topic that last step is an amber callout, with an Add fallback button for owners that opens the editor on the field. That gap is what incident #36 hit. Web UI only: no endpoint or JSON shape changed, so nothing to mirror in terdut-tui.
1273 lines
58 KiB
CSS
1273 lines
58 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: #d9dde4;
|
||
--border-strong: #c5cad3;
|
||
--text: #16181d;
|
||
--muted: #5b626e;
|
||
--faint: #676d79;
|
||
|
||
--accent: #2f5bd3;
|
||
--accent-text: #ffffff;
|
||
--accent-soft: #e8eefc;
|
||
|
||
--crit: #c22d26;
|
||
--crit-soft: #fdecea;
|
||
--warn: #8f5500;
|
||
--warn-soft: #fdf3e1;
|
||
--info: #245fc7;
|
||
--info-soft: #e9f0fd;
|
||
--ok: #1a7445;
|
||
--ok-soft: #e6f5ec;
|
||
--snooze: #6050c8;
|
||
--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: #0c7180;
|
||
--teal-soft: #e3f4f6;
|
||
--pink: #a63b73;
|
||
--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;
|
||
|
||
/* A ~4-size type scale, per issue #26's "use one type scale" ask. This
|
||
file still has a dozen one-off font-size values below; migrating the
|
||
low-risk, purely cosmetic ones (standalone titles with no dimensional
|
||
or functional constraint) onto these tokens is a start, not the whole
|
||
job — the rest (sizes tied to a fixed shape like the avatar circle, to
|
||
a deliberately prominent display like a stat tile or the on-call name,
|
||
or to a non-negotiable constraint like the 16px that stops iOS zooming
|
||
into an input) stay as either their own pixel value or a documented
|
||
exception, since guessing at those without seeing them render risks
|
||
trading one inconsistency for a worse one. */
|
||
--fs-xs: 12px;
|
||
--fs-sm: 13px;
|
||
--fs-base: 14px;
|
||
--fs-lg: 18px;
|
||
--fs-xl: 21px;
|
||
|
||
--topbar-h: 52px;
|
||
/* The phone-width bottom tab bar's height. Unused above 900px: the
|
||
desktop block overrides .nav/.view/.toast directly rather than reading
|
||
this back down to 0. */
|
||
--tabbar-h: 58px;
|
||
--safe-top: env(safe-area-inset-top, 0px);
|
||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
|
||
/* Dark palette. Applied by the OS preference unless the user chose Light in
|
||
Account (data-theme="light" on <html>, set by js/theme.js), and always when
|
||
they chose Dark. The two blocks must stay identical: CSS has no way to share
|
||
a declaration list between a media query and an attribute selector. */
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--bg: #0f1115;
|
||
--surface: #1a1e26;
|
||
--surface-2: #232834;
|
||
--surface-hover: #20252e;
|
||
--border: #343b49;
|
||
--border-strong: #444d5f;
|
||
--text: #e7e9ed;
|
||
--muted: #a0a7b3;
|
||
--faint: #8a92a0;
|
||
|
||
--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%);
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--bg: #0f1115;
|
||
--surface: #1a1e26;
|
||
--surface-2: #232834;
|
||
--surface-hover: #20252e;
|
||
--border: #343b49;
|
||
--border-strong: #444d5f;
|
||
--text: #e7e9ed;
|
||
--muted: #a0a7b3;
|
||
--faint: #8a92a0;
|
||
|
||
--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%);
|
||
}
|
||
:root[data-theme="light"] { color-scheme: light; }
|
||
:root[data-theme="dark"] { color-scheme: dark; }
|
||
|
||
*, *::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; }
|
||
.login-password { display: grid; gap: 14px; }
|
||
/* "or" between the single sign-on button and the password form. */
|
||
.login-divider {
|
||
display: flex; align-items: center; gap: 10px; margin: 0;
|
||
color: var(--faint); font-size: 13px;
|
||
}
|
||
.login-divider::before, .login-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
||
|
||
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, .btn-sm: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;
|
||
}
|
||
.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); }
|
||
/* "All clear" is a status, not an action: green with a check, no dot. */
|
||
.open-pill.all-clear { background: var(--ok-soft); color: var(--ok); }
|
||
.open-pill.all-clear::before { content: none; }
|
||
.open-pill .icon { width: 14px; height: 14px; }
|
||
|
||
/* Bottom tab bar on phones (Queue, On-call, Alerts, Team, More); becomes the
|
||
left sidebar from 900px, where the desktop block below redeclares display
|
||
and shows every section flat, .nav-link-secondary included. */
|
||
.nav {
|
||
display: flex;
|
||
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; }
|
||
/* Hidden here (shown from 900px below): on the phone bar the team switcher
|
||
lives in the topbar instead, as #team-selector-mobile. */
|
||
.nav-team-selector { display: none; }
|
||
.nav-link {
|
||
position: relative;
|
||
/* flex: 1 spreads the tabs evenly across the bar's width; the desktop
|
||
block below cancels it back to a natural-width row item. */
|
||
flex: 1 1 0;
|
||
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;
|
||
}
|
||
/* Must come after .nav-link above: same specificity (one class each), so
|
||
whichever is later in the file wins for an element wearing both classes,
|
||
and this needs to beat .nav-link's display:flex here on the phone bar. */
|
||
.nav-link-secondary { display: none; }
|
||
.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;
|
||
}
|
||
|
||
/* ---------- team selector ---------- */
|
||
/* The global control for which team the app is scoped to. Hidden (via the
|
||
`hidden` attribute, set from teamselector.js) for anybody in fewer than two
|
||
teams, the same rule every other team-aware control in this file follows. */
|
||
|
||
.nav-team-selector,
|
||
.team-selector-mobile {
|
||
align-items: center; gap: 8px;
|
||
border: 1px solid var(--border-strong); border-radius: 999px;
|
||
background: var(--surface); color: var(--text);
|
||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||
padding: 4px 12px; max-width: 100%;
|
||
}
|
||
.team-selector-mobile { display: inline-flex; padding: 4px 10px; font-size: 12px; max-width: 120px; }
|
||
.team-selector-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.team-selector-chevron { width: 14px; height: 14px; flex: none; color: var(--faint); margin-left: -2px; }
|
||
|
||
/* A team's identity colour — not a status, so never the severity palette. Six
|
||
colours, then they repeat; teamColorClass() in format.js picks one by the
|
||
team's id, the same rcN convention the rota's per-person chips use. */
|
||
.team-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--border-strong); }
|
||
.team-dot.rc1 { background: var(--accent); }
|
||
.team-dot.rc2 { background: var(--ok); }
|
||
.team-dot.rc3 { background: var(--snooze); }
|
||
.team-dot.rc4 { background: var(--warn); }
|
||
.team-dot.rc5 { background: var(--teal); }
|
||
.team-dot.rc6 { background: var(--pink); }
|
||
|
||
.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 {
|
||
position: relative;
|
||
display: flex; gap: 6px;
|
||
padding: 12px 16px 8px;
|
||
overflow-x: auto; scrollbar-width: none;
|
||
}
|
||
.chips::-webkit-scrollbar { display: none; }
|
||
.chip {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
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"], .chip[aria-checked="true"] { background: var(--text); border-color: var(--text); color: var(--bg); }
|
||
.theme-picker { padding: 0 0 8px; }
|
||
.chip .count { margin-left: 4px; opacity: 0.7; }
|
||
/* Nothing in it: step back so the chips that have something stand out. */
|
||
.chip .count.zero { opacity: 0.4; }
|
||
/* An overlay, not a flex item: absolute against .chips' own (non-scrolling)
|
||
box stays flush with its real right edge regardless of scroll position,
|
||
which turned out not to be true of position:sticky here — as a flex
|
||
item, its sticky offset interacted with the row's gap and its own
|
||
negative margin, landing short of the edge by about one gap's width. */
|
||
.chips-fade {
|
||
position: absolute; top: 0; right: 0; bottom: 0;
|
||
width: 24px;
|
||
background: linear-gradient(to right, transparent, var(--bg));
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ---------- 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; overflow-wrap: anywhere;
|
||
/* Two lines, not one: titles differ at the end ("PodRestarting
|
||
(namespace=...") and an ellipsis cut exactly the part that tells rows apart. */
|
||
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
|
||
}
|
||
.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;
|
||
}
|
||
/* The page a terminal's sign-in prompt links to. */
|
||
.device-card { max-width: 420px; margin: 24px auto; padding: 20px; display: grid; gap: 14px; }
|
||
.device-code {
|
||
margin: 0; padding: 14px; text-align: center;
|
||
font: 700 30px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||
letter-spacing: 0.12em;
|
||
background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||
}
|
||
/* Access the identity provider's groups grant. The tint says "not yours to edit here". */
|
||
.row-team.sso { margin-left: 6px; background: var(--accent-soft); border-color: transparent; color: var(--accent); }
|
||
.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: var(--fs-lg); 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 .badge-icon { width: 12px; height: 12px; stroke-width: 2.4; }
|
||
.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, .badge.st-healthy { background: var(--ok-soft); color: var(--ok); }
|
||
.badge.st-dead { background: var(--crit-soft); color: var(--crit); }
|
||
/* Dormant is the plain badge on purpose: nothing has gone wrong and nothing has
|
||
gone right, which is what the muted default already says. */
|
||
.badge.st-dormant, .badge.st-never { background: var(--surface-2); color: var(--muted); }
|
||
.badge.st-active { background: var(--ok-soft); color: var(--ok); }
|
||
.badge.st-quiet, .badge.st-escalating { background: var(--warn-soft); color: var(--warn); }
|
||
.badge.st-ready, .badge.st-oncall { background: var(--ok-soft); color: var(--ok); }
|
||
.badge.st-reachable { background: var(--surface-2); color: var(--muted); }
|
||
.badge.st-unreachable, .badge.st-unpageable { background: var(--crit-soft); color: var(--crit); }
|
||
.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 .copy { margin-left: auto; }
|
||
.clip-buffer { position: fixed; top: 0; left: 0; opacity: 0; pointer-events: none; }
|
||
.detail-head .crumb { font-weight: 600; color: var(--muted); font-size: 14px; }
|
||
.detail-title { font-size: var(--fs-xl); 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); }
|
||
.facts dd.fact-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||
.fact-chip { display: inline-flex; align-items: center; gap: 4px; }
|
||
.fact-icon { width: 15px; height: 15px; 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; }
|
||
|
||
/* One .tl-phase per status the incident has been through (see
|
||
timelinePhases() in incident.js) — each with its own .timeline <ol>, so
|
||
the existing :first-child/:last-child rail-capping below gives each phase
|
||
its own self-contained connecting line rather than one running through
|
||
the headings. */
|
||
.tl-phase + .tl-phase { border-top: 1px solid var(--border); }
|
||
.tl-phase-title {
|
||
padding: 10px 14px 0;
|
||
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
|
||
color: var(--faint);
|
||
}
|
||
.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: 18px; }
|
||
.tl-item:last-child::before { bottom: calc(100% - 18px); }
|
||
.tl-dot {
|
||
position: relative; z-index: 1;
|
||
display: grid; place-items: center;
|
||
width: 20px; height: 20px; border-radius: 50%;
|
||
background: var(--surface); border: 2px solid var(--border-strong); color: var(--muted);
|
||
}
|
||
/* An event with no icon keeps the old plain dot. */
|
||
.tl-dot:empty { width: 10px; height: 10px; margin: 5px 0 0 5px; }
|
||
.tl-icon { width: 11px; height: 11px; stroke-width: 2.6; }
|
||
.tl-triggered .tl-dot, .tl-notify_failed .tl-dot, .tl-deadman_silent .tl-dot { border-color: var(--crit); color: var(--crit); }
|
||
.tl-acknowledged .tl-dot { border-color: var(--warn); color: var(--warn); }
|
||
.tl-resolved .tl-dot { border-color: var(--ok); color: var(--ok); }
|
||
.tl-snoozed .tl-dot { border-color: var(--snooze); color: var(--snooze); }
|
||
.tl-note .tl-dot, .tl-resolution_note .tl-dot { border-color: var(--accent); color: var(--accent); }
|
||
.tl-problem .tl-dot { border-color: var(--warn); background: var(--warn-soft); color: var(--warn); }
|
||
.tl-problem.tl-notify_failed .tl-dot, .tl-problem.tl-deadman_silent .tl-dot { border-color: var(--crit); background: var(--crit-soft); color: var(--crit); }
|
||
.tl-problem .tl-text { color: var(--warn); font-weight: 600; }
|
||
.tl-problem.tl-notify_failed .tl-text, .tl-problem.tl-deadman_silent .tl-text { color: var(--crit); }
|
||
.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 14px; }
|
||
.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; }
|
||
/* The extra actions that only phones hide behind "More" — see actionBar() and
|
||
extraActions() in incident.js. Hidden by default; the desktop block below
|
||
shows them and hides the now-redundant More button instead. */
|
||
.action-extra { display: none; }
|
||
|
||
.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: var(--fs-lg); 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); }
|
||
|
||
.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); }
|
||
.you { color: var(--accent); font-weight: 650; font-size: 13px; margin-left: 6px; }
|
||
/* Oncall's own "you" indicator only (see you() in oncall.js) — a pill badge
|
||
is easier to spot there than this plain accent-coloured text. */
|
||
.you-badge { margin-left: 6px; }
|
||
|
||
.week-nav { display: flex; align-items: center; gap: 4px; }
|
||
.week-nav .month-label { font-size: 15px; font-weight: 650; min-width: 8em; text-align: center; }
|
||
/* The week-nav button that shows the date range, "28 Sep – 4 Oct", with the
|
||
ISO week number as secondary text inside it — a separate class from
|
||
.label above (team.js's month-nav uses that one) so its <small> isn't
|
||
caught by the unrelated .label > span styling meant for label chips. */
|
||
.week-nav .week-label { font-size: 14px; font-weight: 650; min-width: 11.5em; text-align: center; white-space: nowrap; }
|
||
.week-label small { color: var(--faint); font-weight: 600; font-size: 11px; margin-left: 2px; }
|
||
.oncall-title h1 { font-size: 24px; font-weight: 600; letter-spacing: -0.01em; margin: 8px 0 0; }
|
||
.oncall-title p { margin: 2px 0 0; font-size: 14px; }
|
||
.oncall-grid { display: grid; gap: 0; }
|
||
.oncall-main, .oncall-side { min-width: 0; }
|
||
.hero-list { display: grid; gap: 12px; }
|
||
|
||
.hero { display: grid; gap: 14px; padding: 18px; margin-top: 16px; }
|
||
.hero-top { display: flex; align-items: center; gap: 16px; }
|
||
.hero-label { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
|
||
.hero-avatar { width: 64px; height: 64px; font-size: 24px; }
|
||
.hero-name { font-size: 24px; font-weight: 650; letter-spacing: -0.01em; display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
|
||
.hero-until { color: var(--muted); font-size: 15px; margin-top: 2px; }
|
||
|
||
.week-card { margin-top: 16px; padding: 16px 12px 12px; }
|
||
.week-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 12px; padding: 0 4px; }
|
||
.week-head h2 { margin: 0; font-size: 15px; font-weight: 650; }
|
||
/* Bordered, so the arrows read as buttons. 40px: a touch target without
|
||
crowding the date between them. */
|
||
.week-arrow { width: 40px; min-height: 40px; border: 1px solid var(--border-strong); }
|
||
.strip { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
|
||
.strip-day {
|
||
position: relative; min-width: 0;
|
||
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||
padding: 10px 0 8px; border: 1px solid transparent; border-radius: var(--radius);
|
||
}
|
||
.strip-name { font-size: 12px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
|
||
.strip-date { font-size: 15px; font-weight: 650; }
|
||
.strip-avatar { width: 32px; height: 32px; font-size: 13px; }
|
||
.strip-who { max-width: 100%; padding: 0 2px; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.strip-who.nobody { font-style: italic; color: var(--faint); }
|
||
.strip-day.past { opacity: 0.6; }
|
||
/* "Current", not "selected": a neutral tint, a bar and aria-current, so it
|
||
reads as a marker and not as a pressed button. The accent stays for things
|
||
you can act on. */
|
||
.strip-day.today { background: var(--surface-2); border-color: var(--border-strong); }
|
||
.strip-day.today::before {
|
||
content: ""; position: absolute; top: -1px; left: 12px; right: 12px; height: 3px;
|
||
border-radius: 0 0 3px 3px; background: var(--text);
|
||
}
|
||
.strip-day.today .strip-name { color: var(--text); }
|
||
.strip-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 10px; padding: 10px 4px 0; border-top: 1px solid var(--border); font-size: 13px; color: var(--muted); }
|
||
.strip-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
||
.strip-legend .strip-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
|
||
|
||
.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; }
|
||
.account-fold { border-top: 1px solid var(--border); padding-top: 12px; }
|
||
.account-fold .stacked-form { margin-top: 8px; }
|
||
.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-team-selector { display: inline-flex; margin: -8px 10px 14px; width: calc(100% - 20px); }
|
||
.nav-link-secondary { display: flex; }
|
||
.nav-more-btn { display: none; }
|
||
.nav-link {
|
||
flex: none; 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; }
|
||
.chips-fade { display: none; }
|
||
/* With nothing selected there is no detail to show next to, so the list
|
||
takes the whole row instead of leaving the second column as dead space
|
||
around the placeholder text. Selecting an incident (.has-detail) drops
|
||
back to the base minmax(340,420) 1fr rule above. */
|
||
.view-queue:not(.has-detail) { grid-template-columns: 1fr; }
|
||
.view-queue:not(.has-detail) .pane-list { border-right: 0; }
|
||
/* Full width reads better capped than edge-to-edge on a very wide monitor,
|
||
matching .detail's own cap below. */
|
||
.view-queue:not(.has-detail) .list { max-width: 900px; margin: 0 auto; }
|
||
|
||
/* 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;
|
||
flex-wrap: wrap;
|
||
}
|
||
.actionbar .btn-primary { flex: 0 1 240px; }
|
||
/* Room enough to show every action, so More is fully redundant here. */
|
||
.action-extra { display: inline-flex; }
|
||
.more-btn { display: none; }
|
||
|
||
.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); }
|
||
|
||
/* --- status lists: alert sources and dead man's switches -----------------
|
||
Six columns do not fit a phone, so the table scrolls inside its card rather
|
||
than the page. A heartbeat under a switch with several is indented, the way
|
||
the escalation ladder indents its levels. */
|
||
/* A card with a header row (the Team and Admin settings cards) pads itself:
|
||
plain .card has no padding, so these used to print their text flush against
|
||
the border with the button jammed in the corner. The divider separates the
|
||
title row from the content below it. */
|
||
.card:has(> .card-head) { padding: 20px; }
|
||
@media (min-width: 900px) { .card:has(> .card-head) { padding: 24px; } }
|
||
.card-head {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
|
||
padding-bottom: 14px; margin-bottom: 14px; border-bottom: 1px solid var(--border);
|
||
}
|
||
.card-head h2 { margin: 0; font-size: 17px; font-weight: 650; }
|
||
.table-scroll { overflow-x: auto; margin-top: 12px; }
|
||
.status-table th, .status-table td { white-space: nowrap; }
|
||
.status-table td.wrap { white-space: normal; min-width: 12em; }
|
||
.status-table .source-row td { border-bottom-style: dashed; }
|
||
.status-table tr.clickable { cursor: pointer; }
|
||
.status-table tr.clickable:hover td, .status-table tr.clickable:focus-visible td { background: rgba(127, 127, 127, .1); }
|
||
.switch-facts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 12px 0; }
|
||
.switch-facts dt { opacity: .7; }
|
||
.switch-facts dd { margin: 0; }
|
||
.status-table .source-row td:first-child { padding-left: 16px; }
|
||
.source-labels { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
|
||
|
||
.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%); }
|
||
|
||
/* 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 { padding: 10px 12px 4px; }
|
||
.rota-heads, .rota-row { display: grid; grid-template-columns: 2.4em repeat(7, minmax(0, 1fr)); column-gap: 2px; }
|
||
.rota-wd {
|
||
padding-bottom: 6px; text-align: center;
|
||
color: var(--muted); font-size: 11px; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: 0.04em;
|
||
}
|
||
.rota-row { grid-template-rows: 26px 34px; padding: 4px 0 6px; }
|
||
.rota-row + .rota-row { border-top: 1px solid var(--border); }
|
||
/* A day is a cell that spans both of its row's lines: the number on the first,
|
||
the shift bar drawn over the second. The bar ignores the pointer, so a tap
|
||
anywhere in the cell reaches the button. */
|
||
.rota-day {
|
||
grid-row: 1 / span 2;
|
||
display: flex; justify-content: center; align-items: flex-start;
|
||
min-width: 0; padding-top: 2px;
|
||
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); }
|
||
button.rota-day:focus-visible, button.rota-week:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||
/* The week number starts each row. Quiet by default, because it is a label
|
||
first; an owner's tap on it is the second thing it does. */
|
||
.rota-week {
|
||
grid-column: 1; grid-row: 1 / span 2;
|
||
display: grid; place-items: center;
|
||
border: 0; border-radius: var(--radius-sm); background: none;
|
||
font: inherit; font-size: 12px; font-variant-numeric: tabular-nums;
|
||
color: var(--faint);
|
||
}
|
||
button.rota-week { cursor: pointer; }
|
||
button.rota-week:hover { background: var(--surface-2); color: var(--text); }
|
||
.rota-week.current { color: var(--text); font-weight: 700; }
|
||
/* The sheet's row of who holds each day of the week. */
|
||
.week-holders { display: flex; justify-content: space-between; gap: 4px; margin: 4px 0 12px; }
|
||
.week-holder { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; }
|
||
.week-holder.past { opacity: 0.55; }
|
||
.rota-num {
|
||
min-width: 22px; height: 22px; padding: 0 3px; border-radius: 11px;
|
||
text-align: center; line-height: 22px;
|
||
color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums;
|
||
}
|
||
/* Today is a filled number, a marker and not the accent, which means "you can
|
||
act on this". */
|
||
.rota-day.today .rota-num { background: var(--text); color: var(--bg); font-weight: 700; }
|
||
/* 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 .rota-num { opacity: 0.4; }
|
||
|
||
/* A shift: a run of consecutive days held by one person, in that person's
|
||
colour (.rc1-.rc6 below). Rounded ends are where it really starts or stops;
|
||
a flat end with a chevron carries on across the row break. */
|
||
.rota-bar {
|
||
grid-row: 2; align-self: center; z-index: 1; pointer-events: none;
|
||
display: flex; align-items: center; min-width: 0; height: 28px; padding: 0 8px;
|
||
border-radius: 6px;
|
||
font-size: 13px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.rota-bar.past { opacity: 0.55; }
|
||
.rota-bar.outside { opacity: 0.4; }
|
||
.rota-bar.cont-l { margin-left: -2px; padding-left: 10px; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
||
.rota-bar.cont-r { margin-right: -2px; border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
||
.rota-bar.cont-l::before { content: "‹"; margin-right: 4px; opacity: 0.7; }
|
||
/* Nobody on call. Loud while it can still be fixed, quiet once it is history. */
|
||
.rota-bar.gap {
|
||
background: repeating-linear-gradient(135deg, transparent 0 5px, var(--warn-soft) 5px 10px);
|
||
border: 1px dashed var(--warn); color: var(--warn);
|
||
}
|
||
.rota-bar.gap.past { background: none; border-color: var(--border-strong); color: var(--faint); }
|
||
|
||
.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-banner {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 12px 14px; border-top: 1px solid var(--border); font-size: 14px;
|
||
}
|
||
.rota-banner .icon { width: 20px; height: 20px; }
|
||
.rota-banner.ok { color: var(--ok); }
|
||
.rota-banner.warn { color: var(--warn); background: var(--warn-soft); }
|
||
.rota-foot {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
|
||
padding: 12px 14px; border-top: 1px solid var(--border);
|
||
}
|
||
.rota-bulk-form { padding: 0 14px 14px; }
|
||
.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; }
|
||
.ladder-editor { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; margin-top: 12px; }
|
||
/* A target that would not wake anybody says why, in place: it is the reason a
|
||
level is red, and the thing to go and fix. */
|
||
/* The escalation ladder: a numbered step per level, the wait between levels as
|
||
its own line, and what happens after the last one at the bottom. */
|
||
.ladder { list-style: none; margin: 16px 0 0; padding: 0; }
|
||
.step { display: grid; grid-template-columns: 32px minmax(0, 1fr); column-gap: 14px; }
|
||
.step-rail { display: flex; flex-direction: column; align-items: center; }
|
||
.step-node {
|
||
display: grid; place-items: center; flex: none;
|
||
width: 32px; height: 32px; border-radius: 50%;
|
||
border: 2px solid var(--border-strong); background: var(--surface);
|
||
font-size: 14px; font-weight: 750;
|
||
}
|
||
.step-node.st-ready { border-color: var(--ok); color: var(--ok); }
|
||
.step-node.st-escalating { border-color: var(--warn); color: var(--warn); background: var(--warn-soft); }
|
||
.step-node.st-unreachable { border-color: var(--crit); color: var(--crit); background: var(--crit-soft); }
|
||
.step-node.end { border-style: dashed; color: var(--muted); }
|
||
.step-node.end.warn { border-color: var(--warn); color: var(--warn); background: var(--warn-soft); }
|
||
.step-node .icon { width: 16px; height: 16px; }
|
||
.step-line { flex: 1; width: 2px; min-height: 8px; background: var(--border-strong); }
|
||
.step-body { min-width: 0; padding-bottom: 4px; }
|
||
.step-top { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-height: 32px; }
|
||
.step-pages { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; font-size: 14px; }
|
||
.step-waiting { margin-top: 6px; font-size: 13px; color: var(--muted); }
|
||
.step-waiting a { font-weight: 650; }
|
||
.step-gap { display: flex; align-items: center; gap: 8px; padding: 6px 0 10px; color: var(--muted); font-size: 13px; font-weight: 600; }
|
||
.step-gap .icon { width: 14px; height: 14px; }
|
||
.callout {
|
||
display: flex; align-items: flex-start; gap: 12px; margin-top: 4px;
|
||
padding: 12px 14px; border: 1px solid var(--warn); border-radius: var(--radius);
|
||
background: var(--warn-soft);
|
||
}
|
||
.callout > .icon { flex: none; width: 20px; height: 20px; margin-top: 1px; color: var(--warn); }
|
||
.callout strong { color: var(--warn); }
|
||
.callout p { margin: 2px 0 10px; font-size: 14px; }
|
||
.ladder-facts { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); }
|
||
.ladder-facts p { margin: 0; }
|
||
.ladder-facts p + p { margin-top: 4px; }
|
||
|
||
.target-line { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
|
||
.target-problem { color: var(--crit); font-size: 12px; font-weight: 600; }
|
||
|
||
/* 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; }
|
||
/* A fade on the right edge says "there is more" where the strip overflows;
|
||
the desktop width fits every entry, so it is phone-only. */
|
||
@media (max-width: 899px) {
|
||
.subnav { -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); }
|
||
}
|
||
.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; }
|
||
.overview-note-icon { width: 13px; height: 13px; vertical-align: -2px; color: var(--ok); }
|
||
|
||
/* ---------- 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); }
|
||
}
|
||
|
||
/* On-call, wide: the hero and the week on the left, your shifts on the right.
|
||
.view-page > * caps its children at 760px, so the grid lifts that. */
|
||
@media (min-width: 900px) {
|
||
#view-oncall > .oncall-grid { max-width: 1080px; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 0 24px; align-items: start; }
|
||
#view-oncall > .oncall-title { max-width: 1080px; }
|
||
}
|