Files
selfpost/internal/web/view/static/panel.css
T
mixandClaude Opus 5.5 c171fb5bc7
test / test (push) Canceled after 0s
from-header-binding: the sender check, what it costs, its pages; guide, architecture, security
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>
2026-10-12 19:35:43 +03:00

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; }