diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a82d1c..d1f9833 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
`/mail-queue` (was `/queue`), System log at `/system-log` (was `/logtail`).
Bookmarks to the old paths stop working.
+- panel: each entry in the navigation bar now carries an icon beside its label,
+ so the bar is scannable at a glance instead of a row of similar-length words.
+ The icons are inline SVG drawn in the entry's own colour — no extra request,
+ no exemption from the panel's Content-Security-Policy — and are hidden from
+ screen readers, which still announce the label alone.
+
## [0.2.0] - 2026-08-03
- panel: every authenticated page now ends with the running version
diff --git a/internal/web/static/panel.css b/internal/web/static/panel.css
index e1c4b8d..123964a 100644
--- a/internal/web/static/panel.css
+++ b/internal/web/static/panel.css
@@ -89,19 +89,29 @@ details form { margin-top: 0.6rem; }
/* Panel navigation: rendered once from the layout, so it is present on every
authenticated page without each content template having to include it. */
.nav {
- display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
+ display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center;
gap: 0.4rem 1rem; margin-bottom: 1.2rem; padding-bottom: 0.6rem;
border-bottom: 1px solid #e2e5e9;
}
@media (prefers-color-scheme: dark) { .nav { border-color: #2b3138 !important; } }
-.nav .links, .nav .session { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; align-items: baseline; }
-.nav .links a, .nav .links [aria-current] { padding: 0.2rem 0.5rem; border-radius: 6px; }
-.nav .links [aria-current] {
+.nav .links, .nav .session { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; align-items: center; }
+/* 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 bar centres its items
+ instead of aligning them on the text baseline. Account is included: it is a
+ page like the others and would otherwise be the one bare word in the bar. */
+.nav a, .nav [aria-current] {
+ display: inline-flex; align-items: center; gap: 0.4rem;
+ padding: 0.2rem 0.5rem; border-radius: 6px;
+}
+.nav [aria-current] {
font-weight: 600; color: #1b1f24; background: #e6ebf5; box-shadow: inset 0 -2px 0 #2563eb;
}
@media (prefers-color-scheme: dark) {
- .nav .links [aria-current] { color: #e6e8eb !important; background: #22303f !important; }
+ .nav [aria-current] { color: #e6e8eb !important; background: #22303f !important; }
}
+/* The icons draw in the entry's own colour, so the active entry's darker text
+ and a link's blue carry through without a second rule per state. */
+.nav .icon { width: 1rem; height: 1rem; flex: none; }
/* 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. */
diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html
index 808a40f..da2d362 100644
--- a/internal/web/templates/layout.html
+++ b/internal/web/templates/layout.html
@@ -36,19 +36,33 @@
{{define "nav"}}
{{end}}
+
+{{/* Navigation icons. Inline SVG rather than an icon font or sprite file: they
+ inherit the link's colour through currentColor, cost no extra request, and
+ need no exemption from the panel's "default-src 'self'" policy. Each is
+ aria-hidden because the entry's own text is already the accessible name;
+ the icon is a landmark for the eye, not a second label. Kept as separate
+ templates so the nav above stays one readable line per page. */}}
+{{define "icon-status"}}{{end}}
+{{define "icon-domains"}}{{end}}
+{{define "icon-deliveries"}}{{end}}
+{{define "icon-mail-queue"}}{{end}}
+{{define "icon-system-log"}}{{end}}
+{{define "icon-backup"}}{{end}}
+{{define "icon-account"}}{{end}}
diff --git a/internal/web/templates_test.go b/internal/web/templates_test.go
index 7db70c0..0d1083e 100644
--- a/internal/web/templates_test.go
+++ b/internal/web/templates_test.go
@@ -105,7 +105,9 @@ func TestNavMarksActivePage(t *testing.T) {
t.Fatalf("execute nav: %v", err)
}
out := buf.String()
- if !strings.Contains(out, `Mail queue`) {
+ // The label is checked apart from the opening tag because each entry now
+ // carries an icon between the two.
+ if !strings.Contains(out, ``) || !strings.Contains(out, `Mail queue`) {
t.Errorf("active page is not marked:\n%s", out)
}
if strings.Contains(out, `href="/mail-queue"`) {
@@ -129,7 +131,7 @@ func TestNavLeadsWithStatusAndPointsDomainsAtItsOwnPath(t *testing.T) {
t.Fatalf("execute nav: %v", err)
}
out := buf.String()
- if !strings.Contains(out, `Status`) {
+ if !strings.Contains(out, ``) || !strings.Contains(out, `Status`) {
t.Errorf("the status page is not marked active:\n%s", out)
}
if !strings.Contains(out, `href="/domains"`) {