fae930a37a
test / test (push) Has been cancelled
Settings had been sharing the account silhouette with Users and the signed-in user label, so three different concepts read as the same icon. Settings now uses a gear, the user line the single-user icon, and Users a two-person group mark. Co-Authored-By: Cursor <cursoragent@cursor.com>
652 lines
40 KiB
CSS
652 lines
40 KiB
CSS
/* Panel stylesheet. It lives in a file rather than in a <style> block in the
|
||
layout so the panel's Content-Security-Policy can be a plain
|
||
"default-src 'self'" with no inline-style exemption. Any rule
|
||
added here must therefore stay here: an inline style="..." attribute in a
|
||
template is blocked by that policy and silently does nothing.
|
||
|
||
The palette and the type below come from the mark's own approval sheet
|
||
(docs/assets/selfpost-proof.html) — brick, warm paper, IBM Plex. Before it
|
||
the panel was a default blue-on-cool-grey admin theme with the stamp pasted
|
||
onto it, and the two read as different pieces of work. */
|
||
|
||
/* The fonts ship with the panel rather than being asked for by name. Two
|
||
reasons, and the second is the one that matters. A named family resolves to
|
||
whatever the operator's machine has, so the mark — Plex converted to
|
||
outlines — would be the only Plex on the page. And ui-monospace is Consolas,
|
||
SF Mono or DejaVu Sans Mono depending on that machine, each with its own
|
||
advance width: the six-column send log and the mail.log tables would lay out
|
||
differently for every operator. Served from the panel's own origin, so the
|
||
policy above already covers them and needs no font-src of its own.
|
||
One variable file for the sans (100–700 in 46 KB, less than the four static
|
||
weights it replaces); mono has no variable release, so it is two files.
|
||
swap, so a cold load shows the system stack for a frame instead of nothing. */
|
||
@font-face {
|
||
font-family: "IBM Plex Sans"; font-style: normal; font-weight: 100 700;
|
||
font-display: swap; src: url("/static/ibm-plex-sans.woff2") format("woff2");
|
||
}
|
||
@font-face {
|
||
font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400;
|
||
font-display: swap; src: url("/static/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("/static/ibm-plex-mono-600.woff2") format("woff2");
|
||
}
|
||
|
||
/* Colour tokens. Light values live on :root; the dark media query below
|
||
reassigns the same names rather than re-declaring every rule that uses
|
||
them, so a rule needs !important nowhere in this file — the custom
|
||
property already carries the right value for the active scheme. */
|
||
:root {
|
||
color-scheme: light dark;
|
||
--font-sans: "IBM Plex Sans", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
--bg: #f4f2ed; --fg: #12161c;
|
||
--muted: #6b7684; /* secondary text: .muted, th, timestamps, footers */
|
||
--card-bg: #fff;
|
||
--border: #dedcd7; /* dividers: card, table, app, nav, code, encrypt-fields */
|
||
--control-border: #cbc8c1; /* input, select, textarea, action buttons */
|
||
--input-bg: #fff;
|
||
--code-bg: #efede9;
|
||
--surface-bg: #eae7e0; --surface-bg-hover: #e0dcd3; --surface-open-bg: #d8d3c8;
|
||
--nav-active-bg: #ede4de;
|
||
/* Brick, the mark's own colour, is the panel's accent — but as two values,
|
||
not one. A filled control carries --accent-fill with white on it; a link or
|
||
an active entry carries --accent-text, because the fill on a page
|
||
background is a colour to sit behind text, not to be text. In the light
|
||
scheme the two coincide. */
|
||
--accent-fill: #7a3b2e; --accent-fill-hover: #632f25; --accent-text: #7a3b2e;
|
||
--on-accent: #fff;
|
||
--flash-bg: #edf6ef; --flash-border: #c2e0cc; --flash-fg: #2c6b43;
|
||
--credential-bg: #fcf6e4; --credential-border: #e0c874;
|
||
--danger-fill: #b42318; --danger-fill-hover: #912018;
|
||
--danger-bg: #fbedea; --danger-border: #efccc4; --danger-fg: #b42318; --danger-bg-hover: #f7dfda;
|
||
--st-ok-bg: #edf6ef; --st-ok-fg: #2c6b43; --st-ok-border: #c2e0cc;
|
||
--st-warn-bg: #fbf2e2; --st-warn-fg: #8a5510; --st-warn-border: #ebd5a6;
|
||
--st-error-bg: #fbedea; --st-error-fg: #b42318; --st-error-border: #efccc4;
|
||
--st-unknown-bg: #efede9; --st-unknown-fg: #6b7684; --st-unknown-border: #dedcd7;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--bg: #16181b; --fg: #e9e6e0;
|
||
--muted: #9aa1a9;
|
||
--card-bg: #1d2024;
|
||
--border: #2c2f34;
|
||
--control-border: #3a3e44;
|
||
--input-bg: #14161a;
|
||
--code-bg: #14161a;
|
||
--surface-bg: #23262b; --surface-bg-hover: #2c3036; --surface-open-bg: #343941;
|
||
--nav-active-bg: #2a1f1b;
|
||
/* Brick as drawn is about 2:1 against this background — a link nobody can
|
||
read. The fill lightens just enough to keep white on it above 4.5:1
|
||
(6.9:1 as set), and the text value lightens further to clear the same bar
|
||
as text on the page. This is where the two values earn their keep. */
|
||
--accent-fill: #8e4535; --accent-fill-hover: #a0503e; --accent-text: #ce7b66;
|
||
--flash-bg: #132318; --flash-border: #22452f; --flash-fg: #7fcb9b;
|
||
--credential-bg: #26210d; --credential-border: #5e5013;
|
||
--danger-fill: #9b2c22; --danger-fill-hover: #b0342a;
|
||
--danger-bg: #2a1412; --danger-border: #5e2721; --danger-fg: #eb9b92; --danger-bg-hover: #381a17;
|
||
--st-ok-bg: #132318; --st-ok-fg: #7fcb9b; --st-ok-border: #22452f;
|
||
--st-warn-bg: #2a2109; --st-warn-fg: #e5be72; --st-warn-border: #5e4b12;
|
||
--st-error-bg: #2a1412; --st-error-fg: #eb9b92; --st-error-border: #5e2721;
|
||
--st-unknown-bg: #23262b; --st-unknown-fg: #9aa1a9; --st-unknown-border: #2c2f34;
|
||
}
|
||
}
|
||
* { box-sizing: border-box; }
|
||
/* The scrollbar's width is reserved whether or not the page is long enough to
|
||
need one. Without it a short page (Account) and a long one (Status) are laid
|
||
out in viewports differing by the scrollbar's width, and everything on the
|
||
page — the navigation column included — shifts sideways on the way between
|
||
them. The panel is a set of pages an operator moves through, so that shift
|
||
read as the layout jumping. */
|
||
html { scrollbar-gutter: stable; }
|
||
body {
|
||
font: 400 15px/1.5 var(--font-sans);
|
||
margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--fg);
|
||
}
|
||
/* The two columns — navigation on the left, page on the right — centred as a
|
||
pair. The navigation is a fixed width and the page keeps the measure it had:
|
||
the column is added beside the content, not taken out of it. */
|
||
.shell { display: flex; justify-content: center; align-items: flex-start; gap: 1.75rem; }
|
||
/* One column for every page. It used to be the reading measure, 48rem, which
|
||
the three pages holding data widened to 64rem — and since the shell centres
|
||
the navigation and the page as a pair, that difference moved the navigation
|
||
column and the left edge of every card on the way between two pages. The
|
||
column is now the wider of the two everywhere, so nothing moves; the reading
|
||
measure is kept inside it by the rule below rather than by the column.
|
||
No auto margins: inside the shell they would eat the free space and push the
|
||
navigation column off to the far edge, so the centring is the shell's job
|
||
(justify-content above). */
|
||
main { flex: 1 1 auto; min-width: 0; width: 100%; max-width: 64rem; }
|
||
/* 48rem is a reading measure: right for the forms and prose that make up most
|
||
of the panel, and too narrow for the pages that are tables of data. Every
|
||
page therefore keeps its content at that measure, centred in the column, and
|
||
the pages made of data opt out (main.wide below) and take the column whole.
|
||
The cap is on <main>'s children rather than on .card so a page's heading, its
|
||
back link and the version footer travel with the cards: capping the cards
|
||
alone would leave the heading spanning the column with the card it belongs to
|
||
floating in the middle — the misalignment that gave login and setup their own
|
||
narrow column (main.page-login further down). */
|
||
main > * { max-width: 48rem; margin-left: auto; margin-right: auto; }
|
||
/* The four monitoring pages are the ones that opt out: six columns of send
|
||
log, raw mail.log lines that are long by nature — which at the measure left
|
||
Subject and Status fighting over the last inch and wrapped every second log
|
||
line — and a single delivery's page, which stands two cards side by side over
|
||
a block of those same log lines. The class is declared by the page itself
|
||
(the "wide" block in layout.html) rather than derived from the page name: a
|
||
page's name is the navigation entry it belongs under, which several pages
|
||
share, and how wide a page needs to be is a property of that page alone. */
|
||
main.wide > * { max-width: none; }
|
||
/* Longhands, not the margin shorthand: the shorthand would re-zero the auto
|
||
side margins above and pin the heading to the column's edge, a measure's
|
||
worth to the left of the card under it. */
|
||
/* Light rather than bold, which is the sheet's own setting for the name and
|
||
the one thing a variable font makes free. It is the page's title, not an
|
||
alarm: the size already says which line is the heading, and at 700 it shouted
|
||
over the card under it. */
|
||
h1 { font-size: 1.5rem; font-weight: 300; letter-spacing: -0.01em; margin-top: 0; margin-bottom: 1rem; }
|
||
/* The full mark, on the two pages that have no navigation bar to carry the
|
||
compact one. It takes the column's width so its edges line up with the card
|
||
below it, capped at that column's own 24rem; height stays automatic because
|
||
the stamp's proportions are part of the mark and it is never scaled unevenly.
|
||
The subtitle stops resolving below 280px, which a 320px viewport still clears
|
||
once the body's padding is taken off. */
|
||
.mark { display: block; width: 100%; max-width: 24rem; height: auto; margin-bottom: 1.4rem; }
|
||
/* The radius comes down from 10px across the panel: the mark is a stamp with a
|
||
square edge and a hairline, and a card rounded like a phone widget sat in
|
||
front of it as a different kind of object. 6px on the containers, 5px on the
|
||
controls inside them. */
|
||
.card {
|
||
background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
|
||
padding: 1.5rem; margin: 0 auto;
|
||
}
|
||
.card.narrow { max-width: 24rem; }
|
||
label { display: block; font-weight: 600; margin: 0.9rem 0 0.3rem; }
|
||
input {
|
||
width: 100%; padding: 0.55rem 0.7rem; font-family: inherit; font-size: 1rem;
|
||
border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit;
|
||
}
|
||
/* One vocabulary for actions. Anything that performs an action looks like a
|
||
button: a <button>, or an <a> carrying .btn/.danger where the action is a
|
||
plain navigation (the delete confirmation page, the full queue view).
|
||
Several of these used to render as bold blue text instead — a POST wrapped
|
||
in form.inline, a disclosure toggle, the delete links — which read as links
|
||
and left two appearances for the same kind of control. They all get the
|
||
button look now: filled for a card's own action, and the compact outlined
|
||
variant further down where actions cluster (table rows, the nav bar). Bare
|
||
<a> is left for links that read as part of a sentence or a list. */
|
||
button, a.btn, a.danger {
|
||
display: inline-block; margin-top: 1.2rem; padding: 0.6rem 1.1rem;
|
||
font: inherit; font-size: 1rem; font-weight: 600; text-decoration: none;
|
||
color: var(--on-accent); background: var(--accent-fill); border: 0; border-radius: 5px; cursor: pointer;
|
||
}
|
||
button:hover, a.btn:hover { background: var(--accent-fill-hover); }
|
||
.error { color: var(--danger-fg); margin: 0.6rem 0 0; font-weight: 600; }
|
||
.muted { color: var(--muted); }
|
||
/* Links take the accent. The panel never set a colour here and ran on the
|
||
browser's own blue, which passed unnoticed while the accent was itself blue;
|
||
against brick it left every domain name and back link belonging to a palette
|
||
the panel no longer uses. The controls that are links dressed as buttons
|
||
(a.btn, a.danger, a.st) all carry a class and out-specify this. */
|
||
a { color: var(--accent-text); }
|
||
.topbar { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 1.2rem; }
|
||
.topbar .actions { display: flex; gap: 0.9rem; align-items: baseline; }
|
||
/* form.inline only exists so a POST can sit next to other content without a
|
||
form's block layout; its button is styled like any other. */
|
||
form.inline { display: inline; margin: 0; }
|
||
/* The signed-out pages are a single card and nothing else, and .card.narrow
|
||
centres itself inside whatever holds it — so at the panel's usual width the
|
||
card floated in the middle while the mark and the heading stayed at the far
|
||
left, three alignments on a page with four elements. Narrowing the column to
|
||
the card's own width makes the three line up and puts the block as a whole in
|
||
the middle of the page.
|
||
By page name and not by "the page holds a narrow card", which is what this
|
||
briefly was. Settings and the user form hold one too, and narrowing their
|
||
column moves the navigation sideways with it — the shell centres the column
|
||
and the page as a pair, so 296px of it, measured. Their heading and card are
|
||
still not aligned with each other; that is worth fixing on its own terms and
|
||
not by making every page's chrome move. */
|
||
main.page-login, main.page-setup { max-width: 24rem; }
|
||
.card + .card { margin-top: 1.2rem; }
|
||
.flash { background: var(--flash-bg); border: 1px solid var(--flash-border); color: var(--flash-fg); padding: 0.7rem 1rem; border-radius: 6px; margin-bottom: 1.2rem; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
/* A table column is at least as wide as the longest unbreakable run inside it,
|
||
and the panel's tables are full of runs with nothing to break on: email
|
||
addresses, domains, queue ids. One 40-character recipient was enough to widen
|
||
the send-log past its card and hang Status over the edge. Cells may break mid
|
||
word, so a column can always be squeezed to the width available. */
|
||
th, td { text-align: left; padding: 0.5rem 0.4rem; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
|
||
/* The exceptions: a timestamp broken across two lines is unreadable, and so is
|
||
a send-log status — "deferred" was being hyphenless-broken into "deferre" and
|
||
"d" whenever the row was tight. Both are short enough to never be the reason
|
||
a row does not fit. */
|
||
td.time, td.status { white-space: nowrap; }
|
||
/* Column headings are set in the mono face: they are labels for machine data
|
||
rather than prose, and the wider tracking a small monospaced capital wants
|
||
also holds them apart from the first row of values. Same treatment on the
|
||
other two small all-caps labels in the panel (.sections-title, .fact-label),
|
||
so a heading of that size reads as one thing wherever it appears. */
|
||
th {
|
||
font-family: var(--font-mono); font-size: 0.75rem; font-weight: 500;
|
||
text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
||
}
|
||
/* The controls at the end of a row are short and fixed — Details, Delete — and
|
||
the cell is the last one, so it is the one the automatic layout squeezes
|
||
first: in the send log it was breaking "Details" after "Detail". Holding it
|
||
on one line costs the row nothing and hands the width back to the status
|
||
column, which was wrapping single words for the same reason. */
|
||
td.actions { text-align: right; white-space: nowrap; }
|
||
/* Subject is the one cell whose text we do not control. Breaking mid word (the
|
||
rule above) keeps it inside the card, but a long subject would do it by
|
||
growing the row several lines tall, which buries the rows around it. So the
|
||
subject is clipped to one line instead, with the whole of it in the tooltip.
|
||
The clamp sits on an inner block box rather than the cell because max-width
|
||
on a <td> is only advisory in the automatic table layout. */
|
||
td.subject span {
|
||
display: block; max-width: 18rem;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
.code { display: block; white-space: pre-wrap; word-break: break-all; font-family: var(--font-mono);
|
||
font-size: 0.85rem; background: var(--code-bg); border: 1px solid var(--border); border-radius: 5px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0; }
|
||
/* Semibold, not the browser's bold: a card's title sits a step under the page's
|
||
own heading, and 700 against a 300 h1 read as the louder of the two. */
|
||
h2 { font-size: 1.05rem; font-weight: 600; margin: 0 0 0.4rem; }
|
||
/* The back link is a block so it takes the measure and lines up with the
|
||
heading above it and the card below: auto side margins centre a block, but
|
||
they do nothing for an inline-block, which would have stayed at the column's
|
||
edge while everything around it moved in. Its own text stays at the left of
|
||
that block — only the click target grows to the line, as it does for the
|
||
navigation entries. */
|
||
.back { display: block; margin-bottom: 1rem; }
|
||
/* Build version, closing every authenticated page. Quiet on purpose: it is
|
||
reference material, not something to read on the way past. */
|
||
.version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: var(--muted); line-height: 1.45; }
|
||
.version a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
||
.version a:hover { color: var(--fg); }
|
||
/* The signed-out column is 24rem; a right-aligned multi-link notice wraps
|
||
into a ragged edge, so centre it there. */
|
||
main.page-login .version, main.page-setup .version { text-align: center; }
|
||
select, textarea {
|
||
width: 100%; padding: 0.55rem 0.7rem; font-size: 1rem;
|
||
border: 1px solid var(--control-border); border-radius: 5px; background: var(--input-bg); color: inherit;
|
||
font-family: inherit;
|
||
}
|
||
textarea { resize: vertical; }
|
||
button.danger, a.danger { background: var(--danger-fill); }
|
||
button.danger:hover, a.danger:hover { background: var(--danger-fill-hover); }
|
||
/* The disclosure toggle is an action too, so it is drawn as a button (see the
|
||
compact rule below); it carries no marker because the pressed background
|
||
already shows the open state. */
|
||
.actions > label.toggle { display: inline-block; cursor: pointer; }
|
||
/* Applications are a list of blocks, not table rows. As a table it fell apart:
|
||
four columns of which the last held six controls — two of them <details>
|
||
panels with textareas — never fit the panel's default width. The controls
|
||
wrapped into a staircase, .code on the login cell grew into a slab as tall
|
||
as the row, and the two text cells sat on the baseline halfway down it. One
|
||
block per application gives the identity a line of its own and the
|
||
controls a row of their own, at the width they actually need. */
|
||
.apps { list-style: none; margin: 1.2rem 0 0; padding: 0; }
|
||
.app { padding: 0.9rem 0; border-top: 1px solid var(--border); }
|
||
.app:last-child { padding-bottom: 0; }
|
||
.app-login { margin: 0; font-family: var(--font-mono); font-weight: 600; }
|
||
.app-addr { margin: 0.15rem 0 0; word-break: break-all; }
|
||
.app .actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem; }
|
||
/* Mode and rate limit open under the whole row rather than inside it. As
|
||
<details> the panel sat where its summary was, so opening one cut the row of
|
||
four controls in half and pushed the rest below a block of fields — the
|
||
buttons moved every time a panel opened or closed. The panels are therefore
|
||
the last children of the row, and the checkbox that opens each one is
|
||
visually hidden earlier in the row with its label drawn as the button. The
|
||
checkbox stays in the tab order and keeps its focus ring on the label, so it
|
||
works from the keyboard, and being pure CSS it also works with JavaScript
|
||
blocked, as <details> did. */
|
||
.app .actions > .panel-toggle {
|
||
position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none;
|
||
}
|
||
.app .actions > .panel { display: none; flex: 1 0 100%; }
|
||
.app .actions > .t-mode:checked ~ .panel-mode,
|
||
.app .actions > .t-limit:checked ~ .panel-limit { display: block; }
|
||
.app .actions > .t-mode:checked ~ .for-mode,
|
||
.app .actions > .t-limit:checked ~ .for-limit { background: var(--surface-open-bg); }
|
||
.app .actions > .t-mode:focus-visible ~ .for-mode,
|
||
.app .actions > .t-limit:focus-visible ~ .for-limit { outline: 2px solid var(--accent-fill); outline-offset: 2px; }
|
||
.panel form { margin-top: 0.6rem; }
|
||
/* A panel's own submit is a form button, not one of the controls in the row
|
||
above, so it takes back the spacing the compact .actions rule zeroes out:
|
||
without it Save sits flush against the field it saves and against the
|
||
Remove button under it, reading as one stack of edges. */
|
||
.app .actions > .panel button { margin-top: 0.9rem; }
|
||
/* Where a panel has two of them (save the limit, remove it) they share one row
|
||
under the fields, which the row itself spaces off instead of each button.
|
||
The buttons align on their tops rather than stretching: one of the two is
|
||
wrapped in a form, and that wrapper's margin would otherwise make the row
|
||
taller than a button and stretch the unwrapped one to match it. */
|
||
.app .actions > .panel .panel-buttons {
|
||
display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0.4rem; margin-top: 0.9rem;
|
||
}
|
||
.app .actions > .panel .panel-buttons button,
|
||
.app .actions > .panel .panel-buttons form { margin-top: 0; }
|
||
.credential { border-color: var(--credential-border); background: var(--credential-bg); }
|
||
/* Panel navigation: rendered once from the layout, so it is present on every
|
||
authenticated page without each content template having to include it. */
|
||
/* A column down the left edge rather than a bar across the top. The six page
|
||
entries need about 660px and the session block another 260px, against the
|
||
738px the panel was wide, so as a bar it had to be split over two rows — and
|
||
even then it cost the top of every page. Standing it up removes that: the
|
||
entries share one left edge to scan down, the session sits at the foot where
|
||
it is out of the way, and there is room between them for the current page's
|
||
own sections (.sections below), which is what makes the long pages navigable.
|
||
Sticky, so both lists stay in view while the page scrolls past them. The
|
||
layout template lists the blocks in the order they are drawn, so reading and
|
||
tab order follow the eye without a CSS `order`. */
|
||
.nav {
|
||
position: sticky; top: 2rem; align-self: flex-start;
|
||
flex: none; width: 13.5rem;
|
||
display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;
|
||
/* A viewport shorter than the column would otherwise cut off whatever hangs
|
||
below the fold — with no page scroll left to reach it, since the column is
|
||
stuck to the viewport. */
|
||
max-height: calc(100vh - 4rem); overflow-y: auto;
|
||
}
|
||
.nav .links, .nav .session { display: flex; flex-direction: column; gap: 0.1rem; }
|
||
/* The session is the column's last block and the only one that is not
|
||
navigation, so it is ruled off from the entries above it. */
|
||
.nav .session { padding-top: 0.75rem; border-top: 1px solid var(--border); }
|
||
/* The signed-in administrator's name: a label above the two controls, not one
|
||
of them. It may be long and there is nothing to break it on, so it is allowed
|
||
to break mid word rather than widen the column. */
|
||
.nav .session .muted { padding: 0 0.6rem; font-size: 0.85rem; overflow-wrap: anywhere; }
|
||
.nav .session-user {
|
||
display: flex; align-items: center; gap: 0.5rem;
|
||
}
|
||
/* The mark is a link, but not one of the column's entries: it takes none of the
|
||
padding and rounding the entry rule below applies, so its own edge lines up
|
||
with the entries' icons rather than sitting half a step inside them. */
|
||
.nav .brand { padding: 0; }
|
||
/* The mark takes the column's full width, so its edges line up with the entries
|
||
below it rather than ending halfway across — the same way .mark fills the
|
||
card column on the pages that carry no navigation. width/height are on the
|
||
element too, so the column reserves the right height before the SVG has
|
||
loaded; height stays automatic because the stamp's proportions are part of
|
||
the mark — it is never scaled unevenly. */
|
||
.nav .brand img { display: block; width: 100%; height: auto; }
|
||
/* Each entry pairs an icon with its label, so the entry itself is a flex row
|
||
rather than a run of text — that is also why the entries centre their items
|
||
instead of aligning them on the text baseline. In the column the entry fills
|
||
the width, so the whole strip is the click target and the hover and active
|
||
backgrounds read as one stack of bars. Account is included: it is a page like
|
||
the others and would otherwise be the one bare word in the column. */
|
||
/* No underline: in a row the entries were separated by space alone and the
|
||
underline was what marked them as links, but a column of them reads as a
|
||
list of links already, and eight underlines down the left edge is a lot of
|
||
line for no added meaning. The hover background carries the affordance. */
|
||
/* The entries are page text, not accented links: a column of eight brick rows
|
||
would leave nothing for the current one to stand out from — that is what the
|
||
active entry's colour is for. */
|
||
.nav a, .nav [aria-current] {
|
||
display: flex; align-items: center; gap: 0.5rem;
|
||
padding: 0.35rem 0.6rem; border-radius: 5px; text-decoration: none;
|
||
color: var(--fg);
|
||
}
|
||
.nav a:hover { background: var(--surface-bg); }
|
||
/* The marker moved with the bar: a rule under the entry made sense in a row,
|
||
where the entries sat side by side; standing up, the current entry is marked
|
||
down its leading edge, which is the edge every entry shares. */
|
||
.nav [aria-current] {
|
||
font-weight: 600; color: var(--accent-text); background: var(--nav-active-bg);
|
||
box-shadow: inset 2px 0 0 var(--accent-fill);
|
||
}
|
||
/* Sign out carries an icon too, so it needs the same row layout; its colours
|
||
come from the compact button rule further down, and its type is put back to
|
||
the column's beside it (see the rule after that one). */
|
||
.nav button { display: flex; align-items: center; gap: 0.5rem; }
|
||
/* The icons draw in the entry's own colour, so the active entry's darker text,
|
||
a link's blue and Sign out's red all carry through without a rule apiece. */
|
||
.nav .icon { width: 1rem; height: 1rem; flex: none; }
|
||
/* The current page's own sections, listed under the page entries. The domain
|
||
page and the status page are eight and nine cards tall, and the only way to
|
||
the card you came for was to scroll past all the ones you did not. Only pages
|
||
that long carry an index — it comes from the page's own "sections" template
|
||
(see layout.html), so a page with two cards renders nothing here. */
|
||
.sections {
|
||
display: flex; flex-direction: column; gap: 0.1rem;
|
||
padding-top: 0.75rem; border-top: 1px solid var(--border);
|
||
}
|
||
.sections-title {
|
||
margin: 0 0 0.25rem 0.6rem;
|
||
font-family: var(--font-mono);
|
||
font-size: 0.7rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.1em;
|
||
color: var(--muted);
|
||
}
|
||
/* Quieter and a step in from the page entries: this is an index of one page,
|
||
subordinate to the list of pages above it. */
|
||
.nav .sections a {
|
||
padding: 0.2rem 0.6rem 0.2rem 0.9rem;
|
||
font-size: 0.85rem; color: var(--muted); text-decoration: none;
|
||
}
|
||
.nav .sections a:hover { color: var(--fg); }
|
||
/* panel.js marks the section the page is scrolled to. Without JavaScript
|
||
nothing is marked and the list is still a working index. */
|
||
.nav .sections a.current { color: var(--fg); font-weight: 600; background: var(--nav-active-bg); }
|
||
/* Jumping to a card should not leave it touching the top edge of the window. */
|
||
.card[id] { scroll-margin-top: 1rem; }
|
||
/* Below the width the two columns need (13.5rem of navigation, 1.75rem of gap
|
||
and the 48rem measure, plus the body's padding — the measure, not the column
|
||
it sits in: a page column short of its 64rem is only a page column that has
|
||
not reached its cap, while a measure that no longer fits is a page that has),
|
||
the column lies back down
|
||
into a bar above the page — the same wrapping rows it used to be. A drawer
|
||
behind a hamburger would save more height, but it would need script to open,
|
||
and the panel's navigation is six entries: they fit. */
|
||
@media (max-width: 66rem) {
|
||
.shell { flex-direction: column; align-items: stretch; gap: 1.2rem; }
|
||
main { margin: 0 auto; }
|
||
.nav {
|
||
position: static; width: auto; max-height: none; overflow-y: visible;
|
||
flex-direction: row; flex-wrap: wrap; align-items: center; gap: 0.4rem 1.2rem;
|
||
padding-bottom: 0.6rem; border-bottom: 1px solid var(--border);
|
||
}
|
||
/* Lying down there is no column for the mark to span, and at its full width
|
||
it would take a row to itself above the entries, so it goes back to the
|
||
compact size it had — enough to read beside them. */
|
||
.nav .brand img { width: 110px; }
|
||
/* Each block keeps its own group of entries together and wraps as one; the
|
||
rules that separated the blocks vertically become the space between them. */
|
||
.nav .links, .nav .session, .nav .sections {
|
||
flex-direction: row; flex-wrap: wrap; align-items: center;
|
||
gap: 0.2rem 0.6rem; padding-top: 0; border-top: 0;
|
||
}
|
||
.sections-title { margin: 0; }
|
||
}
|
||
/* Status badges: one vocabulary (ok/warn/error/unknown) shared by the server
|
||
status page and the per-domain DNS checks, so a colour means the same thing
|
||
everywhere. The class suffix is the check's own status value. */
|
||
/* Mono and squared off rather than a bold uppercase pill: the badge carries a
|
||
value the server produced (ok, warn, delivered, bounced), and the mono face
|
||
is where the panel puts machine output everywhere else. Uppercasing it was
|
||
the pill's own emphasis — dropped with the pill, since the colour already
|
||
says how loud the badge is. */
|
||
/* line-height 1 and the padding doing the centring, rather than the 1.5 the
|
||
badge would inherit from the body. In a tall line box a word of lowercase
|
||
with no descender — ok, warn, unknown — sits above the optical centre of the
|
||
space around it, and inside a bordered box that reads as text stuck to the
|
||
top. With the box hugging the line, the padding is what centres the word, and
|
||
it is uneven on purpose: the extra tenth at the top pays for the descender
|
||
space every line box reserves below the baseline and these words rarely use.
|
||
The one that does use it, queued, keeps the same box — the tail hangs into
|
||
the bottom padding rather than growing the badge. */
|
||
.st {
|
||
display: inline-block; padding: 0.28rem 0.45rem 0.2rem; border-radius: 4px;
|
||
font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500; line-height: 1;
|
||
letter-spacing: 0.02em;
|
||
vertical-align: middle; border: 1px solid transparent;
|
||
}
|
||
.st-ok { background: var(--st-ok-bg); color: var(--st-ok-fg); border-color: var(--st-ok-border); }
|
||
.st-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
|
||
.st-error { background: var(--st-error-bg); color: var(--st-error-fg); border-color: var(--st-error-border); }
|
||
.st-unknown { background: var(--st-unknown-bg); color: var(--st-unknown-fg); border-color: var(--st-unknown-border); }
|
||
/* In the domain list the badge is also the link to that domain's DNS section,
|
||
so it must not pick up the link colour and underline that would fight with
|
||
the badge's own palette. */
|
||
a.st, a.st:hover { color: inherit; text-decoration: none; }
|
||
a.st:hover { filter: brightness(1.08); }
|
||
/* Usage bars on the status page's machine card. <meter> rather than a div sized
|
||
from the reading, because the CSP forbids inline styles (see the note at the
|
||
top of this file) and a bar's length has to travel on an attribute. The
|
||
element grades itself from low/high/optimum, so the colour matches the
|
||
badges' meaning without this file restating the thresholds — and a browser
|
||
that does not render meters falls back to the percentage beside it, which is
|
||
printed either way. */
|
||
meter { width: 5rem; height: 0.7rem; vertical-align: middle; margin-right: 0.4rem; }
|
||
/* The card's own two narrow columns. Cells may break mid word by default (see
|
||
the th, td rule above), which the detail column needs and these two must not
|
||
have: the resource names and the readings are short, and the long detail
|
||
beside them would otherwise win the width and leave "Memory" broken across
|
||
two lines. */
|
||
.metric { white-space: nowrap; }
|
||
.code-row { display: flex; align-items: flex-start; gap: 0.5rem; }
|
||
.code-row .code { flex: 1; min-width: 0; }
|
||
|
||
/* Two cards abreast, on the one page that has a pair of them worth reading
|
||
together (a delivery's facts beside its history). auto-fit with a minimum
|
||
rather than a media query: the pair falls into one column exactly when a
|
||
column would drop below the width its content needs, which is also what
|
||
happens inside the narrow column the navigation lies down at. */
|
||
.split {
|
||
display: grid; grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
|
||
gap: 1.2rem; margin-top: 1.2rem;
|
||
}
|
||
/* The gap already spaces the cards inside the grid; the general .card + .card
|
||
rule would add a second gap's worth on top of it, and only to the second
|
||
card, which in one column reads as an uneven stack. The blocks around the
|
||
grid keep their spacing from it instead. */
|
||
.split > .card + .card { margin-top: 0; }
|
||
.split + .card { margin-top: 1.2rem; }
|
||
/* The subject heads a delivery's page, and it is the one heading in the panel
|
||
whose text we do not control: it may be a hundred characters with nothing to
|
||
break on. It wraps to as many lines as it needs (this is the page's name, not
|
||
a table cell to clip) but never widens the column. */
|
||
h1.subject { overflow-wrap: anywhere; }
|
||
/* The line under it: sender, recipient and outcome. Wrapping rather than
|
||
clipping, since either address may be long and both are the point. The
|
||
vertical margins are longhands for the reason given at the h1 rule above —
|
||
the shorthand would re-zero the auto side margins that keep a page's blocks
|
||
centred on the measure. It sits close under the heading: the two are one
|
||
statement of what this page is about. */
|
||
.route {
|
||
display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem;
|
||
margin-top: -0.5rem; margin-bottom: 1rem;
|
||
}
|
||
.route .addr { font-family: var(--font-mono); font-size: 0.9rem; overflow-wrap: anywhere; }
|
||
.route .arrow { color: var(--muted); }
|
||
/* A card's short facts, as a grid of tiles rather than the label-over-value
|
||
stack the rest of the panel uses. That stack is right for the pages where a
|
||
value is a DNS record or a password to copy — one per line, full width — and
|
||
wrong for six values of a few characters each, which down a narrow column
|
||
would be a page of mostly empty rows. Same auto-fit as .split: as many
|
||
columns as fit, and one when nothing else does. */
|
||
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.5rem; margin-top: 1rem; }
|
||
.fact { min-width: 0; padding: 0.5rem 0.7rem; border-radius: 6px; background: var(--surface-bg); }
|
||
.fact-label {
|
||
display: block; font-family: var(--font-mono); font-size: 0.7rem; font-weight: 500;
|
||
text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
|
||
}
|
||
/* Nothing here is guaranteed to have a space in it — domains, logins and queue
|
||
ids are all one run — so a value may break mid word rather than push its tile
|
||
wider than the column it shares. */
|
||
.fact-value { display: block; margin-top: 0.1rem; overflow-wrap: anywhere; }
|
||
.fact-value.mono { font-family: var(--font-mono); font-size: 0.85rem; }
|
||
/* A message's history: the steps down a line, each with the time it happened,
|
||
the status it reached and what that means. The line is the list's own left
|
||
border and the dots sit on top of it, so nothing has to be positioned against
|
||
a fixed row height — a step's text is two lines or five depending on what
|
||
there is to say about it. */
|
||
.timeline { list-style: none; display: flex; flex-direction: column; gap: 1.1rem;
|
||
margin: 1rem 0 0; padding: 0.2rem 0 0.2rem 1.4rem; border-left: 2px solid var(--border); }
|
||
.event { position: relative; min-width: 0; }
|
||
.event::before {
|
||
content: ""; position: absolute; left: -1.85rem; top: 0.3rem;
|
||
width: 0.65rem; height: 0.65rem; border-radius: 50%;
|
||
background: var(--card-bg); border: 2px solid var(--control-border);
|
||
}
|
||
/* The dot carries the step's own verdict, in the same four colours as the badge
|
||
beside it — so the shape of the history is readable down the line alone. */
|
||
.event.lvl-ok::before { border-color: var(--st-ok-fg); background: var(--st-ok-bg); }
|
||
.event.lvl-warn::before { border-color: var(--st-warn-fg); background: var(--st-warn-bg); }
|
||
.event.lvl-error::before { border-color: var(--st-error-fg); background: var(--st-error-bg); }
|
||
/* The step that has not happened yet — a queued message's delivery report — is
|
||
the one hollow dot, and its text is quiet: it is what to expect, not a record
|
||
of anything. */
|
||
.event.pending { opacity: 0.7; }
|
||
.event.pending::before { background: var(--card-bg); border-style: dashed; }
|
||
.event-time { margin: 0; font-family: var(--font-mono); font-size: 0.75rem; color: var(--muted); }
|
||
.event-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0.15rem 0 0; font-weight: 600; }
|
||
.event-detail { margin: 0.2rem 0 0; font-size: 0.9rem; }
|
||
/* A message's own mail.log lines, as a table of when and what. The time column
|
||
is sized to its content — width:1% is as narrow as the automatic layout will
|
||
take a column, which with the nowrap on td.time settles on exactly the stamp
|
||
— so everything left over goes to the text, which is the column that needs
|
||
it. */
|
||
table.log { margin-top: 1rem; }
|
||
table.log th:first-child, table.log td.time { width: 1%; }
|
||
/* The log's own text: monospace, because it is full of addresses, response
|
||
codes and identifiers that are compared character by character rather than
|
||
read as words. pre-wrap keeps the spacing a server's reply was written with
|
||
while still letting a long line wrap inside the cell instead of widening it
|
||
(the th/td rule above allows the break). */
|
||
table.log td.log-text {
|
||
font-family: var(--font-mono);
|
||
font-size: 0.8rem; white-space: pre-wrap;
|
||
}
|
||
/* Compact outlined button: same affordance as the filled one but quiet enough
|
||
that several can sit together without shouting — the Copy buttons beside a
|
||
value, the controls of a table row or of an application block. Sign out
|
||
overrides this with .danger below since signing out is a deliberate,
|
||
singular action. .actions is the shared hook: a cell that holds controls, or
|
||
the control row of an application. */
|
||
button.copy, .actions button, .actions > label.toggle, .actions a.danger, .nav button {
|
||
margin: 0; padding: 0.45rem 0.7rem; font-size: 0.8rem; font-weight: 600;
|
||
border-radius: 5px; white-space: nowrap;
|
||
background: var(--surface-bg); color: var(--accent-text); border: 1px solid var(--control-border);
|
||
}
|
||
button.copy:hover, .actions button:hover, .actions > label.toggle:hover,
|
||
.actions a.danger:hover, .nav button:hover { background: var(--surface-bg-hover); }
|
||
button.copy { flex: none; margin-top: 0.3rem; }
|
||
.actions button.danger, .actions a.danger, .nav button.danger {
|
||
color: var(--danger-fg); background: var(--danger-bg); border-color: var(--danger-border);
|
||
}
|
||
.actions button.danger:hover, .actions a.danger:hover, .nav button.danger:hover { background: var(--danger-bg-hover); }
|
||
/* Sign out takes the compact button's colours but not its type. The rule above
|
||
is written for controls that cluster — a row of them beside a value or in a
|
||
table cell — where 0.8rem/600 is what keeps four of them from shouting. In
|
||
the navigation column it has no cluster to belong to: it stands under
|
||
Settings, among entries set 0.95rem/400, and being smaller and heavier than
|
||
every word around it made it read as a different kind of object rather than
|
||
as the last item of the list. Type and padding go back to the entries';
|
||
the red, the border and the background stay, and are what say it acts. */
|
||
.nav button {
|
||
padding: 0.35rem 0.6rem; font-size: 0.95rem; font-weight: 400;
|
||
}
|
||
|
||
/* The optional "encrypt this download" block on the backup and export forms.
|
||
Its label is the one checkbox in the panel, so it opts out of the
|
||
block-level label rule above and sits on one line with its box; the fields it
|
||
reveals are indented under it to read as its consequence rather than as three
|
||
more fields of the form. panel.js hides the inner block until the box is
|
||
ticked (and empties it when unticked); without JavaScript everything stays
|
||
visible, which the server handles identically. The import form reuses the
|
||
same indented .encrypt-fields look for its password field, but reveals it
|
||
by file extension instead of a checkbox (see panel.js). */
|
||
.encrypt { margin-top: 1.2rem; }
|
||
.encrypt label.check {
|
||
display: flex; align-items: center; gap: 0.5rem; margin: 0; font-weight: 600;
|
||
}
|
||
.encrypt label.check input { width: auto; margin: 0; }
|
||
.encrypt-fields {
|
||
margin-top: 1.2rem; margin-left: 1.6rem; padding-left: 0.9rem;
|
||
border-left: 2px solid var(--border);
|
||
}
|
||
.encrypt .encrypt-fields { margin-top: 0; }
|
||
.encrypt-fields label { margin-top: 0.7rem; }
|
||
.encrypt-fields .muted { margin: 0.5rem 0 0; font-size: 0.85rem; }
|