diff --git a/news/templates/_nav.html b/news/templates/_nav.html
index 6f0304e..037a0ee 100644
--- a/news/templates/_nav.html
+++ b/news/templates/_nav.html
@@ -4,4 +4,29 @@
DigestTuneConfig
+
+
diff --git a/news/templates/_theme.html b/news/templates/_theme.html
new file mode 100644
index 0000000..14ecd19
--- /dev/null
+++ b/news/templates/_theme.html
@@ -0,0 +1,44 @@
+{#- Shared palette, theme toggle and nav styling.
+
+ Three states, not two: system (follow the OS), light, dark. The dark rules
+ are guarded with :not([data-theme="light"]) so an explicit light choice wins
+ over a dark OS setting -- without that guard the toggle only works one way.
+-#}
+
+
diff --git a/news/templates/config.html b/news/templates/config.html
index 700babe..1e62cf4 100644
--- a/news/templates/config.html
+++ b/news/templates/config.html
@@ -2,15 +2,8 @@
Config
+{% include "_theme.html" %}
diff --git a/news/templates/digest.html b/news/templates/digest.html
index 2b9f29f..b0910b1 100644
--- a/news/templates/digest.html
+++ b/news/templates/digest.html
@@ -2,12 +2,8 @@
Briefing — {{ day }}
+{% include "_theme.html" %}
diff --git a/news/templates/triage.html b/news/templates/triage.html
index 9c427f3..4c23cf4 100644
--- a/news/templates/triage.html
+++ b/news/templates/triage.html
@@ -2,15 +2,8 @@
Triage
+{% include "_theme.html" %}
diff --git a/news/templates/tune.html b/news/templates/tune.html
index 171d4d7..7cc6920 100644
--- a/news/templates/tune.html
+++ b/news/templates/tune.html
@@ -2,13 +2,8 @@
Tune
+{% include "_theme.html" %}
diff --git a/tests/test_delivery.py b/tests/test_delivery.py
index d4e4148..8228fb7 100644
--- a/tests/test_delivery.py
+++ b/tests/test_delivery.py
@@ -243,3 +243,27 @@ def test_every_page_carries_the_nav(client, path, active):
for target in ("/tune", "/config", "/digest"):
assert f'href="{target}"' in html, f"{path} is missing a link to {target}"
assert f'class="here">{active}<' in html, f"{path} does not mark itself active"
+
+
+@pytest.mark.parametrize("path", ["/", "/tune", "/config", "/digest"])
+def test_every_page_has_a_working_theme_toggle(client, path):
+ """Three states (system/light/dark), no flash, and the guard that makes the
+ light choice win over a dark OS setting."""
+ c, _ = client
+ html = c.get(path).text
+ assert 'id="theme"' in html, "theme button missing"
+ assert '[data-theme="dark"]' in html, "explicit dark rules missing"
+ assert ':root:not([data-theme="light"])' in html, (
+ "without this guard, choosing light does nothing under a dark OS theme")
+ # The theme must be applied before first paint, so its script has to appear
+ # ahead of any rendered content.
+ assert html.index("localStorage.getItem('theme')") < html.index("