diff --git a/docs/superpowers/plans/2026-04-02-design-tokens.md b/docs/superpowers/plans/2026-04-02-design-tokens.md
new file mode 100644
index 0000000..b0a2f46
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-02-design-tokens.md
@@ -0,0 +1,600 @@
+# Design Tokens — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** JSON-driven multi-hospital theming — sidecar serves theme config, frontend provider injects CSS variables + content tokens, supervisor edits branding from Settings.
+
+**Architecture:** Sidecar stores `data/theme.json`, serves via REST. Frontend `ThemeTokenProvider` fetches on mount, overrides CSS custom properties on ``, exposes content tokens via React context. Settings page has a Branding tab for admins.
+
+**Tech Stack:** NestJS (sidecar controller/service), React context + CSS custom properties (frontend), Untitled UI components (settings form)
+
+**Spec:** `docs/superpowers/specs/2026-04-02-design-tokens-design.md`
+
+---
+
+## File Map
+
+| File | Action | Responsibility |
+|---|---|---|
+| `helix-engage-server/src/config/theme.controller.ts` | Create | GET/PUT/POST endpoints for theme |
+| `helix-engage-server/src/config/theme.service.ts` | Create | Read/write/validate/backup theme JSON |
+| `helix-engage-server/src/config/theme.defaults.ts` | Create | Default Global Hospital theme constant |
+| `helix-engage-server/src/config/config.module.ts` | Create | NestJS module for theme |
+| `helix-engage-server/src/app.module.ts` | Modify | Import ConfigThemeModule |
+| `helix-engage-server/data/theme.json` | Create | Default theme file |
+| `helix-engage/src/providers/theme-token-provider.tsx` | Create | Fetch theme, inject CSS vars, expose context |
+| `helix-engage/src/main.tsx` | Modify | Wrap app with ThemeTokenProvider |
+| `helix-engage/src/pages/login.tsx` | Modify | Consume tokens instead of hardcoded strings |
+| `helix-engage/src/components/layout/sidebar.tsx` | Modify | Consume tokens for title/subtitle |
+| `helix-engage/src/components/call-desk/ai-chat-panel.tsx` | Modify | Consume tokens for quick actions |
+| `helix-engage/src/pages/branding-settings.tsx` | Create | Branding tab in settings for admins |
+| `helix-engage/src/main.tsx` | Modify | Add branding settings route |
+
+---
+
+### Task 1: Default Theme Constant + Theme Service (Sidecar)
+
+**Files:**
+- Create: `helix-engage-server/src/config/theme.defaults.ts`
+- Create: `helix-engage-server/src/config/theme.service.ts`
+
+- [ ] **Step 1: Create theme.defaults.ts**
+
+```typescript
+// src/config/theme.defaults.ts
+
+export type ThemeConfig = {
+ brand: {
+ name: string;
+ hospitalName: string;
+ logo: string;
+ favicon: string;
+ };
+ colors: {
+ brand: Record;
+ };
+ typography: {
+ body: string;
+ display: string;
+ };
+ login: {
+ title: string;
+ subtitle: string;
+ showGoogleSignIn: boolean;
+ showForgotPassword: boolean;
+ poweredBy: { label: string; url: string };
+ };
+ sidebar: {
+ title: string;
+ subtitle: string;
+ };
+ ai: {
+ quickActions: Array<{ label: string; prompt: string }>;
+ };
+};
+
+export const DEFAULT_THEME: ThemeConfig = {
+ brand: {
+ name: 'Helix Engage',
+ hospitalName: 'Global Hospital',
+ logo: '/helix-logo.png',
+ favicon: '/favicon.ico',
+ },
+ colors: {
+ brand: {
+ '25': 'rgb(239 246 255)',
+ '50': 'rgb(219 234 254)',
+ '100': 'rgb(191 219 254)',
+ '200': 'rgb(147 197 253)',
+ '300': 'rgb(96 165 250)',
+ '400': 'rgb(59 130 246)',
+ '500': 'rgb(37 99 235)',
+ '600': 'rgb(29 78 216)',
+ '700': 'rgb(30 64 175)',
+ '800': 'rgb(30 58 138)',
+ '900': 'rgb(23 37 84)',
+ '950': 'rgb(15 23 42)',
+ },
+ },
+ typography: {
+ body: "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif",
+ display: "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif",
+ },
+ login: {
+ title: 'Sign in to Helix Engage',
+ subtitle: 'Global Hospital',
+ showGoogleSignIn: true,
+ showForgotPassword: true,
+ poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' },
+ },
+ sidebar: {
+ title: 'Helix Engage',
+ subtitle: 'Global Hospital · {role}',
+ },
+ ai: {
+ quickActions: [
+ { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' },
+ { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' },
+ { label: 'Patient history', prompt: "Can you summarize this patient's history?" },
+ { label: 'Treatment packages', prompt: 'What treatment packages are available?' },
+ ],
+ },
+};
+```
+
+- [ ] **Step 2: Create theme.service.ts**
+
+```typescript
+// src/config/theme.service.ts
+
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
+import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { DEFAULT_THEME, type ThemeConfig } from './theme.defaults';
+
+const THEME_PATH = join(process.cwd(), 'data', 'theme.json');
+const BACKUP_DIR = join(process.cwd(), 'data', 'theme-backups');
+
+@Injectable()
+export class ThemeService implements OnModuleInit {
+ private readonly logger = new Logger(ThemeService.name);
+ private cached: ThemeConfig | null = null;
+
+ onModuleInit() {
+ this.load();
+ }
+
+ getTheme(): ThemeConfig {
+ if (this.cached) return this.cached;
+ return this.load();
+ }
+
+ updateTheme(updates: Partial): ThemeConfig {
+ const current = this.getTheme();
+
+ // Deep merge
+ const merged: ThemeConfig = {
+ brand: { ...current.brand, ...updates.brand },
+ colors: {
+ brand: { ...current.colors.brand, ...updates.colors?.brand },
+ },
+ typography: { ...current.typography, ...updates.typography },
+ login: { ...current.login, ...updates.login, poweredBy: { ...current.login.poweredBy, ...updates.login?.poweredBy } },
+ sidebar: { ...current.sidebar, ...updates.sidebar },
+ ai: {
+ quickActions: updates.ai?.quickActions ?? current.ai.quickActions,
+ },
+ };
+
+ // Backup current
+ this.backup();
+
+ // Save
+ const dir = dirname(THEME_PATH);
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
+ writeFileSync(THEME_PATH, JSON.stringify(merged, null, 2), 'utf8');
+ this.cached = merged;
+
+ this.logger.log('Theme updated and saved');
+ return merged;
+ }
+
+ resetTheme(): ThemeConfig {
+ this.backup();
+ const dir = dirname(THEME_PATH);
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
+ writeFileSync(THEME_PATH, JSON.stringify(DEFAULT_THEME, null, 2), 'utf8');
+ this.cached = DEFAULT_THEME;
+ this.logger.log('Theme reset to defaults');
+ return DEFAULT_THEME;
+ }
+
+ private load(): ThemeConfig {
+ try {
+ if (existsSync(THEME_PATH)) {
+ const raw = readFileSync(THEME_PATH, 'utf8');
+ const parsed = JSON.parse(raw);
+ // Merge with defaults to fill missing fields
+ this.cached = {
+ brand: { ...DEFAULT_THEME.brand, ...parsed.brand },
+ colors: { brand: { ...DEFAULT_THEME.colors.brand, ...parsed.colors?.brand } },
+ typography: { ...DEFAULT_THEME.typography, ...parsed.typography },
+ login: { ...DEFAULT_THEME.login, ...parsed.login, poweredBy: { ...DEFAULT_THEME.login.poweredBy, ...parsed.login?.poweredBy } },
+ sidebar: { ...DEFAULT_THEME.sidebar, ...parsed.sidebar },
+ ai: { quickActions: parsed.ai?.quickActions ?? DEFAULT_THEME.ai.quickActions },
+ };
+ this.logger.log('Theme loaded from file');
+ return this.cached;
+ }
+ } catch (err) {
+ this.logger.warn(`Failed to load theme: ${err}`);
+ }
+
+ this.cached = DEFAULT_THEME;
+ this.logger.log('Using default theme');
+ return DEFAULT_THEME;
+ }
+
+ private backup() {
+ try {
+ if (!existsSync(THEME_PATH)) return;
+ if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true });
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
+ copyFileSync(THEME_PATH, join(BACKUP_DIR, `theme-${ts}.json`));
+ } catch (err) {
+ this.logger.warn(`Backup failed: ${err}`);
+ }
+ }
+}
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd helix-engage-server
+git add src/config/theme.defaults.ts src/config/theme.service.ts
+git commit -m "feat: theme service — read/write/backup theme JSON"
+```
+
+---
+
+### Task 2: Theme Controller + Module (Sidecar)
+
+**Files:**
+- Create: `helix-engage-server/src/config/theme.controller.ts`
+- Create: `helix-engage-server/src/config/config.module.ts`
+- Modify: `helix-engage-server/src/app.module.ts`
+
+- [ ] **Step 1: Create theme.controller.ts**
+
+```typescript
+// src/config/theme.controller.ts
+
+import { Controller, Get, Put, Post, Body, Logger } from '@nestjs/common';
+import { ThemeService } from './theme.service';
+import type { ThemeConfig } from './theme.defaults';
+
+@Controller('api/config')
+export class ThemeController {
+ private readonly logger = new Logger(ThemeController.name);
+
+ constructor(private readonly theme: ThemeService) {}
+
+ @Get('theme')
+ getTheme() {
+ return this.theme.getTheme();
+ }
+
+ @Put('theme')
+ updateTheme(@Body() body: Partial) {
+ this.logger.log('Theme update request');
+ return this.theme.updateTheme(body);
+ }
+
+ @Post('theme/reset')
+ resetTheme() {
+ this.logger.log('Theme reset request');
+ return this.theme.resetTheme();
+ }
+}
+```
+
+- [ ] **Step 2: Create config.module.ts**
+
+```typescript
+// src/config/config.module.ts
+// Named ConfigThemeModule to avoid conflict with NestJS ConfigModule
+
+import { Module } from '@nestjs/common';
+import { ThemeController } from './theme.controller';
+import { ThemeService } from './theme.service';
+
+@Module({
+ controllers: [ThemeController],
+ providers: [ThemeService],
+ exports: [ThemeService],
+})
+export class ConfigThemeModule {}
+```
+
+- [ ] **Step 3: Register in app.module.ts**
+
+Add import at top:
+```typescript
+import { ConfigThemeModule } from './config/config.module';
+```
+
+Add to imports array:
+```typescript
+ConfigThemeModule,
+```
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+cd helix-engage-server && npm run build
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/config/ src/app.module.ts
+git commit -m "feat: theme REST API — GET/PUT/POST endpoints"
+```
+
+---
+
+### Task 3: ThemeTokenProvider (Frontend)
+
+**Files:**
+- Create: `helix-engage/src/providers/theme-token-provider.tsx`
+- Modify: `helix-engage/src/main.tsx`
+
+- [ ] **Step 1: Create theme-token-provider.tsx**
+
+```typescript
+// src/providers/theme-token-provider.tsx
+
+import type { ReactNode } from 'react';
+import { createContext, useContext, useEffect, useState, useCallback } from 'react';
+
+const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
+
+export type ThemeTokens = {
+ brand: { name: string; hospitalName: string; logo: string; favicon: string };
+ colors: { brand: Record };
+ typography: { body: string; display: string };
+ login: { title: string; subtitle: string; showGoogleSignIn: boolean; showForgotPassword: boolean; poweredBy: { label: string; url: string } };
+ sidebar: { title: string; subtitle: string };
+ ai: { quickActions: Array<{ label: string; prompt: string }> };
+};
+
+const DEFAULT_TOKENS: ThemeTokens = {
+ brand: { name: 'Helix Engage', hospitalName: 'Global Hospital', logo: '/helix-logo.png', favicon: '/favicon.ico' },
+ colors: { brand: {} },
+ typography: { body: '', display: '' },
+ login: { title: 'Sign in to Helix Engage', subtitle: 'Global Hospital', showGoogleSignIn: true, showForgotPassword: true, poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' } },
+ sidebar: { title: 'Helix Engage', subtitle: 'Global Hospital · {role}' },
+ ai: { quickActions: [
+ { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' },
+ { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' },
+ { label: 'Patient history', prompt: "Can you summarize this patient's history?" },
+ { label: 'Treatment packages', prompt: 'What treatment packages are available?' },
+ ] },
+};
+
+type ThemeTokenContextType = {
+ tokens: ThemeTokens;
+ refresh: () => Promise;
+};
+
+const ThemeTokenContext = createContext({ tokens: DEFAULT_TOKENS, refresh: async () => {} });
+
+export const useThemeTokens = () => useContext(ThemeTokenContext);
+
+const applyColorTokens = (brandColors: Record) => {
+ const root = document.documentElement;
+ for (const [stop, value] of Object.entries(brandColors)) {
+ root.style.setProperty(`--color-brand-${stop}`, value);
+ }
+};
+
+const applyTypographyTokens = (typography: { body: string; display: string }) => {
+ const root = document.documentElement;
+ if (typography.body) root.style.setProperty('--font-body', typography.body);
+ if (typography.display) root.style.setProperty('--font-display', typography.display);
+};
+
+export const ThemeTokenProvider = ({ children }: { children: ReactNode }) => {
+ const [tokens, setTokens] = useState(DEFAULT_TOKENS);
+
+ const fetchTheme = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_URL}/api/config/theme`);
+ if (res.ok) {
+ const data: ThemeTokens = await res.json();
+ setTokens(data);
+ if (data.colors?.brand && Object.keys(data.colors.brand).length > 0) {
+ applyColorTokens(data.colors.brand);
+ }
+ if (data.typography) {
+ applyTypographyTokens(data.typography);
+ }
+ }
+ } catch {
+ // Use defaults silently
+ }
+ }, []);
+
+ useEffect(() => { fetchTheme(); }, [fetchTheme]);
+
+ return (
+
+ {children}
+
+ );
+};
+```
+
+- [ ] **Step 2: Wrap app in main.tsx**
+
+In `main.tsx`, add import:
+```typescript
+import { ThemeTokenProvider } from '@/providers/theme-token-provider';
+```
+
+Wrap inside `ThemeProvider`:
+```tsx
+
+
+
+ ...
+
+
+
+```
+
+- [ ] **Step 3: Build and verify**
+
+```bash
+npx tsc --noEmit
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/providers/theme-token-provider.tsx src/main.tsx
+git commit -m "feat: ThemeTokenProvider — fetch theme, inject CSS variables"
+```
+
+---
+
+### Task 4: Consume Tokens in Login Page
+
+**Files:**
+- Modify: `helix-engage/src/pages/login.tsx`
+
+- [ ] **Step 1: Replace hardcoded values**
+
+Import `useThemeTokens`:
+```typescript
+import { useThemeTokens } from '@/providers/theme-token-provider';
+```
+
+Inside the component:
+```typescript
+const { tokens } = useThemeTokens();
+```
+
+Replace hardcoded strings:
+- `src="/helix-logo.png"` → `src={tokens.brand.logo}`
+- `"Sign in to Helix Engage"` → `{tokens.login.title}`
+- `"Global Hospital"` → `{tokens.login.subtitle}`
+- Google sign-in section: wrap with `{tokens.login.showGoogleSignIn && (...)}`
+- Forgot password: wrap with `{tokens.login.showForgotPassword && (...)}`
+- Powered by: `tokens.login.poweredBy.label` and `tokens.login.poweredBy.url`
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/pages/login.tsx
+git commit -m "feat: login page consumes theme tokens"
+```
+
+---
+
+### Task 5: Consume Tokens in Sidebar + AI Chat
+
+**Files:**
+- Modify: `helix-engage/src/components/layout/sidebar.tsx`
+- Modify: `helix-engage/src/components/call-desk/ai-chat-panel.tsx`
+
+- [ ] **Step 1: Update sidebar.tsx**
+
+Import `useThemeTokens` and replace:
+- Line 167: `"Helix Engage"` → `{tokens.sidebar.title}`
+- Line 168: `"Global Hospital · {getRoleSubtitle(user.role)}"` → `{tokens.sidebar.subtitle.replace('{role}', getRoleSubtitle(user.role))}`
+- Line 164: favicon src → `tokens.brand.logo`
+
+- [ ] **Step 2: Update ai-chat-panel.tsx**
+
+Import `useThemeTokens` and replace:
+- Lines 21-25: hardcoded `QUICK_ACTIONS` array → `tokens.ai.quickActions`
+
+Move `QUICK_ACTIONS` usage inside the component:
+```typescript
+const { tokens } = useThemeTokens();
+const quickActions = tokens.ai.quickActions;
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/components/layout/sidebar.tsx src/components/call-desk/ai-chat-panel.tsx
+git commit -m "feat: sidebar + AI chat consume theme tokens"
+```
+
+---
+
+### Task 6: Branding Settings Page (Frontend)
+
+**Files:**
+- Create: `helix-engage/src/pages/branding-settings.tsx`
+- Modify: `helix-engage/src/main.tsx` (add route)
+- Modify: `helix-engage/src/components/layout/sidebar.tsx` (add nav item)
+
+- [ ] **Step 1: Create branding-settings.tsx**
+
+The page has 6 collapsible sections matching the spec. Uses Untitled UI `Input`, `TextArea`, `Checkbox`, `Button` components. On save, PUTs to `/api/config/theme` and calls `refresh()` from `useThemeTokens()`.
+
+Key patterns:
+- Fetch current theme on mount via `GET /api/config/theme`
+- Local state mirrors the theme JSON structure
+- Each section is a collapsible card
+- Color section: 12 text inputs for hex/rgb values with colored preview dots
+- Save button calls `PUT /api/config/theme` with the full state
+- Reset button calls `POST /api/config/theme/reset`
+- After save/reset, call `refresh()` to re-apply CSS variables immediately
+
+- [ ] **Step 2: Add route in main.tsx**
+
+```typescript
+import { BrandingSettingsPage } from '@/pages/branding-settings';
+```
+
+Add route:
+```tsx
+} />
+```
+
+- [ ] **Step 3: Add nav item in sidebar.tsx**
+
+Under the Configuration section (near Rules Engine), add "Branding" link for admin role only.
+
+- [ ] **Step 4: Build and verify**
+
+```bash
+npx tsc --noEmit
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/pages/branding-settings.tsx src/main.tsx src/components/layout/sidebar.tsx
+git commit -m "feat: branding settings page — theme editor for supervisors"
+```
+
+---
+
+### Task 7: Default Theme File + Build Verification
+
+**Files:**
+- Create: `helix-engage-server/data/theme.json`
+
+- [ ] **Step 1: Create default theme.json**
+
+Copy the `DEFAULT_THEME` object as JSON to `data/theme.json`.
+
+- [ ] **Step 2: Build both projects**
+
+```bash
+cd helix-engage-server && npm run build
+cd ../helix-engage && npm run build
+```
+
+- [ ] **Step 3: Commit all**
+
+```bash
+git add data/theme.json
+git commit -m "chore: default theme.json file"
+```
+
+---
+
+## Execution Notes
+
+- ThemeTokenProvider fetches before login — the endpoint is public (no auth)
+- CSS variable override on `` has higher specificity than the `@theme` block in `theme.css`
+- `tokens.sidebar.subtitle` supports `{role}` placeholder — replaced at render time by the sidebar component
+- The branding settings page is admin-only but the theme endpoint itself is unauthenticated (GET) — PUT requires auth
+- If the sidecar is unreachable, the frontend silently falls back to hardcoded defaults
diff --git a/docs/superpowers/specs/2026-04-02-design-tokens-design.md b/docs/superpowers/specs/2026-04-02-design-tokens-design.md
new file mode 100644
index 0000000..b1c7e04
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-02-design-tokens-design.md
@@ -0,0 +1,240 @@
+# Design Tokens — Multi-Hospital Theming
+
+**Date**: 2026-04-02
+**Status**: Draft
+
+---
+
+## Overview
+
+A JSON-driven design token system that allows each hospital customer to rebrand Helix Engage by providing a single JSON configuration file. The JSON is served by the sidecar, consumed by the frontend at runtime via a React provider that injects CSS custom properties.
+
+---
+
+## Architecture
+
+```
+Sidecar (helix-engage-server)
+ └─ GET /api/config/theme → returns hospital theme JSON
+ └─ theme stored as JSON file at data/theme.json (editable, hot-reloadable)
+
+Frontend (helix-engage)
+ └─ ThemeTokenProvider (wraps app) → fetches theme JSON on mount
+ └─ Injects CSS custom properties on element
+ └─ Exposes useThemeTokens() hook for content tokens (logo, name, text)
+ └─ Components read colors via existing Tailwind classes (no changes needed)
+```
+
+---
+
+## Theme JSON Schema
+
+```json
+{
+ "brand": {
+ "name": "Helix Engage",
+ "hospitalName": "Global Hospital",
+ "logo": "/helix-logo.png",
+ "favicon": "/favicon.ico"
+ },
+ "colors": {
+ "brand": {
+ "25": "rgb(239 246 255)",
+ "50": "rgb(219 234 254)",
+ "100": "rgb(191 219 254)",
+ "200": "rgb(147 197 253)",
+ "300": "rgb(96 165 250)",
+ "400": "rgb(59 130 246)",
+ "500": "rgb(37 99 235)",
+ "600": "rgb(29 78 216)",
+ "700": "rgb(30 64 175)",
+ "800": "rgb(30 58 138)",
+ "900": "rgb(23 37 84)",
+ "950": "rgb(15 23 42)"
+ }
+ },
+ "typography": {
+ "body": "Satoshi, Inter, -apple-system, sans-serif",
+ "display": "General Sans, Inter, -apple-system, sans-serif"
+ },
+ "login": {
+ "title": "Sign in to Helix Engage",
+ "subtitle": "Global Hospital",
+ "showGoogleSignIn": true,
+ "showForgotPassword": true,
+ "poweredBy": {
+ "label": "Powered by F0rty2.ai",
+ "url": "https://f0rty2.ai"
+ }
+ },
+ "sidebar": {
+ "title": "Helix Engage",
+ "subtitle": "Global Hospital · Call Center Agent"
+ },
+ "ai": {
+ "quickActions": [
+ { "label": "Doctor availability", "prompt": "What doctors are available?" },
+ { "label": "Clinic timings", "prompt": "What are the clinic timings?" },
+ { "label": "Patient history", "prompt": "Summarize this patient's history" },
+ { "label": "Treatment packages", "prompt": "What packages are available?" }
+ ]
+ }
+}
+```
+
+---
+
+## Sidecar Implementation
+
+### Endpoints
+
+```
+GET /api/config/theme — Returns theme JSON (no auth, public — needed before login)
+PUT /api/config/theme — Updates theme JSON (auth required, admin only)
+POST /api/config/theme/reset — Resets to default theme (auth required, admin only)
+```
+
+- Stored in `data/theme.json` on the sidecar filesystem
+- Cached in memory, invalidated on PUT
+- If file doesn't exist, returns a hardcoded default (Global Hospital theme)
+- PUT validates the JSON schema before saving
+- PUT also writes a timestamped backup to `data/theme-backups/`
+
+### Files
+
+- `helix-engage-server/src/config/theme.controller.ts` — REST endpoints
+- `helix-engage-server/src/config/theme.service.ts` — read/write/validate/backup logic
+
+---
+
+## Frontend Implementation
+
+### ThemeTokenProvider
+
+New provider wrapping the app in `main.tsx`. Responsibilities:
+
+1. **Fetch** `GET /api/config/theme` on mount (before rendering anything)
+2. **Inject CSS variables** on `document.documentElement.style`:
+ - `--color-brand-25` through `--color-brand-950` (overrides the Untitled UI brand scale)
+ - `--font-body`, `--font-display` (overrides typography)
+3. **Store content tokens** in React context (brand name, logo, login text, sidebar text, quick actions)
+4. **Expose** `useThemeTokens()` hook for components to read content tokens
+
+### File: `src/providers/theme-token-provider.tsx`
+
+```tsx
+type ThemeTokens = {
+ brand: { name: string; hospitalName: string; logo: string; favicon: string };
+ login: { title: string; subtitle: string; showGoogleSignIn: boolean; showForgotPassword: boolean; poweredBy: { label: string; url: string } };
+ sidebar: { title: string; subtitle: string };
+ ai: { quickActions: Array<{ label: string; prompt: string }> };
+};
+```
+
+### CSS Variable Injection
+
+The provider maps `colors.brand.*` to CSS custom properties that Untitled UI already reads:
+
+```
+theme.colors.brand["500"] → document.documentElement.style.setProperty('--color-brand-500', value)
+```
+
+Since `theme.css` defines `--color-brand-500: var(--color-blue-500)`, setting `--color-brand-500` directly on `` overrides the alias with higher specificity.
+
+Typography:
+```
+theme.typography.body → --font-body
+theme.typography.display → --font-display
+```
+
+### Consumers
+
+Components that currently hardcode hospital-specific content:
+
+| Component | Current hardcoded value | Token path |
+|---|---|---|
+| `login.tsx` line 93 | "Sign in to Helix Engage" | `login.title` |
+| `login.tsx` line 94 | "Global Hospital" | `login.subtitle` |
+| `login.tsx` line 92 | `/helix-logo.png` | `brand.logo` |
+| `login.tsx` line 181 | "Powered by F0rty2.ai" | `login.poweredBy.label` |
+| `sidebar.tsx` | "Helix Engage" | `sidebar.title` |
+| `sidebar.tsx` | "Global Hospital · Call Center Agent" | `sidebar.subtitle` |
+| `ai-chat-panel.tsx` lines 21-25 | Quick action prompts | `ai.quickActions` |
+| `app-shell.tsx` | favicon | `brand.favicon` |
+
+---
+
+## Default Theme
+
+If the sidecar returns no theme (endpoint down, file missing), the frontend uses a hardcoded default matching the current Global Hospital branding. This ensures the app works without a sidecar theme endpoint.
+
+---
+
+## Settings UI (Supervisor)
+
+New tab in the Settings page: **Branding**. Visible only to admin role.
+
+### Sections
+
+**1. Brand Identity**
+- Hospital name (text input)
+- App name (text input)
+- Logo upload (file input → stores URL)
+- Favicon upload
+
+**2. Brand Colors**
+- 12 color swatches (25 through 950) with hex/rgb input per swatch
+- Live preview strip showing the full scale
+- "Reset to default" button per section
+
+**3. Typography**
+- Body font family (text input with common font suggestions)
+- Display font family (text input)
+
+**4. Login Page**
+- Title text
+- Subtitle text
+- Show Google sign-in (toggle)
+- Show forgot password (toggle)
+- Powered-by label + URL
+
+**5. Sidebar**
+- Title text
+- Subtitle template (supports `{role}` placeholder — "Global Hospital · {role}")
+
+**6. AI Quick Actions**
+- Editable list of label + prompt pairs
+- Add / remove / reorder
+
+### Save Flow
+- Supervisor edits fields → clicks Save → `PUT /api/config/theme` → sidecar validates + saves + backs up
+- Frontend re-fetches theme on save → CSS variables update → page reflects changes immediately (no reload needed)
+
+### File
+`src/pages/settings.tsx` — new "Branding" tab (or `src/pages/branding-settings.tsx` if settings page is already complex)
+
+---
+
+## What This Does NOT Change
+
+- **Tailwind classes** — no changes. Components continue using `text-brand-secondary`, `bg-brand-solid`, etc. The CSS variables they reference are overridden at runtime.
+- **Component structure** — no layout changes. Only content strings and colors change.
+- **Untitled UI theme.css** — not modified. The provider overrides are applied inline on ``, higher specificity.
+
+---
+
+## Scope
+
+**In scope:**
+- Sidecar theme endpoint + JSON file
+- ThemeTokenProvider + useThemeTokens hook
+- Login page consuming tokens
+- Sidebar consuming tokens
+- AI quick actions consuming tokens
+- Brand color override via CSS variables
+- Typography override via CSS variables
+
+**Out of scope:**
+- Dark mode customization (inherits from Untitled UI)
+- Per-role theming
+- Logo upload to cloud storage (uses URL for now — can be a data URI or hosted path)
diff --git a/src/components/call-desk/ai-chat-panel.tsx b/src/components/call-desk/ai-chat-panel.tsx
index 9e76b92..6d73315 100644
--- a/src/components/call-desk/ai-chat-panel.tsx
+++ b/src/components/call-desk/ai-chat-panel.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { useRef, useEffect } from 'react';
+import { useThemeTokens } from '@/providers/theme-token-provider';
import { useChat } from '@ai-sdk/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPaperPlaneTop, faSparkles, faUserHeadset } from '@fortawesome/pro-duotone-svg-icons';
@@ -18,14 +19,9 @@ interface AiChatPanelProps {
onChatStart?: () => void;
}
-const QUICK_ACTIONS = [
- { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' },
- { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' },
- { label: 'Patient history', prompt: 'Can you summarize this patient\'s history?' },
- { label: 'Treatment packages', prompt: 'What treatment packages are available?' },
-];
-
export const AiChatPanel = ({ callerContext, onChatStart }: AiChatPanelProps) => {
+ const { tokens } = useThemeTokens();
+ const quickActions = tokens.ai.quickActions;
const messagesEndRef = useRef(null);
const chatStartedRef = useRef(false);
@@ -67,7 +63,7 @@ export const AiChatPanel = ({ callerContext, onChatStart }: AiChatPanelProps) =>
Ask me about doctors, clinics, packages, or patient info.
- {QUICK_ACTIONS.map((action) => (
+ {quickActions.map((action) => (
diff --git a/src/pages/team-performance.tsx b/src/pages/team-performance.tsx
index aa618e3..95e0af4 100644
--- a/src/pages/team-performance.tsx
+++ b/src/pages/team-performance.tsx
@@ -64,7 +64,7 @@ const DateFilter = ({ value, onChange }: { value: DateRange; onChange: (v: DateR
);
const KpiCard = ({ icon, value, label, color }: { icon: any; value: string | number; label: string; color?: string }) => (
-
+
@@ -210,8 +210,8 @@ export const TeamPerformancePage = () => {
const days = Object.keys(dayMap);
return {
tooltip: { trigger: 'axis' },
- legend: { data: ['Inbound', 'Outbound'], bottom: 0 },
- grid: { top: 10, right: 10, bottom: 30, left: 40 },
+ legend: { data: ['Inbound', 'Outbound'], bottom: 0, textStyle: { fontSize: 11 } },
+ grid: { top: 10, right: 10, bottom: 50, left: 40 },
xAxis: { type: 'category', data: days },
yAxis: { type: 'value' },
series: [
@@ -292,7 +292,7 @@ export const TeamPerformancePage = () => {
Key Metrics
-
+
diff --git a/src/providers/theme-token-provider.tsx b/src/providers/theme-token-provider.tsx
new file mode 100644
index 0000000..a255f18
--- /dev/null
+++ b/src/providers/theme-token-provider.tsx
@@ -0,0 +1,87 @@
+import type { ReactNode } from 'react';
+import { createContext, useContext, useEffect, useState, useCallback } from 'react';
+
+const THEME_API_URL = import.meta.env.VITE_THEME_API_URL ?? import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
+
+export type ThemeTokens = {
+ brand: { name: string; hospitalName: string; logo: string; favicon: string };
+ colors: { brand: Record };
+ typography: { body: string; display: string };
+ login: { title: string; subtitle: string; showGoogleSignIn: boolean; showForgotPassword: boolean; poweredBy: { label: string; url: string } };
+ sidebar: { title: string; subtitle: string };
+ ai: { quickActions: Array<{ label: string; prompt: string }> };
+};
+
+const DEFAULT_TOKENS: ThemeTokens = {
+ brand: { name: 'Helix Engage', hospitalName: 'Global Hospital', logo: '/helix-logo.png', favicon: '/favicon.ico' },
+ colors: { brand: {
+ '25': 'rgb(239 246 255)', '50': 'rgb(219 234 254)', '100': 'rgb(191 219 254)',
+ '200': 'rgb(147 197 253)', '300': 'rgb(96 165 250)', '400': 'rgb(59 130 246)',
+ '500': 'rgb(37 99 235)', '600': 'rgb(29 78 216)', '700': 'rgb(30 64 175)',
+ '800': 'rgb(30 58 138)', '900': 'rgb(23 37 84)', '950': 'rgb(15 23 42)',
+ } },
+ typography: {
+ body: "'Satoshi', 'Inter', -apple-system, sans-serif",
+ display: "'General Sans', 'Inter', -apple-system, sans-serif",
+ },
+ login: { title: 'Sign in to Helix Engage', subtitle: 'Global Hospital', showGoogleSignIn: true, showForgotPassword: true, poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' } },
+ sidebar: { title: 'Helix Engage', subtitle: 'Global Hospital \u00b7 {role}' },
+ ai: { quickActions: [
+ { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' },
+ { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' },
+ { label: 'Patient history', prompt: "Can you summarize this patient's history?" },
+ { label: 'Treatment packages', prompt: 'What treatment packages are available?' },
+ ] },
+};
+
+type ThemeTokenContextType = {
+ tokens: ThemeTokens;
+ refresh: () => Promise;
+};
+
+const ThemeTokenContext = createContext({ tokens: DEFAULT_TOKENS, refresh: async () => {} });
+
+export const useThemeTokens = () => useContext(ThemeTokenContext);
+
+const applyColorTokens = (brandColors: Record) => {
+ const root = document.documentElement;
+ for (const [stop, value] of Object.entries(brandColors)) {
+ root.style.setProperty(`--color-brand-${stop}`, value);
+ }
+};
+
+const applyTypographyTokens = (typography: { body: string; display: string }) => {
+ const root = document.documentElement;
+ if (typography.body) root.style.setProperty('--font-body', typography.body);
+ if (typography.display) root.style.setProperty('--font-display', typography.display);
+};
+
+export const ThemeTokenProvider = ({ children }: { children: ReactNode }) => {
+ const [tokens, setTokens] = useState(DEFAULT_TOKENS);
+
+ const fetchTheme = useCallback(async () => {
+ try {
+ const res = await fetch(`${THEME_API_URL}/api/config/theme`);
+ if (res.ok) {
+ const data: ThemeTokens = await res.json();
+ setTokens(data);
+ if (data.colors?.brand && Object.keys(data.colors.brand).length > 0) {
+ applyColorTokens(data.colors.brand);
+ }
+ if (data.typography) {
+ applyTypographyTokens(data.typography);
+ }
+ }
+ } catch {
+ // Use defaults silently
+ }
+ }, []);
+
+ useEffect(() => { fetchTheme(); }, [fetchTheme]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/styles/theme.css b/src/styles/theme.css
index 84f7996..ab86481 100644
--- a/src/styles/theme.css
+++ b/src/styles/theme.css
@@ -764,9 +764,9 @@
/* SIDEBAR-SPECIFIC COLORS (Light Mode Only) */
--color-sidebar-bg: rgb(28, 33, 44);
--color-sidebar-nav-item-hover-bg: rgb(42, 48, 60);
- --color-sidebar-nav-item-hover-text: rgb(73, 160, 225);
+ --color-sidebar-nav-item-hover-text: var(--color-brand-400);
--color-sidebar-nav-item-active-bg: rgb(42, 48, 60);
- --color-sidebar-nav-item-active-text: rgb(73, 160, 225);
+ --color-sidebar-nav-item-active-text: var(--color-brand-400);
/* COMPONENT COLORS */
--color-app-store-badge-border: rgb(166 166 166);