c0c66dfcdb
The navigation was a bar across the top that did not fit on one row — six page entries and the session block against the panel's width — and had to be split into two, costing the top of every page. It is now a column down the left edge: one left edge to scan, the current entry marked down its leading edge, sticky so it stays in view, and room under the entries for the current page's own sections. Below the width the two columns need it lies back down into the same wrapping rows as before; six entries need no drawer. The section index is for the two pages long enough to need one — the domain page (nine cards) and the status page (eight). Each card carries an id and the page's template defines the list by overriding an empty "sections" block in the layout, so a page that defines nothing renders no index. panel.js marks the section in view, looking targets up by id on each pass so the status page swapping its cards out every five seconds cannot leave it measuring boxes that have left the document; the links themselves are plain fragment links and need no script. Verified against the real pages rendered by a local panel at 1300px, 924px and 481px wide.
355 lines
21 KiB
CSS
355 lines
21 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; }
|
|
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; }
|
|
/* 48rem is a reading measure: right for the forms and prose that make up most
|
|
of the panel. Page-specific overrides below widen or narrow it for the pages
|
|
that need something else. 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: 48rem; }
|
|
h1 { font-size: 1.4rem; margin: 0 0 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 <details> 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 three monitoring pages hold data instead of prose — seven columns of
|
|
send-log, and raw mail.log lines that are long by nature — and at the
|
|
default measure the send-log's Subject and Status were fighting over the
|
|
last inch while the log pages wrapped every second line. They get a wider
|
|
measure; every other page keeps the default one. The class comes from the
|
|
layout, which stamps the page name onto <main>. */
|
|
main.page-deliveries, main.page-mail_queue, main.page-system_log { max-width: 64rem; }
|
|
/* 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; }
|
|
.back { display: inline-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; }
|
|
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); the marker is dropped because the pressed background
|
|
already shows the open state. */
|
|
.actions summary { display: inline-block; list-style: none; cursor: pointer; }
|
|
.actions summary::-webkit-details-marker { display: none; }
|
|
/* 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; }
|
|
/* An open panel claims a row to itself: its textareas and number inputs want
|
|
the block's full width, not the width of the summary that opened them. */
|
|
.app .actions > details[open] { flex: 1 0 100%; }
|
|
details form { margin-top: 0.6rem; }
|
|
.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; }
|
|
/* width/height are on the element too, so the row reserves the space before the
|
|
SVG has loaded; these keep the ratio if the box is ever squeezed. The stamp's
|
|
proportions are part of the mark — it is never scaled unevenly. */
|
|
.nav .brand img { display: block; width: 110px; 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 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);
|
|
}
|
|
/* 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); }
|
|
/* 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; }
|
|
/* 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 summary, .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 summary:hover,
|
|
.actions a.danger:hover, .nav button:hover { background: var(--surface-bg-hover); }
|
|
button.copy { flex: none; margin-top: 0.3rem; }
|
|
.actions details[open] > summary { background: var(--surface-open-bg); }
|
|
.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, export and import
|
|
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. */
|
|
.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-left: 1.6rem; padding-left: 0.9rem; border-left: 2px solid var(--border);
|
|
}
|
|
.encrypt-fields label { margin-top: 0.7rem; }
|
|
.encrypt-fields .muted { margin: 0.5rem 0 0; font-size: 0.85rem; }
|