From 89632e99376d1def791813aab141ac70b584cfe4 Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Fri, 22 May 2026 21:51:23 +0100 Subject: [PATCH] ui: light theme by default (dark is opt-in) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swaps the role of `:root` (now light) and the data-theme attribute (now `[data-theme="dark"]`) in cassandra.css, flips the localStorage fallback from 'dark' to 'light' in base/login/verify templates, and updates the theme-toggle label and the branding-consistency test selectors to match. Existing users with cassandra.theme=dark in localStorage still see dark — their explicit preference wins. Only first-time visitors and users with no stored preference shift to light. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/branding.py | 9 +++---- app/static/css/cassandra.css | 38 +++++++++++++++--------------- app/templates/base.html | 4 ++-- app/templates/login.html | 4 ++-- app/templates/verify.html | 4 ++-- tests/test_branding_consistency.py | 22 +++++++++-------- 6 files changed, 42 insertions(+), 39 deletions(-) diff --git a/app/branding.py b/app/branding.py index 9769d2f..8f8bd17 100644 --- a/app/branding.py +++ b/app/branding.py @@ -16,10 +16,11 @@ detection test (`tests/test_branding_consistency.py`) parses `cassandra.css` and asserts every variable matches. Update both or neither. -The light theme is the *default* in emails — mail clients can't read -`localStorage`, so we can't replicate the dashboard's user-toggled -theme. Clients that honour `prefers-color-scheme` get the dark palette -via media query. +The light theme is the *default* everywhere — dashboard `:root` block, +auth pages, and emails. Dark is opt-in via the in-app toggle (which +sets `data-theme="dark"` on `` and persists in `localStorage`). +Mail clients that honour `prefers-color-scheme: dark` get the dark +palette via media query. """ from __future__ import annotations diff --git a/app/static/css/cassandra.css b/app/static/css/cassandra.css index 2cbeddf..391be28 100644 --- a/app/static/css/cassandra.css +++ b/app/static/css/cassandra.css @@ -2,23 +2,7 @@ * Mono for data, headers, terminal feel; sans for prose surfaces (log + chat). */ :root { - /* Dark theme (default) */ - --bg: #0a0e14; - --surface: #11151c; - --surface-2: #161b25; - --border: #2a3142; - --text: #d4dae8; /* lifted from #c0caf5 for readability */ - --muted: #8189a1; /* lifted from #565f89 — was unreadably dim */ - --dim: #565f89; - --accent: #00d9ff; - --positive: #50fa7b; - --negative: #ff5b5b; - --alert: #ff8a4a; - --warning: #f1fa8c; - --user-bubble-bg: rgba(0, 217, 255, 0.08); -} - -[data-theme="light"] { + /* Light theme (default) */ --bg: #f5f3ec; /* warm off-white, easier on the eyes than pure white */ --surface: #ffffff; --surface-2: #efece3; @@ -34,6 +18,22 @@ --user-bubble-bg: rgba(14, 116, 144, 0.07); } +[data-theme="dark"] { + --bg: #0a0e14; + --surface: #11151c; + --surface-2: #161b25; + --border: #2a3142; + --text: #d4dae8; /* lifted from #c0caf5 for readability */ + --muted: #8189a1; /* lifted from #565f89 — was unreadably dim */ + --dim: #565f89; + --accent: #00d9ff; + --positive: #50fa7b; + --negative: #ff5b5b; + --alert: #ff8a4a; + --warning: #f1fa8c; + --user-bubble-bg: rgba(0, 217, 255, 0.08); +} + /* Font stacks. Mono for terminal feel; sans for reading. */ :root { --font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', ui-monospace, monospace; @@ -104,8 +104,8 @@ a:hover { text-decoration: underline; } text-transform: lowercase; } .theme-toggle:hover { color: var(--accent); border-color: var(--accent); } -.theme-toggle__label::before { content: "◐ dark"; } -[data-theme="light"] .theme-toggle__label::before { content: "◐ light"; } +.theme-toggle__label::before { content: "◐ light"; } +[data-theme="dark"] .theme-toggle__label::before { content: "◐ dark"; } /* Tone toggle (segmented control: Novice | Intermediate) */ .tone-toggle { diff --git a/app/templates/base.html b/app/templates/base.html index 18280a7..4a2f402 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -8,9 +8,9 @@ diff --git a/app/templates/login.html b/app/templates/login.html index 5274257..7349631 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -6,8 +6,8 @@ {{ BRAND_NAME }} · Sign in diff --git a/app/templates/verify.html b/app/templates/verify.html index 6c210b5..ae62056 100644 --- a/app/templates/verify.html +++ b/app/templates/verify.html @@ -6,8 +6,8 @@ {{ BRAND_NAME }} · Verify email diff --git a/tests/test_branding_consistency.py b/tests/test_branding_consistency.py index 16edd39..a10c3a4 100644 --- a/tests/test_branding_consistency.py +++ b/tests/test_branding_consistency.py @@ -44,17 +44,10 @@ def css_text() -> str: return CSS_PATH.read_text(encoding="utf-8") -def test_dark_palette_matches_css(css_text): - css_dark = _extract_vars(css_text, ":root") - for key, expected in branding.DARK.items(): - actual = css_dark.get(key) - assert actual == expected.lower(), ( - f"DARK[{key!r}] mismatch: branding.py={expected!r} vs css={actual!r}" - ) - - def test_light_palette_matches_css(css_text): - css_light = _extract_vars(css_text, '[data-theme="light"]') + # Light is the default — lives in `:root`. Dark is opt-in via the + # data-theme attribute toggle. + css_light = _extract_vars(css_text, ":root") for key, expected in branding.LIGHT.items(): actual = css_light.get(key) assert actual == expected.lower(), ( @@ -62,6 +55,15 @@ def test_light_palette_matches_css(css_text): ) +def test_dark_palette_matches_css(css_text): + css_dark = _extract_vars(css_text, '[data-theme="dark"]') + for key, expected in branding.DARK.items(): + actual = css_dark.get(key) + assert actual == expected.lower(), ( + f"DARK[{key!r}] mismatch: branding.py={expected!r} vs css={actual!r}" + ) + + def test_palette_keys_match_between_themes(): """If a colour is defined in dark, it must also be defined in light (and vice versa) — otherwise the theme switch leaves elements