155b721438
test / test (push) Has been cancelled
Lay out view, auth, validate, and handlers under internal/web while keeping the cmd/panel API unchanged; update roadmap and changelog for web-split closure. Co-authored-by: Cursor <cursoragent@cursor.com>
532 lines
32 KiB
CSS
532 lines
32 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. */
|
|
|
|
/* 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;
|
|
--bg: #f6f7f9; --fg: #1b1f24;
|
|
--card-bg: #fff;
|
|
--border: #e2e5e9; /* dividers: card, table, app, nav, code, encrypt-fields */
|
|
--control-border: #cfd4da; /* input, select, textarea, action buttons */
|
|
--input-bg: #fff;
|
|
--code-bg: #f0f2f4;
|
|
--surface-bg: #eef1f5; --surface-bg-hover: #e2e7ee; --surface-open-bg: #dde3ec;
|
|
--nav-active-bg: #e6ebf5;
|
|
--flash-bg: #ecfdf3; --flash-border: #abefc6; --flash-fg: #067647;
|
|
--credential-bg: #fffbeb; --credential-border: #f5c518;
|
|
--danger-bg: #fef3f2; --danger-border: #fecdca; --danger-fg: #b42318; --danger-bg-hover: #fee4e2;
|
|
--st-ok-bg: #ecfdf3; --st-ok-fg: #067647; --st-ok-border: #abefc6;
|
|
--st-warn-bg: #fffaeb; --st-warn-fg: #b54708; --st-warn-border: #fedf89;
|
|
--st-error-bg: #fef3f2; --st-error-fg: #b42318; --st-error-border: #fecdca;
|
|
--st-unknown-bg: #f0f2f4; --st-unknown-fg: #6b7280; --st-unknown-border: #e2e5e9;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--bg: #14171a; --fg: #e6e8eb;
|
|
--card-bg: #1d2125;
|
|
--border: #2b3138;
|
|
--control-border: #2b3138;
|
|
--input-bg: #14171a;
|
|
--code-bg: #14171a;
|
|
--surface-bg: #22262b; --surface-bg-hover: #2b3138; --surface-open-bg: #313841;
|
|
--nav-active-bg: #22303f;
|
|
--flash-bg: #0d2818; --flash-border: #1a5336; --flash-fg: #75d99b;
|
|
--credential-bg: #2a2408; --credential-border: #6b5a10;
|
|
--danger-bg: #2d1211; --danger-border: #6b201a; --danger-fg: #f5a29b; --danger-bg-hover: #3d1a18;
|
|
--st-ok-bg: #0d2818; --st-ok-fg: #75d99b; --st-ok-border: #1a5336;
|
|
--st-warn-bg: #2e2308; --st-warn-fg: #f5c86b; --st-warn-border: #6b5210;
|
|
--st-error-bg: #2d1211; --st-error-fg: #f5a29b; --st-error-border: #6b201a;
|
|
--st-unknown-bg: #22262b; --st-unknown-fg: #9aa3ad; --st-unknown-border: #2b3138;
|
|
}
|
|
}
|
|
* { 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: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
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. */
|
|
h1 { font-size: 1.4rem; 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; }
|
|
.card {
|
|
background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px;
|
|
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-size: 1rem;
|
|
border: 1px solid var(--control-border); border-radius: 6px; 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: #fff; background: #2563eb; border: 0; border-radius: 6px; cursor: pointer;
|
|
}
|
|
button:hover, a.btn:hover { background: #1d4ed8; }
|
|
.error { color: #b42318; margin: 0.6rem 0 0; font-weight: 600; }
|
|
.muted { color: #6b7280; }
|
|
.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. */
|
|
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: 8px; 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 exception: a timestamp broken across two lines is unreadable, and it is
|
|
short enough to never be the reason a row does not fit. */
|
|
td.time { white-space: nowrap; }
|
|
th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; color: #6b7280; }
|
|
td.actions { text-align: right; }
|
|
/* 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: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.85rem; background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0; }
|
|
h2 { font-size: 1.05rem; 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: #6b7280; 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: 6px; background: var(--input-bg); color: inherit;
|
|
font-family: inherit;
|
|
}
|
|
textarea { resize: vertical; }
|
|
button.danger, a.danger { background: #b42318; }
|
|
button.danger:hover, a.danger:hover { background: #912018; }
|
|
/* 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: ui-monospace, SFMono-Regular, Menlo, monospace; 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 #2563eb; 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; }
|
|
/* 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. */
|
|
.nav a, .nav [aria-current] {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
padding: 0.35rem 0.6rem; border-radius: 6px; text-decoration: none;
|
|
}
|
|
.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(--fg); background: var(--nav-active-bg); box-shadow: inset 2px 0 0 #2563eb;
|
|
}
|
|
/* Sign out carries an icon too, so it needs the same row layout; its padding
|
|
and colours come from the compact button rule further down. */
|
|
.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-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
|
|
color: #6b7280;
|
|
}
|
|
/* 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: #6b7280; 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. */
|
|
.st {
|
|
display: inline-block; padding: 0.05rem 0.45rem; border-radius: 999px;
|
|
font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
|
|
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: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9rem; overflow-wrap: anywhere; }
|
|
.route .arrow { color: #6b7280; }
|
|
/* 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: 8px; background: var(--surface-bg); }
|
|
.fact-label {
|
|
display: block; font-size: 0.7rem; font-weight: 700;
|
|
text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280;
|
|
}
|
|
/* 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: ui-monospace, SFMono-Regular, Menlo, monospace; 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-size: 0.75rem; color: #6b7280; }
|
|
.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: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
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: 6px; white-space: nowrap;
|
|
background: var(--surface-bg); color: #2563eb; 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); }
|
|
|
|
/* 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; }
|