# Systray dashboard theme (System / Light / Dark)

audience: AI coding agents first.

## Goal

Dashboard popup MUST match Cinnamon dark themes by default. Settings MUST offer System / Light / Dark. System MUST keep following the desktop theme.

## Decisions

1. Persist `theme: "system" | "light" | "dark"` on `Settings`. Default `"system"`. Unknown/missing → `"system"`.
2. Resolve preference → `"light" | "dark"` via `resolve_theme(preference, *, system_is_dark=...)`.
3. System dark detection order (first hit wins):
   - `org.gnome.desktop.interface color-scheme` contains `prefer-dark`
   - active GTK theme name contains `dark` (case-insensitive) — covers Cinnamon `gtk-theme` like `Adwaita-dark`
   - `Gtk.Settings.gtk-application-prefer-dark-theme` is true
   - else light
4. Apply resolved mode as CSS class on dashboard root: exactly one of `theme-light` / `theme-dark`.
5. Dark palette lives as `.dashboard.theme-dark …` overrides in `ui/style.css`. Light stays the unscoped / `theme-light` baseline.
6. Settings dialog: ComboBoxText labeled "Theme" with ids `system` / `light` / `dark`, labels System / Light / Dark.
7. On settings save + whenever popup rebuilds / opens: re-resolve and re-apply class. No live gsettings watcher in v1 (YAGNI).

## Seams

```
ThemePreference = Literal["system", "light", "dark"]
ResolvedTheme = Literal["light", "dark"]

resolve_theme(preference: str, *, system_is_dark: Callable[[], bool] | None = None) -> ResolvedTheme
detect_system_dark() -> bool
Settings.theme: ThemePreference = "system"
Dashboard.set_theme(resolved: ResolvedTheme) -> None
  - toggles theme-light / theme-dark on root widget
remove_css_class(widget, name) -> None
```

JSON settings payload gains `"theme": "system"|"light"|"dark"`.

## Out of scope

- Live theme-change while popup stays open
- Settings dialog / auth dialog theming beyond what GTK theme already provides
- Removing intentional dashboard colors to go fully native GTK

## Tests

- Store: default theme, round-trip, invalid theme → system, atomic write includes theme
- Dialog: combo reflects setting; save persists theme; preserves last_tab
- `resolve_theme` / `detect_system_dark` with injectable detector / mocked gsettings outputs
- Dashboard `set_theme` swaps css classes on fallback widget

## Architecture Decisions

- Collapse separate `theme.py` vs put helpers in `ui/style.py`: **keep `ui/theme.py`** — detection ≠ CSS load; style.py stays CssProvider-only.
- No dual CSS files — one file + class overrides (Approach 1).
