diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml
new file mode 100644
index 0000000..2128050
--- /dev/null
+++ b/.forgejo/workflows/mirror.yml
@@ -0,0 +1,21 @@
+name: Mirror to GitHub
+
+on:
+ push:
+ branches: ['**']
+ tags: ['**']
+
+jobs:
+ mirror:
+ runs-on: [self-hosted, hestia]
+ steps:
+ - name: Mirror to GitHub
+ run: |
+ set -euo pipefail
+ git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git
+ cd repo.git
+ # Mirror only branches and tags (not refs/pull/*, which GitHub rejects);
+ # --prune deletes GitHub refs that no longer exist on Forgejo.
+ git push --prune \
+ "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bread-ecosystem.git" \
+ '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml
new file mode 100644
index 0000000..6725e22
--- /dev/null
+++ b/.forgejo/workflows/package.yml
@@ -0,0 +1,40 @@
+name: Build and publish package
+
+on:
+ push:
+ tags: ['v*']
+
+jobs:
+ package:
+ runs-on: [self-hosted, hestia]
+ container:
+ image: archlinux:latest
+ steps:
+ # Note: no actions/checkout — the archlinux image has no Node, which JS
+ # actions require. Everything runs as shell steps and clones manually.
+ - name: Build and publish
+ env:
+ PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
+ run: |
+ set -euo pipefail
+ VERSION="${GITHUB_REF_NAME#v}"
+ pacman -Syu --noconfirm base-devel git rust cargo
+ useradd -m builder
+ git config --global --add safe.directory '*'
+ git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
+ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src
+ cd /home/builder/src
+ git archive --format=tar.gz --prefix="bakery-${VERSION}/" HEAD \
+ > packaging/arch/bakery-${VERSION}.tar.gz
+ SHA=$(sha256sum packaging/arch/bakery-${VERSION}.tar.gz | awk '{print $1}')
+ sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD
+ sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD
+ chown -R builder:builder /home/builder/src
+ # --nocheck: packaging builds the artifact; tests belong in a CI job.
+ su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck"
+ PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1)
+ curl -fsS -X PUT \
+ -H "Authorization: token ${PUBLISH_TOKEN}" \
+ -H "Content-Type: application/octet-stream" \
+ --data-binary "@${PKG}" \
+ "https://git.breadway.dev/api/packages/Breadway/arch/os"
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