diff --git a/BREAD_DESIGN_SYSTEM.md b/BREAD_DESIGN_SYSTEM.md
new file mode 100644
index 0000000..942f7a2
--- /dev/null
+++ b/BREAD_DESIGN_SYSTEM.md
@@ -0,0 +1,118 @@
+# Bread Design System
+
+Unified visual identity for breadbar, breadbox, breadpad/breadman, and
+bos-settings.
+
+## Architecture (single source of truth)
+
+The tokens below are implemented once in the **`bread-theme`** crate as
+`stylesheet(&Palette)` — the full component stylesheet (buttons, entries,
+switches, lists/rows/sidebars, cards, chips, scrollbars, headings) over a
+canonical `@define-color` palette (`surface`=color0, `overlay`=color7,
+`accent`=color4).
+
+- The `bread-theme` **CLI** renders it from the live pywal palette to
+ `$XDG_RUNTIME_DIR/bread/theme.css` (run at login and from a pywal hook).
+- Every GUI loads that file via `bread_theme::gtk::apply_shared()` and
+ **live-reloads** it, then layers on only its own app-specific rules.
+
+Result: one definition, no per-app drift, and palette changes recolour the
+whole desktop with no rebuilds. Apps reference the shared `@define-color`
+names rather than raw palette slots.
+
+## Typography
+
+- **Font Family**: Varela Round, sans-serif
+- **Base Size**: 14px
+- **Secondary**: 12px (metadata, helper text, secondary labels)
+- **Font Weight**: Normal (400) for body, Bold (700) for emphasis
+
+## Spacing Scale (4px units)
+
+Use these values consistently across all projects:
+
+- **xs**: 4px (small gaps, internal padding)
+- **sm**: 8px (default spacing between elements)
+- **md**: 12px (medium spacing, main padding)
+- **lg**: 16px (large padding, major spacing)
+- **xl**: 20px (extra large spacing, section breaks)
+
+## Border Radius
+
+Establish a visual hierarchy with consistent rounding:
+
+- **Primary** (buttons, cards, main containers): **8px**
+- **Secondary** (input fields, chips, entries): **6px**
+- **Tertiary** (small interactive elements): **4px**
+- **Pill** (fully rounded buttons, badges): **999px**
+
+## Color System
+
+All projects use **pywal dynamic theming** with **Catppuccin Mocha** as the fallback palette:
+
+- **Background**: `#1e1e2e` (Catppuccin)
+- **Foreground**: `#cdd6f4` (Catppuccin)
+- **Surface**: `#181825` (Catppuccin)
+- **Accent**: Dynamic (from pywal)
+
+Color palette slots (via wal):
+- color0–color7: ANSI colors
+- Semantic: red, green, yellow, blue, pink, teal
+
+## Component Standards
+
+### Buttons
+- Border Radius: 8px
+- Padding: 8px 16px (primary), 4px 8px (secondary)
+- Font Size: 14px
+- Background: Theme accent color
+
+### Input Fields
+- Border Radius: 6px
+- Padding: 12px 16px
+- Font Size: 14px
+- Border: 1px or 2px solid (blue on focus)
+
+### Cards
+- Border Radius: 8px
+- Padding: 12px
+- Margin: 8px
+- Box Shadow: Optional, for depth
+
+### Stat Labels
+- Font Size: 14px
+- Margin Right (between icon/text): 5px
+- Group Margin Right: 12px
+
+### Notification Cards
+- Border Radius: 8px
+- Padding: 12px
+- Margin Bottom: 8px
+- Font Size: 14px (summary), 12px (body)
+
+## Current Implementation
+
+All GUI apps load `bread_theme::stylesheet` (via the generated shared file) and
+add only app-specific rules:
+
+- **breadbar** — shared base + bar window, workspace buttons, stats, notification
+ and OSD cards.
+- **breadbox** — shared base + launcher panel, search entry, result rows.
+- **breadpad / breadman** — shared base + capture popup, type chips, note cards,
+ reminder window, sidebar rows.
+- **bos-settings** — shared base + content padding only (was previously a
+ hardcoded Nord palette; migrated to the shared stylesheet).
+- **breadcrumbs** — CLI tool; ANSI colours only, no GUI styling.
+
+> Palette note: the fallback is Catppuccin Mocha, but installs (e.g. BOS) drive
+> the real palette from pywal — BOS ships a black-base palette.
+
+## Future Consistency Checks
+
+When adding new components or updating existing ones:
+1. Use Varela Round for all text
+2. Set base font size to 14px (12px for secondary)
+3. Use spacing scale (4px units: 4, 8, 12, 16, 20)
+4. Use border radius from this system (8px default, 6px secondary)
+5. Leverage pywal colors for dynamic theming
+6. Keep margins/padding consistent across similar components
diff --git a/README.md b/README.md
index 405c193..1ec4cff 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,36 @@ bakery install breadbar
| `breadcrumbs` | Profile-aware Wi-Fi state machine with Tailscale exit-node management and a self-healing watch daemon |
| `breadpad` | Quick-capture scratchpad popup with AI-powered note classification, reminders, recurrence, and a full note viewer (`breadman`) |
+## Recommended keybinds
+
+The ecosystem assumes a Hyprland setup with `SUPER` as the modifier. The
+conventional bindings (used by BOS and recommended for any install):
+
+| Keys | Action |
+|------|--------|
+| `SUPER+Space` | `breadbox` — app launcher |
+| `SUPER+U` | `breadpad` — quick-capture notes/reminders |
+| `SUPER+M` | `breadman` — note viewer / manager |
+| `SUPER+,` | settings (`bos-settings`, where installed) |
+
+`breadbar` and `breadd` are services started at login (`exec-once`), not bound
+to keys.
+
+## Theming
+
+All GUIs share one look via `bread-theme`. The `bread-theme` CLI renders the
+component stylesheet from your pywal palette (Catppuccin Mocha fallback) to
+`$XDG_RUNTIME_DIR/bread/theme.css`; every app loads that file and **live-reloads**
+it, so changing your wallpaper recolours the whole ecosystem with no rebuilds:
+
+```sh
+wal -i ~/Pictures/wall.png # regenerate pywal palette
+bread-theme generate # render the shared stylesheet (run from a wal hook)
+```
+
+See [`BREAD_DESIGN_SYSTEM.md`](BREAD_DESIGN_SYSTEM.md) for the tokens (fonts,
+spacing, radii, colour roles) the stylesheet is built from.
+
## Installing bakery
`bakery` is the package manager for the ecosystem. Install it with the bootstrap script:
diff --git a/bread-theme/Cargo.toml b/bread-theme/Cargo.toml
index 39930a1..8dc41e7 100644
--- a/bread-theme/Cargo.toml
+++ b/bread-theme/Cargo.toml
@@ -18,3 +18,9 @@ gtk4 = { version = "0.11", features = ["v4_12"], optional = true }
# Enable GTK4 CSS provider helpers (breadbar, breadbox, breadpad use this).
# bread (daemon) and breadcrumbs (CLI) depend on this crate without the feature.
gtk = ["dep:gtk4"]
+
+# The generator CLI. It only touches the gtk-free lib API (render + write), so
+# it builds without the gtk feature and stays light.
+[[bin]]
+name = "bread-theme"
+path = "src/bin/bread-theme.rs"
diff --git a/bread-theme/src/bin/bread-theme.rs b/bread-theme/src/bin/bread-theme.rs
new file mode 100644
index 0000000..266ea9c
--- /dev/null
+++ b/bread-theme/src/bin/bread-theme.rs
@@ -0,0 +1,62 @@
+//! `bread-theme` — generates the ecosystem's shared GTK stylesheet from the
+//! current pywal palette and writes it to the canonical path that every bread
+//! GUI loads. Run it at session start, and again after the wallpaper/palette
+//! changes (e.g. from a pywal hook); apps watch the file and recolour live.
+//!
+//! bread-theme # same as `generate`
+//! bread-theme generate # render + write the shared stylesheet
+//! bread-theme reload # re-render from the current pywal palette and
+//! # signal every running bread GUI to recolour
+//! bread-theme path # print the stylesheet path
+//! bread-theme print # render to stdout (no write)
+
+use std::process::ExitCode;
+
+fn write_and_report(verb: &str) -> ExitCode {
+ match bread_theme::write_shared_css() {
+ Ok(path) => {
+ eprintln!("bread-theme: {verb} {}", path.display());
+ ExitCode::SUCCESS
+ }
+ Err(e) => {
+ eprintln!("bread-theme: failed to write stylesheet: {e}");
+ ExitCode::FAILURE
+ }
+ }
+}
+
+fn main() -> ExitCode {
+ let cmd = std::env::args().nth(1).unwrap_or_else(|| "generate".into());
+ match cmd.as_str() {
+ "path" => {
+ println!("{}", bread_theme::shared_css_path().display());
+ ExitCode::SUCCESS
+ }
+ "print" => {
+ print!("{}", bread_theme::render());
+ ExitCode::SUCCESS
+ }
+ "generate" => write_and_report("wrote"),
+ // `reload` is `generate` from the caller's view, but it's the verb to use
+ // after changing pywal colours: rewriting the file (atomic rename) trips
+ // the file monitor in every running bread GUI, so they all re-read the
+ // palette and recolour live — shared widgets *and* each app's own rules.
+ "reload" => write_and_report("reloaded"),
+ "-h" | "--help" | "help" => {
+ eprintln!(
+ "bread-theme — shared stylesheet generator\n\n\
+ USAGE:\n bread-theme [generate|reload|path|print]\n\n\
+ generate render the pywal palette to the shared stylesheet (default)\n\
+ reload re-render and signal running bread GUIs to recolour live\n\
+ path print the stylesheet path ({})\n\
+ print render to stdout without writing",
+ bread_theme::shared_css_path().display()
+ );
+ ExitCode::SUCCESS
+ }
+ other => {
+ eprintln!("bread-theme: unknown command '{other}' (try generate|reload|path|print)");
+ ExitCode::FAILURE
+ }
+ }
+}
diff --git a/bread-theme/src/gtk.rs b/bread-theme/src/gtk.rs
index 6e62cb4..aab7d01 100644
--- a/bread-theme/src/gtk.rs
+++ b/bread-theme/src/gtk.rs
@@ -1,7 +1,100 @@
+use gtk4::gio;
+use gtk4::prelude::*;
use gtk4::CssProvider;
use std::cell::RefCell;
use std::path::Path;
+thread_local! {
+ static SHARED_PROVIDER: RefCell