diff --git a/.opencode/command/changelog.md b/.opencode/commands/changelog.md
similarity index 100%
rename from .opencode/command/changelog.md
rename to .opencode/commands/changelog.md
diff --git a/.opencode/skills/theme-system/SKILL.md b/.opencode/skills/theme-system/SKILL.md
new file mode 100644
index 00000000..9304a6ec
--- /dev/null
+++ b/.opencode/skills/theme-system/SKILL.md
@@ -0,0 +1,205 @@
+---
+name: theme-system
+description: Use when creating or modifying UI components, styling, or visual elements in OpenChamber. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
+license: MIT
+compatibility: opencode
+---
+
+## Overview
+
+OpenChamber uses a JSON-based theme system. Themes are defined in `packages/ui/src/lib/theme/themes/`. Users can also add custom themes via `~/.config/openchamber/themes/`.
+
+**Core principle:** UI colors must use theme tokens - never hardcoded hex colors or Tailwind color classes.
+
+## When to Use
+
+- Creating or modifying UI components
+- Working with colors, backgrounds, borders, or text
+
+## Quick Decision Tree
+
+1. **Code display?** → `syntax.*`
+2. **Feedback/status?** → `status.*`
+3. **Primary CTA?** → `primary.*`
+4. **Interactive/clickable?** → `interactive.*`
+5. **Background layer?** → `surface.*`
+6. **Text?** → `surface.foreground` or `surface.mutedForeground`
+
+## Critical Rules
+
+- `surface.elevated` = inputs, cards, panels
+- `interactive.hover` = **ONLY on clickable elements**
+- `interactive.selection` = active/selected states (not primary!)
+- Status colors = **ONLY for actual feedback** (errors, warnings, success)
+- Input footers = `bg-transparent` on elevated background
+
+### Never Use
+
+- Hardcoded hex colors (`#FF0000`)
+- Tailwind colors (`bg-white`, `text-blue-500`, `bg-gray-*`)
+- Deprecated: `bg-secondary`, `bg-muted`
+
+## Usage
+
+### Via Hook
+```tsx
+import { useThemeSystem } from '@/contexts/useThemeSystem';
+const { currentTheme } = useThemeSystem();
+
+
+```
+
+### Via CSS Variables
+```tsx
+
+```
+
+## Color Tokens
+
+### Surface Colors
+
+| Token | Usage |
+|-------|-------|
+| `surface.background` | Main app background |
+| `surface.elevated` | Inputs, cards, panels, popovers |
+| `surface.muted` | Secondary backgrounds, sidebars |
+| `surface.foreground` | Primary text |
+| `surface.mutedForeground` | Secondary text, hints |
+| `surface.subtle` | Subtle dividers |
+
+### Interactive Colors
+
+| Token | Usage |
+|-------|-------|
+| `interactive.border` | Default borders |
+| `interactive.hover` | Hover on **clickable elements only** |
+| `interactive.selection` | Active/selected items |
+| `interactive.selectionForeground` | Text on selection |
+| `interactive.focusRing` | Focus indicators |
+
+### Status Colors
+
+| Token | Usage |
+|-------|-------|
+| `status.error` | Errors, validation failures |
+| `status.warning` | Warnings, cautions |
+| `status.success` | Success messages |
+| `status.info` | Informational messages |
+
+Each has variants: `*`, `*Foreground`, `*Background`, `*Border`.
+
+### Primary Colors
+
+| Token | Usage |
+|-------|-------|
+| `primary.base` | Primary CTA buttons |
+| `primary.hover` | Hover on primary elements |
+| `primary.foreground` | Text on primary background |
+
+**Primary vs Selection:** Primary = "click me" (CTA), Selection = "currently active" (state).
+
+### Syntax Colors
+
+For code display only. Never use for UI elements.
+
+| Token | Usage |
+|-------|-------|
+| `syntax.base.background` | Code block background |
+| `syntax.base.foreground` | Default code text |
+| `syntax.base.keyword` | Keywords |
+| `syntax.base.string` | Strings |
+| `syntax.highlights.diffAdded` | Added lines |
+| `syntax.highlights.diffRemoved` | Removed lines |
+
+## Examples
+
+### Input Area
+
+```tsx
+const { currentTheme } = useThemeSystem();
+
+
+
+
{/* Footer - transparent! */}
+
+```
+
+### Active Tab
+
+```tsx
+
+```
+
+### Error Message
+
+```tsx
+
+```
+
+### Card
+
+```tsx
+
+```
+
+## Wrong vs Right
+
+### Wrong
+
+```tsx
+// Hardcoded colors
+
+
+
+// Primary for active tab
+Active
+
+// Hover on static element
+Static card
+
+// Colored footer on input
+
+```
+
+### Right
+
+```tsx
+// Theme tokens
+
+
+
+// Selection for active tab
+Active
+
+// Hover only on clickable
+Click
+
+// Transparent footer
+
+```
+
+## References
+
+- **[Adding Themes](references/adding-themes.md)** - Built-in and custom themes
+
+## Key Files
+
+- Theme types: `packages/ui/src/types/theme.ts`
+- Theme hook: `packages/ui/src/contexts/useThemeSystem.ts`
+- CSS generator: `packages/ui/src/lib/theme/cssGenerator.ts`
+- Built-in themes: `packages/ui/src/lib/theme/themes/`
diff --git a/.opencode/skills/theme-system/references/adding-themes.md b/.opencode/skills/theme-system/references/adding-themes.md
new file mode 100644
index 00000000..0209a660
--- /dev/null
+++ b/.opencode/skills/theme-system/references/adding-themes.md
@@ -0,0 +1,51 @@
+---
+title: Adding Themes
+---
+
+# Adding Themes
+
+## Custom Themes (User)
+
+Drop a JSON file into `~/.config/openchamber/themes/`. No rebuild needed.
+
+1. Create theme file (e.g., `my-theme.json`)
+2. In app: **Settings → Theme → Reload themes**
+3. Select from dropdown
+
+See `docs/CUSTOM_THEMES.md` for full format reference.
+
+## Built-in Themes (Development)
+
+### 1. Create JSON Files
+
+Add to `packages/ui/src/lib/theme/themes/`:
+- `-light.json`
+- `-dark.json`
+
+Use existing themes (e.g., `flexoki-dark.json`) as reference for the full structure.
+
+### 2. Register in presets.ts
+
+```typescript
+import mytheme_light_Raw from './mytheme-light.json';
+import mytheme_dark_Raw from './mytheme-dark.json';
+
+export const presetThemes: Theme[] = [
+ // ... existing themes
+ mytheme_light_Raw as Theme,
+ mytheme_dark_Raw as Theme,
+];
+```
+
+### 3. Validate
+
+```bash
+bun run type-check && bun run lint && bun run build
+```
+
+## Key Files
+
+- Theme types: `packages/ui/src/types/theme.ts`
+- Presets: `packages/ui/src/lib/theme/themes/presets.ts`
+- Example: `packages/ui/src/lib/theme/themes/flexoki-dark.json`
+- Custom themes doc: `docs/CUSTOM_THEMES.md`
diff --git a/AGENTS.md b/AGENTS.md
index 6200f8b2..ebe3c174 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -72,6 +72,17 @@ All scripts are in `package.json`.
- No new deps unless asked.
- Never add secrets (`.env`, keys) or log sensitive data.
+## Theme System (MANDATORY for UI work)
+
+When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first.
+
+**Before starting any UI work:**
+```
+skill({ name: "theme-system" })
+```
+
+This skill contains all color tokens, semantic logic, decision tree, and usage patterns. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes.
+
## Recent changes
- Releases + high-level changes: `CHANGELOG.md`
- Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`)
diff --git a/README.md b/README.md
index 59b1a80a..1194d07c 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,12 @@ Run OpenChamber remotely using GitHub Actions. No local computer required.
[**Read the Guide: OpenChamber for Actions**](docs/OPENCHAMBER_FOR_ACTIONS.md)
+### Custom Themes
+
+Create your own color schemes by dropping JSON files into `~/.config/openchamber/themes/`. Hot reload supported — no restart needed.
+
+[**Read the Guide: Custom Themes**](docs/CUSTOM_THEMES.md)
+
## Installation
### VS Code Extension
diff --git a/docs/CUSTOM_THEMES.md b/docs/CUSTOM_THEMES.md
new file mode 100644
index 00000000..905a6af5
--- /dev/null
+++ b/docs/CUSTOM_THEMES.md
@@ -0,0 +1,218 @@
+# Custom Themes
+
+OpenChamber supports user-defined themes. Drop a JSON file into the themes directory and reload — no app restart required.
+
+## Quick Start
+
+1. Create the themes directory:
+ ```bash
+ mkdir -p ~/.config/openchamber/themes
+ ```
+
+2. Create a theme JSON file (e.g., `my-theme.json`) with the format below.
+
+3. In OpenChamber: **Settings → Theme → Reload themes**.
+
+4. Select your theme from the dropdown.
+
+## Theme Location
+
+| Platform | Path |
+|----------|------|
+| macOS/Linux | `~/.config/openchamber/themes/` |
+
+## Theme Format
+
+```json
+{
+ "metadata": {
+ "id": "my-custom-theme",
+ "name": "My Custom Theme",
+ "description": "A custom theme for OpenChamber",
+ "version": "1.0.0",
+ "variant": "dark",
+ "tags": ["dark", "custom"]
+ },
+ "colors": {
+ "primary": {
+ "base": "#EC8B49",
+ "hover": "#DA702C",
+ "active": "#F9AE77",
+ "foreground": "#100F0F",
+ "muted": "#EC8B4980",
+ "emphasis": "#F9AE77"
+ },
+ "surface": {
+ "background": "#100F0F",
+ "foreground": "#CECDC3",
+ "muted": "#1C1B1A",
+ "mutedForeground": "#878580",
+ "elevated": "#1C1A19",
+ "elevatedForeground": "#CECDC3",
+ "overlay": "#00000080",
+ "subtle": "#1e1d1c"
+ },
+ "interactive": {
+ "border": "#343331",
+ "borderHover": "#403E3C",
+ "borderFocus": "#EC8B49",
+ "selection": "#f4f4f41f",
+ "selectionForeground": "#CECDC3",
+ "focus": "#EC8B49",
+ "focusRing": "#EC8B4950",
+ "cursor": "#CECDC3",
+ "hover": "#ffffff18",
+ "active": "#ffffff1f"
+ },
+ "status": {
+ "error": "#D14D41",
+ "errorForeground": "#100F0F",
+ "errorBackground": "#AF302920",
+ "errorBorder": "#AF302950",
+ "warning": "#DA702C",
+ "warningForeground": "#100F0F",
+ "warningBackground": "#BC521520",
+ "warningBorder": "#BC521550",
+ "success": "#A0AF54",
+ "successForeground": "#100F0F",
+ "successBackground": "#66800B20",
+ "successBorder": "#66800B50",
+ "info": "#4385BE",
+ "infoForeground": "#100F0F",
+ "infoBackground": "#205EA620",
+ "infoBorder": "#205EA650"
+ },
+ "syntax": {
+ "base": {
+ "background": "#1C1B1A",
+ "foreground": "#CECDC3",
+ "comment": "#878580",
+ "keyword": "#4385BE",
+ "string": "#3AA99F",
+ "number": "#8B7EC8",
+ "function": "#DA702C",
+ "variable": "#CECDC3",
+ "type": "#D0A215",
+ "operator": "#D14D41"
+ },
+ "tokens": {
+ "commentDoc": "#575653",
+ "stringEscape": "#CECDC3",
+ "keywordImport": "#D14D41",
+ "storageModifier": "#4385BE",
+ "functionCall": "#DA702C",
+ "method": "#879A39",
+ "variableProperty": "#4385BE",
+ "variableOther": "#879A39",
+ "variableGlobal": "#CE5D97",
+ "variableLocal": "#282726",
+ "parameter": "#CECDC3",
+ "constant": "#CECDC3",
+ "class": "#DA702C",
+ "className": "#DA702C",
+ "interface": "#D0A215",
+ "struct": "#DA702C",
+ "enum": "#DA702C",
+ "typeParameter": "#DA702C",
+ "namespace": "#D0A215",
+ "module": "#D14D41",
+ "tag": "#4385BE",
+ "jsxTag": "#CE5D97",
+ "tagAttribute": "#D0A215",
+ "tagAttributeValue": "#3AA99F",
+ "boolean": "#D0A215",
+ "decorator": "#D0A215",
+ "label": "#CE5D97",
+ "punctuation": "#878580",
+ "macro": "#4385BE",
+ "preprocessor": "#CE5D97",
+ "regex": "#3AA99F",
+ "url": "#4385BE",
+ "key": "#DA702C",
+ "exception": "#CE5D97"
+ },
+ "highlights": {
+ "diffAdded": "#879A39",
+ "diffAddedBackground": "#66800B20",
+ "diffRemoved": "#D14D41",
+ "diffRemovedBackground": "#AF302920",
+ "diffModified": "#4385BE",
+ "diffModifiedBackground": "#205EA620",
+ "lineNumber": "#403E3C",
+ "lineNumberActive": "#CECDC3"
+ }
+ },
+ "markdown": {
+ "heading1": "#fbf9e6",
+ "heading2": "#e6e4d2",
+ "heading3": "#CECDC3",
+ "heading4": "#CECDC3",
+ "link": "#4385BE",
+ "linkHover": "#205EA6",
+ "inlineCode": "#A0AF53",
+ "inlineCodeBackground": "#1C1B1A",
+ "blockquote": "#878580",
+ "blockquoteBorder": "#343331",
+ "listMarker": "#D0A21599"
+ },
+ "chat": {
+ "userMessage": "#CECDC3",
+ "userMessageBackground": "#2d1d15",
+ "assistantMessage": "#CECDC3",
+ "assistantMessageBackground": "#100F0F",
+ "timestamp": "#878580",
+ "divider": "#343331"
+ },
+ "tools": {
+ "background": "#1C1B1A50",
+ "border": "#42403e9d",
+ "headerHover": "#34333150",
+ "icon": "#aca7a1",
+ "title": "#CECDC3",
+ "description": "#878580",
+ "edit": {
+ "added": "#879A39",
+ "addedBackground": "#66800B25",
+ "removed": "#D14D41",
+ "removedBackground": "#AF302925",
+ "lineNumber": "#403E3C"
+ }
+ }
+ },
+ "config": {
+ "fonts": {
+ "sans": "\"IBM Plex Mono\", monospace",
+ "mono": "\"IBM Plex Mono\", monospace",
+ "heading": "\"IBM Plex Mono\", monospace"
+ },
+ "radius": {
+ "none": "0",
+ "sm": "0.125rem",
+ "md": "0.375rem",
+ "lg": "0.5rem",
+ "xl": "0.75rem",
+ "full": "9999px"
+ },
+ "transitions": {
+ "fast": "150ms ease",
+ "normal": "250ms ease",
+ "slow": "350ms ease"
+ }
+ }
+}
+```
+
+## Validation
+
+Themes are validated on load. Invalid themes are skipped with a console warning.
+
+Common issues:
+- Missing required fields
+- Invalid `variant` (must be `"light"` or `"dark"`)
+- File size > 512KB
+
+## Tips
+
+- Use hex with alpha for transparency (e.g., `#FFFFFF20`)
+- Reference built-in themes in `packages/ui/src/lib/theme/themes/` for more examples
+- Theme `id` must be unique; duplicates are skipped
diff --git a/packages/desktop/index.html b/packages/desktop/index.html
index 58daab29..3fe4c2c5 100644
--- a/packages/desktop/index.html
+++ b/packages/desktop/index.html
@@ -26,6 +26,17 @@
document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
// Store for use in inline styles
window.__INITIAL_THEME_DARK__ = isDark;
+
+ // Splash colors persisted by the app theme system
+ var splashBgLight = localStorage.getItem('splashBgLight');
+ var splashFgLight = localStorage.getItem('splashFgLight');
+ var splashBgDark = localStorage.getItem('splashBgDark');
+ var splashFgDark = localStorage.getItem('splashFgDark');
+
+ if (splashBgLight) document.documentElement.style.setProperty('--splash-background-light', splashBgLight);
+ if (splashFgLight) document.documentElement.style.setProperty('--splash-stroke-light', splashFgLight);
+ if (splashBgDark) document.documentElement.style.setProperty('--splash-background-dark', splashBgDark);
+ if (splashFgDark) document.documentElement.style.setProperty('--splash-stroke-dark', splashFgDark);
})();