test / test (push) Canceled after 0s
A message's From: line is held against the application that sends it, under the rule Postfix holds the envelope sender to. OpenDKIM chooses the key by that line, so until now an application could have mail signed as another domain of the server or as the server itself. The check (internal/milter/sender.go) - A role of the panel on a socket of its own (SENDER_CHECK_SOCKET), first in smtpd_milters and strict: default_action=tempfail. The journal milter keeps its socket and stays fail-open. postfix-wrapper waits for the new socket. - It reads no database: what an application may send as comes from Postfix's own sender_login_maps, what the server signs for from OpenDKIM's SigningTable. Both are read again when they change; one that cannot be read defers mail. - One From: field, one address, allowed by the envelope's lookup; then no other name of the SigningTable may appear anywhere in the raw line. Refused: 550 5.7.1 naming the line, reduced to printable ASCII. - A refusal is a send-log row with reason "from" and the header address (migration 0006), written by another goroutine and not waited for. The journal's refusals record their reason too (limit, client), and the notifier takes only a limit's. What it costs (docs/plans/from-header-binding.md, Statistics) - The role marks itself alive every 2 s; at start the gap since the mark is downtime, counted from Postfix's start if later and not at all when supervisord says Postfix is not running. - The log tailer counts the connections Postfix turned away: a warning that names the socket, then milter-reject "Service unavailable" from the same smtpd. Seen on the stand: the client is refused from CONNECT on and leaves at AUTH (454), so no MAIL line is ever written. - sender_check_days, a row per UTC day, kept a year. Pages - Health: the card "Sender check", the socket's row, and the cards layout - boxes numbered by importance in two stacks read in rows; the left stack polls, the three readings of the right one are swapped in by id. - Outbound log: a refused row says why in its tag (wrong From, wrong client, rate-limited); the message's page shows the From: line and what the application may send as. - Help: topic "Sender check", word for word the mockup's. Found by the stand, fixed here: the names for the second look came from the sender bindings, and a domain with a key and no application yet was not among them. e2e: negative_journal_milter_fail_open is gone - it proved that mail passes with the panel down, which is no longer so. In its place sender_check_holds_the_from_line (another domain's From:, the hostname's, a stranger's, an address off the list, another sender in the display name - refused; own addresses - delivered) and panel_down_defers_mail (4xx with the panel stopped, the container alive, mail again after, downtime and one deferred message on Health, the card warning). Test server, this tree before the last cosmetic edits (a fixture row, a fact's size, docs): go vet ok, go test 23 packages ok, make e2e ok (337 s). Locally after them: internal/web/view ok. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
370 lines
23 KiB
CSS
370 lines
23 KiB
CSS
/* The panel's stylesheet: the brand layer (fonts, palette) followed by the
|
|
theme, taken rule for rule from the accepted mockups in
|
|
docs/assets/panel-redesign/ (shared/brand.css, panel/theme.css). A rule is
|
|
added here only after it exists there — a design change is made in the
|
|
mockup first (docs/plans/panel-redesign.md § The contract). The font files
|
|
and perf-edge.svg sit beside this file in /static. TestPanelCSSIsTheMockupStylesheet
|
|
holds this file to that. */
|
|
|
|
/* Brand layer shared by the three redesign directions: the panel's own fonts
|
|
and the palette of the mark (docs/assets/selfpost-proof.html). Each
|
|
direction maps these tokens onto its library's variables in its own
|
|
theme.css — nothing here knows about Pico, Tabler or Bulma.
|
|
|
|
Mockup note: no inline style="" and no inline <script> anywhere in this
|
|
folder, so every screen is drawable under the panel's CSP as it stands. */
|
|
|
|
@font-face {
|
|
font-family: "IBM Plex Sans"; font-style: normal; font-weight: 100 700;
|
|
font-display: swap;
|
|
src: url("ibm-plex-sans.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400;
|
|
font-display: swap;
|
|
src: url("ibm-plex-mono-400.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 600;
|
|
font-display: swap;
|
|
src: url("ibm-plex-mono-600.woff2") format("woff2");
|
|
}
|
|
|
|
:root {
|
|
--sp-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
--sp-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
|
|
--sp-paper: #f4f2ed; /* page */
|
|
--sp-stamp-paper: #f3ede1; /* the mark's own paper */
|
|
--sp-sheet: #fff;
|
|
--sp-ink: #12161c;
|
|
--sp-ink-2: #2a313b;
|
|
--sp-muted: #5c6774; /* 5:1 on the paper and on the tinted cards; #6b7684 was 4.1:1 */
|
|
--sp-rule: #dedcd7;
|
|
--sp-rule-strong: #cbc8c1;
|
|
--sp-well: #efede9; /* code / read-only values */
|
|
|
|
--sp-brick: #7a3b2e;
|
|
--sp-brick-dark: #632f25;
|
|
--sp-brick-tint: #ede4de;
|
|
--sp-on-brick: #f6ede2;
|
|
|
|
--sp-ok: #2c6b43; --sp-ok-bg: #edf6ef; --sp-ok-rule: #c2e0cc;
|
|
--sp-warn: #8a5510; --sp-warn-bg: #fbf2e2; --sp-warn-rule: #ebd5a6;
|
|
--sp-fail: #b42318; --sp-fail-bg: #fbedea; --sp-fail-rule: #efccc4;
|
|
--sp-idle: #6b7684; --sp-idle-bg: #efede9; --sp-idle-rule: #dedcd7;
|
|
}
|
|
|
|
/* The accepted direction: C ("Counter", Bulma) as the base, the postal
|
|
signature of A (postmark, numbered mono section heads, the stamp's
|
|
perforation) and the icons of B. Bulma carries navbar, dropdowns, columns,
|
|
boxes, tables, tags, menu, forms; what follows is brand mapping plus the
|
|
handful of components that are SelfPost's own. */
|
|
|
|
:root {
|
|
--bulma-family-primary: var(--sp-sans);
|
|
--bulma-family-secondary: var(--sp-sans);
|
|
--bulma-family-code: var(--sp-mono);
|
|
--bulma-primary-h: 10deg; --bulma-primary-s: 45%; --bulma-primary-l: 33%;
|
|
--bulma-link-h: 10deg; --bulma-link-s: 45%; --bulma-link-l: 33%;
|
|
--bulma-success-h: 138deg; --bulma-success-s: 42%; --bulma-success-l: 30%;
|
|
--bulma-warning-h: 36deg; --bulma-warning-s: 85%; --bulma-warning-l: 55%;
|
|
--bulma-danger-h: 4deg; --bulma-danger-s: 76%; --bulma-danger-l: 40%;
|
|
--bulma-radius: 6px; --bulma-radius-large: 10px;
|
|
--bulma-body-size: 16px;
|
|
/* The type scale. Three sizes carry all running text; nothing a person has
|
|
to read is below 13px. check.py enforces the floor.
|
|
body 16px text, inputs, buttons, menus, table cells
|
|
s 14px supporting text: labels, help, notes, footers, mono values, logs
|
|
xs 13px upper-case mono micro-labels and tags only */
|
|
--sp-fs-s: .875rem;
|
|
--sp-fs-xs: .8125rem;
|
|
--bulma-link-text: var(--sp-brick);
|
|
--bulma-link-text-hover: var(--sp-brick-dark);
|
|
}
|
|
/* Bulma picks a button's text colour from the lightness of its fill; our brick
|
|
and our red are dark enough to land on the wrong side and get near-black
|
|
text (3:1). Solid buttons are white on colour; the light variants keep
|
|
Bulma's dark-on-tint. */
|
|
.button.is-primary:not(.is-light), .button.is-primary:not(.is-light):hover,
|
|
.button.is-danger:not(.is-light), .button.is-danger:not(.is-light):hover { color: #fff; }
|
|
a strong { color: inherit; }
|
|
html, body { background: var(--sp-paper); }
|
|
body { color: var(--sp-ink); min-height: 100vh; display: flex; flex-direction: column; }
|
|
main { flex: 1; }
|
|
code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; padding: .1em .35em; font-size: .95em; }
|
|
.ti { font-size: 1.15em; vertical-align: -.12em; }
|
|
.button .ti { margin-right: .35rem; }
|
|
.sp-muted { color: var(--sp-muted); }
|
|
/* Bulma ships these at .75rem; on the scale they are s or xs. `sp-small`
|
|
replaces Bulma's `is-size-7` helper, which is .75rem and !important. */
|
|
.help, .label { font-size: var(--sp-fs-s); }
|
|
.tag, .menu-label { font-size: var(--sp-fs-xs); }
|
|
.menu-label { color: var(--sp-muted); } /* Bulma's grey is 4.2:1 on the paper */
|
|
.sp-small { font-size: var(--sp-fs-s); }
|
|
.sp-mono { font-family: var(--sp-mono); }
|
|
.sp-nowrap { white-space: nowrap; }
|
|
|
|
/* ---- navbar, the perforated edge under it, sibling pages below */
|
|
.navbar.is-primary { --bulma-navbar-height: 4rem; }
|
|
/* The name, not the stamp: at this size the stamp's perforation is under a
|
|
pixel and its brick field dissolves into the bar. The stamp stays where it
|
|
has room — sign-in, setup, the favicon. */
|
|
.navbar-brand img { max-height: none; height: 1.45rem; }
|
|
.navbar.is-primary .navbar-item, .navbar.is-primary .navbar-link { color: var(--sp-on-brick); font-weight: 500; }
|
|
.navbar.is-primary .navbar-link::after { border-color: var(--sp-on-brick); }
|
|
.navbar.is-primary .navbar-link .ti { margin-right: .4rem; }
|
|
/* Bulma shrinks dropdown items to .875rem; a menu entry is read as often as
|
|
the bar it hangs from, so it gets the bar's size. */
|
|
.navbar-dropdown { border-top: 0; box-shadow: 0 8px 24px rgb(18 22 28 / .14); font-size: 1rem; }
|
|
.navbar.is-primary .navbar-dropdown .navbar-item { color: var(--sp-ink); font-weight: 400; padding-right: 2rem; }
|
|
.navbar.is-primary .navbar-dropdown .navbar-item .ti { margin-right: .6rem; color: var(--sp-muted); }
|
|
.navbar.is-primary .navbar-dropdown .navbar-item.is-active { color: var(--sp-brick); font-weight: 600; background: var(--sp-brick-tint); }
|
|
.navbar.is-primary .navbar-dropdown .navbar-item.is-active .ti { color: var(--sp-brick); }
|
|
/* Sign out is a POST, so it is a form with a button rather than a link. Bulma
|
|
tints only a.navbar-item; the button is handed the same variables, and the
|
|
form stretches it across the menu, so it answers the pointer, the keyboard
|
|
and a press exactly like the entries above it. */
|
|
.navbar-dropdown form { display: flex; }
|
|
.navbar-dropdown button.navbar-item { flex: 1; cursor: pointer; }
|
|
.navbar-dropdown button.navbar-item:hover, .navbar-dropdown button.navbar-item:focus { --bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-hover-background-l-delta); }
|
|
.navbar-dropdown button.navbar-item:active { --bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-active-background-l-delta); }
|
|
.navbar.is-primary .sp-current > .navbar-link, .navbar.is-primary a.navbar-item.sp-current { background: rgb(0 0 0 / .18); }
|
|
/* The header is built like the mark: a brick field, then the stamp's paper
|
|
margin, and the perforation runs along the outer edge of that margin — round
|
|
holes, flat teeth, the mark's ink hairline (perf-edge.svg is one tooth and
|
|
one hole at the mark's 2.2 / 7.03 ratio). On the mark the margin is one
|
|
pitch deep and a tenth of the brick field's height; here that is 8px under
|
|
a 60px bar — a little over a tenth, so a 2.5px hole still reads as round. */
|
|
.sp-perf {
|
|
height: 8px;
|
|
background:
|
|
url("perf-edge.svg") repeat-x left bottom / 8px 4px,
|
|
linear-gradient(var(--sp-stamp-paper), var(--sp-stamp-paper)) no-repeat left top / 100% 4px;
|
|
}
|
|
.sp-subnav { border-bottom: 1px solid var(--sp-rule); margin-top: -1px; }
|
|
.sp-subnav .container { display: flex; gap: 1.8rem; overflow-x: auto; }
|
|
.sp-subnav a { color: var(--sp-muted); padding: .75rem 0 .65rem; white-space: nowrap; border-bottom: 2px solid transparent; }
|
|
.sp-subnav a .ti { margin-right: .4rem; }
|
|
.sp-subnav a:hover { color: var(--sp-ink); }
|
|
.sp-subnav a[aria-current] { color: var(--sp-ink); font-weight: 600; border-bottom-color: var(--sp-brick); }
|
|
|
|
/* ---- page head */
|
|
.sp-page { padding-block: 1.8rem 2.5rem; }
|
|
.sp-head { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
|
.sp-head .sp-grow { flex: 1; min-width: 14rem; }
|
|
.sp-kicker { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); }
|
|
.sp-kicker a { color: inherit; }
|
|
.sp-kicker a:hover { color: var(--sp-brick); }
|
|
.title { color: var(--sp-ink); font-weight: 600; }
|
|
.sp-head .title { margin: 0; overflow-wrap: anywhere; }
|
|
.sp-head .sp-lead { color: var(--sp-muted); margin-top: .2rem; max-width: 48rem; }
|
|
.sp-head .buttons { margin: 0; }
|
|
.sp-head .buttons > * { margin-bottom: 0; }
|
|
|
|
/* ---- postmark */
|
|
.sp-postmark {
|
|
flex: none; width: 6rem; height: 6rem; border-radius: 50%; transform: rotate(-9deg);
|
|
border: 2px solid currentColor; outline: 1px solid currentColor; outline-offset: 3px; margin: .3rem .5rem;
|
|
display: grid; place-content: center; text-align: center; color: var(--sp-ok);
|
|
font-family: var(--sp-mono); font-weight: 600; letter-spacing: .08em; line-height: 1.15;
|
|
}
|
|
.sp-postmark.sp-warn { color: var(--sp-warn); }
|
|
.sp-postmark.sp-fail { color: var(--sp-fail); }
|
|
.sp-postmark b { font-size: 1.3rem; display: block; }
|
|
/* Below the floor on purpose: an imprint, and the heading beside it says the same in words. */
|
|
.sp-postmark span { font-size: .6rem; display: block; }
|
|
|
|
/* ---- box with a numbered mono head */
|
|
.box { box-shadow: none; border: 1px solid var(--sp-rule); padding: 0; overflow: hidden; }
|
|
.box > .sp-box-head {
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; padding: .7rem 1.25rem; border-bottom: 1px solid var(--sp-rule);
|
|
}
|
|
.sp-box-head form.sp-end { justify-content: flex-end; }
|
|
/* Bulma's .input is 100% wide; in the filter row it takes a flex basis instead, so it sits beside the selects. */
|
|
.sp-box-head form.sp-end > .input { flex: 0 1 10.5rem; }
|
|
.sp-box-head .sp-no { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 600; color: var(--sp-brick); }
|
|
.sp-box-head h2 { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 600; letter-spacing: .1em; text-transform: uppercase; margin: 0; }
|
|
.sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: var(--sp-fs-s); display: flex; gap: .6rem; align-items: center; }
|
|
.sp-box-head .sp-help { color: var(--sp-muted); line-height: 1; }
|
|
.sp-box-head .sp-help:hover { color: var(--sp-brick); }
|
|
.sp-box-head .sp-end + .sp-help { margin-left: 0; }
|
|
.sp-box-head h2 + .sp-help { margin-left: auto; }
|
|
.notification { border: 1px solid transparent; padding: .8rem 1.25rem; }
|
|
.notification.is-success.is-light { border-color: var(--sp-ok-rule); }
|
|
.notification.is-danger.is-light { border-color: var(--sp-fail-rule); }
|
|
.notification .ti { margin-right: .5rem; }
|
|
.box > .sp-box-body { padding: 1.25rem; }
|
|
.box > .sp-box-body > :last-child { margin-bottom: 0; }
|
|
/* A table never sits bare in a box: the container lets a wide one scroll
|
|
sideways instead of being cut off by the box's rounded overflow. */
|
|
.box > .table-container, .box > .table-container > .table { margin: 0; }
|
|
.sp-box-foot .sp-end { margin-left: auto; }
|
|
/* A filter form in the head keeps full-size controls; the head gives up its
|
|
own padding to them instead of shrinking the text. */
|
|
.sp-box-head form.sp-end { margin-block: -.35rem; font-size: 1rem; }
|
|
.box > .sp-box-foot {padding: .7rem 1.25rem; border-top: 1px solid var(--sp-rule); color: var(--sp-muted); font-size: var(--sp-fs-s); display: flex; gap: 1rem; align-items: center; }
|
|
.box.sp-danger-zone { border-color: var(--sp-fail-rule); }
|
|
.box.sp-danger-zone .sp-box-head { background: var(--sp-fail-bg); border-color: var(--sp-fail-rule); }
|
|
.box.sp-credential { border-color: #e0c874; }
|
|
.box.sp-credential .sp-box-head { background: #fcf6e4; border-color: #e0c874; }
|
|
|
|
.table { background: transparent; }
|
|
.table thead th { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 400; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
|
/* Addresses in tables carry a <wbr> after the @ (one template function), so a
|
|
long one breaks where it reads naturally and its column can still shrink;
|
|
`overflow-wrap: anywhere` would do it too, but mid-word. */
|
|
.table td, .table th { vertical-align: middle; padding: .55rem .6rem; }
|
|
.box > .table-container td:first-child, .box > .table-container th:first-child { padding-left: 1.25rem; }
|
|
.box > .table-container td:last-child, .box > .table-container th:last-child { padding-right: 1.25rem; }
|
|
.table td.sp-actions { text-align: right; white-space: nowrap; }
|
|
/* An action that changes state (New password, Delete) is a POST, so it is a form
|
|
with a button, not a link. The form sits inline like the links beside it and
|
|
the button is handed the link's colour and type; Bulma already resets the
|
|
button and shares the pointer and the focus ring between a and button, and a
|
|
link here has no hover colour of its own. */
|
|
.table td.sp-actions a, .table td.sp-actions form { margin-left: .9rem; }
|
|
.table td.sp-actions form { display: inline; }
|
|
.table td.sp-actions button { color: var(--bulma-link-text); font: inherit; }
|
|
.table td.sp-clip { max-width: 13rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.tag { font-family: var(--sp-mono); font-weight: 600; letter-spacing: .04em; }
|
|
.sp-empty { padding: 2.2rem 1.25rem; text-align: center; color: var(--sp-muted); }
|
|
.sp-empty .ti { font-size: 2rem; display: block; margin-bottom: .4rem; color: var(--sp-rule-strong); }
|
|
|
|
/* ---- health cards */
|
|
/* Six cards: six, three or two columns — never a count that leaves one card alone on a row. */
|
|
.sp-health { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0; }
|
|
.sp-health > a {
|
|
display: flex; gap: .7rem; align-items: center; padding: .75rem 1rem; color: var(--sp-ink); min-width: 0;
|
|
border-right: 1px solid var(--sp-rule); border-bottom: 1px solid var(--sp-rule); margin: 0 -1px -1px 0;
|
|
}
|
|
.sp-health > a:hover { background: var(--sp-paper); }
|
|
.sp-health > a > span:last-child { min-width: 0; }
|
|
.sp-h-icon { flex: none; width: 2.1rem; height: 2.1rem; border-radius: 7px; display: grid; place-content: center; background: var(--sp-ok-bg); color: var(--sp-ok); }
|
|
.sp-h-icon .ti { font-size: 1.25rem; vertical-align: 0; }
|
|
.sp-health > a.sp-warn { background: var(--sp-warn-bg); }
|
|
.sp-health > a.sp-warn .sp-h-icon { background: #f6e2b8; color: #8a5510; }
|
|
.sp-health > a.sp-fail { background: var(--sp-fail-bg); }
|
|
.sp-health > a.sp-fail .sp-h-icon { background: #f5d3cc; color: var(--sp-fail); }
|
|
/* Each line is a block with its own tight line height; a verdict is two or
|
|
three words and never wraps — a wrapped card would stretch the whole row. */
|
|
.sp-h-name, .sp-h-value, .sp-h-sub { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.sp-h-name { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); line-height: 1.3; }
|
|
.sp-h-value { font-weight: 600; line-height: 1.3; }
|
|
.sp-h-sub { font-size: var(--sp-fs-s); color: var(--sp-muted); line-height: 1.3; }
|
|
|
|
/* ---- facts, records, side menu */
|
|
.sp-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 1rem 1.5rem; margin: 0; }
|
|
.sp-facts dt { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
|
.sp-facts dd { margin: 0; overflow-wrap: anywhere; }
|
|
.sp-facts dd.sp-big { font-size: 1.5rem; font-weight: 600; line-height: 1.2; }
|
|
.sp-side { position: sticky; top: 1.5rem; }
|
|
.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; }
|
|
.menu-list a { display: flex; align-items: center; gap: .55rem; }
|
|
.menu-list a .ti { color: var(--sp-muted); }
|
|
.menu-list a.is-active .ti { color: inherit; }
|
|
.menu-list a .tag { margin-left: auto; }
|
|
.sp-record { padding: 1.25rem; }
|
|
.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); }
|
|
.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
|
|
.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; }
|
|
.sp-record-head small { margin-left: auto; color: var(--sp-muted); text-align: right; }
|
|
.sp-record .label, .sp-form .label { font-weight: 500; color: var(--sp-ink-2); margin-bottom: .25rem; }
|
|
.sp-record .label { font-weight: 400; color: var(--sp-muted); }
|
|
.input[readonly], .textarea[readonly] { font-family: var(--sp-mono); font-size: var(--sp-fs-s); background: var(--sp-well); box-shadow: none; }
|
|
/* Bulma sizes a control from its own font; the smaller mono text made the
|
|
value shorter than the Copy button joined to it. */
|
|
.input[readonly] { height: 2.5rem; }
|
|
.textarea[readonly] { min-height: 2.5rem; }
|
|
.textarea[readonly] { resize: none; min-height: 0; }
|
|
.sp-record .field:not(:last-child) { margin-bottom: .6rem; }
|
|
.sp-row { display: grid; grid-template-columns: 1fr 7rem; gap: .75rem; }
|
|
.sp-pair { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
|
|
|
|
/* ---- logs, timeline, help */
|
|
.sp-log {
|
|
margin: 0; padding: 1rem 1.25rem; background: #191c21; color: #d9d5cc; border-radius: 0;
|
|
font-family: var(--sp-mono); font-size: var(--sp-fs-s); line-height: 1.6; overflow-x: auto; white-space: pre;
|
|
}
|
|
.sp-log .sp-w { color: #e0b15c; } .sp-log .sp-e { color: #ef8f82; } .sp-log .sp-d { color: #7f8894; }
|
|
.sp-timeline { list-style: none; margin: 0; padding: 0; }
|
|
.sp-timeline li { position: relative; padding: 0 0 1.1rem 1.6rem; border-left: 2px solid var(--sp-rule); margin-left: .4rem; }
|
|
.sp-timeline li:last-child { padding-bottom: 0; border-left-color: transparent; }
|
|
.sp-timeline li::before { content: ""; position: absolute; left: -7px; top: .25rem; width: 12px; height: 12px; border-radius: 50%; background: var(--sp-ok); box-shadow: 0 0 0 3px #fff; }
|
|
.sp-timeline li.sp-warn::before { background: #d9962b; }
|
|
.sp-timeline li.sp-fail::before { background: var(--sp-fail); }
|
|
.sp-timeline li.sp-pending::before { background: #fff; border: 2px solid var(--sp-rule-strong); }
|
|
.sp-timeline time { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); color: var(--sp-muted); display: block; }
|
|
.sp-route { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; font-family: var(--sp-mono); font-size: var(--sp-fs-s); margin-top: .3rem; }
|
|
.sp-help-topic + .sp-help-topic { border-top: 1px solid var(--sp-rule); }
|
|
.sp-help-topic { padding: 1rem 1.25rem; }
|
|
.sp-help-topic h3 { font-weight: 600; margin-bottom: .3rem; }
|
|
.sp-list-check { list-style: none; margin: 0 0 1rem; }
|
|
.sp-list-check li { padding-left: 1.7rem; position: relative; margin-bottom: .45rem; }
|
|
.sp-list-check li .ti { position: absolute; left: 0; top: .15rem; color: var(--sp-fail); }
|
|
|
|
/* ---- signed-out pages */
|
|
.sp-signin { min-height: 100vh; margin: 0; }
|
|
.sp-signin-brand { background: var(--sp-brick); display: grid; place-content: center; text-align: center; padding: 3rem; position: relative; }
|
|
.sp-signin-brand::after {
|
|
content: ""; position: absolute; top: 0; bottom: 0; right: -7px; width: 7px; z-index: 1;
|
|
background: radial-gradient(circle at 0 7px, var(--sp-brick) 5px, transparent 5.6px); background-size: 7px 14px;
|
|
}
|
|
.sp-signin-brand img { width: 20rem; max-width: 100%; height: auto; }
|
|
.sp-signin-brand p { font-family: var(--sp-mono); letter-spacing: .18em; font-size: var(--sp-fs-xs); color: var(--sp-on-brick); margin-top: 1.6rem; }
|
|
.sp-signin-form { display: flex; flex-direction: column; justify-content: center; padding: 3rem; }
|
|
.sp-signin-form .sp-form { width: 22rem; max-width: 100%; margin-inline: auto; }
|
|
.sp-foot { color: var(--sp-muted); font-size: var(--sp-fs-s); padding-block: 1.5rem 0; }
|
|
.sp-foot a { color: inherit; text-decoration: underline; }
|
|
.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; }
|
|
|
|
@media (max-width: 1407px) { .sp-health { grid-template-columns: repeat(3, 1fr); } }
|
|
@media (max-width: 1023px) { .sp-box-head form.sp-end { flex-wrap: wrap; } }
|
|
@media (max-width: 768px) {
|
|
.sp-health { grid-template-columns: repeat(2, 1fr); }
|
|
.sp-row, .sp-pair { grid-template-columns: 1fr; }
|
|
.sp-record-head { flex-wrap: wrap; }
|
|
.sp-record-head small { margin-left: 0; text-align: left; flex-basis: 100%; }
|
|
.sp-postmark { display: none; }
|
|
.sp-cards { display: flex; flex-direction: column; padding-inline: .75rem; }
|
|
.sp-cards > .column { display: contents; }
|
|
.sp-cards .box { margin-bottom: 1.5rem; }
|
|
.sp-cards > .column:first-child > .box:nth-child(1) { order: 1; }
|
|
.sp-cards > .column:first-child > .box:nth-child(2) { order: 3; }
|
|
.sp-cards > .column:first-child > .box:nth-child(3) { order: 5; }
|
|
.sp-cards > .column:first-child > .box:nth-child(4) { order: 7; }
|
|
.sp-cards > .column:last-child > .box:nth-child(1) { order: 2; }
|
|
.sp-cards > .column:last-child > .box:nth-child(2) { order: 4; }
|
|
.sp-cards > .column:last-child > .box:nth-child(3) { order: 6; }
|
|
.sp-cards > .column:last-child > .box:nth-child(4) { order: 8; }
|
|
}
|
|
|
|
/* cards: two stacks of boxes that are read in rows - the odd ones on the left,
|
|
the even ones on the right, each stack closed up so a short box leaves no
|
|
gap beside a tall one. On a phone the stacks dissolve and the boxes come in
|
|
the order of their numbers (the rules above). */
|
|
|
|
/* sheet: what does not fit a table row, opened over the page from the row's
|
|
tag. It is the browser's own popover (the tag is a button with
|
|
popovertarget), so it needs no script: Esc, a click outside and the close
|
|
button all shut it, and focus returns to the tag. It rises from the bottom
|
|
edge across the whole window - the one component that is not laid out by the
|
|
page, hence the widths - with its content, a table of name and value, in a
|
|
column of reading width in the middle. */
|
|
.sp-sheet {
|
|
inset: auto 0 0 0; width: auto; margin: 0; padding: 0; border: 0; border-top: 3px solid var(--sp-brick);
|
|
max-height: 75vh; overflow: auto; background: #fff; color: inherit; box-shadow: 0 -12px 40px rgba(40, 20, 15, .22);
|
|
}
|
|
.sp-sheet-in { max-width: 44rem; margin: 0 auto; padding: 0 1.25rem; }
|
|
.sp-sheet::backdrop { background: rgba(40, 20, 15, .38); }
|
|
.sp-sheet-head { display: flex; align-items: center; gap: .7rem; padding: 1rem 0 .8rem; border-bottom: 1px solid var(--sp-rule); }
|
|
.sp-sheet-head h2 { font-size: 1.15rem; font-weight: 600; margin-right: auto; }
|
|
.sp-sheet-head button { border: 0; background: none; cursor: pointer; font-size: 1.3rem; color: var(--sp-muted); }
|
|
.sp-sheet-body { padding: .4rem 0 1.4rem; }
|
|
/* The sheet's content is a table of name and value: the name is a label, set like the dt of facts. */
|
|
.sp-sheet .table th { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); white-space: nowrap; vertical-align: top; width: 1%; padding-right: 2rem; }
|
|
.sp-sheet .table td { overflow-wrap: anywhere; }
|
|
/* A tag that opens a sheet is a button: it keeps the tag's look and says it can
|
|
be pressed with the dotted underline links to more detail carry. */
|
|
button.tag { border: 0; cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px; }
|