Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Version: 1.4.1
Last Updated: 2026-04-18
Status: Active
Target Audience: Developers
Overview
Use semantic color tokens from the design system (src/index.css with Tailwind v4 @theme inline mappings); avoid hardcoded Tailwind palette classes (green-*, red-*, etc.) except where documented exceptions apply.
Canonical token governance and sync workflow are defined in DESIGN_TOKEN_SYNC_POLICY.md. When this document and runtime values diverge, follow the canonical-source and synchronization process in that policy.
What changed in v1.4.1 (2026-04-18): Chart Palette § “Colorblind safety” updated with the first formal audit results from npm run audit:chart-palette-colorblind (Brettel-Viénot-Mollon simulation + CIE76 ΔE in CIELAB). Audit reveals 9 fail / 25 warn pairs across both modes × 3 dichromacy profiles — see the new Known limitations & action items subsection. The mandatory rule “always pair color with shape/pattern/label” is now load-bearing for accessibility, not just defensive practice.
What changed in v1.4.0 (2026-04-18): Added the Token Catalog Reference (full surface/role/status/special/sidebar inventory), a Module Identity Tokens section (12 active short codes + reserved + deprecated aliases), an expanded Chart Palette section, a Tenant Theming (PF-95) Mapping Table that documents the gaps between platform tokens and tenant-customizable colors, and a Compliance Theming (PF-91-EN-01) Mapping section. Includes a regression note for the previous info.full dark-mode WCAG defect (now fixed).
Typography Token Contract
Typography parity follows the canonical design-system contract and app runtime mapping:
- Canonical tokens:
--font-display: Space Grotesk
--font-body: Inter
--font-mono: DM Mono (code, monospace data)
- Runtime mappings:
font-sans → var(--tenant-font-family, var(--font-body))
font-heading → var(--tenant-font-heading, var(--font-display))
Tenant-specific typography remains supported via PF-95 variables:
--tenant-font-family overrides body/sans
--tenant-font-heading overrides heading/display
Standard Mappings
| Meaning | Hardcoded (DON’T USE) | Semantic Token |
|---|
| Success/Approved/Active | green-*, emerald-* | success, text-success, bg-success/10 |
| Warning/Pending/Caution | yellow-*, amber-*, orange-* | warning, text-warning, bg-warning/10 |
| Error/Rejected/Danger | red-* | destructive, text-destructive, bg-destructive/10 |
| Info/In Progress | blue-* | info, text-info, bg-info/10 |
| Neutral/Inactive | gray-*, slate-* | muted, text-muted-foreground, bg-muted |
Token note (PF-95 update, 2026-04): --color-secondary now maps to hsl(var(--secondary)) (shadcn-canonical) so the tenant bridge --tenant-secondary-hsl actually drives bg-secondary / text-secondary. For the elevated card surface, use bg-card-elevated directly. --color-input resolves to --input, which in :root shares --tenant-border-hsl with --border per shadcn convention — set the tenant border once and inputs follow. See DESIGN_TOKEN_SYNC_POLICY.md.
Badge Variants
Status-style badges must use Badge variants or StatusBadge (with statusConfig) from @/shared/components; do not use raw color classes. Use these Badge component variants from @/shared/ui/badge:
| Variant | Usage | Example |
|---|
success | Positive states (active, approved, complete) | <Badge variant="success">Active</Badge> |
warning | Caution states (pending, at-risk, expiring) | <Badge variant="warning">Pending</Badge> |
destructive | Error/danger states (rejected, error, critical) | <Badge variant="destructive">Error</Badge> |
info | Informational states (in progress, new) | <Badge variant="info">In Progress</Badge> |
secondary | Neutral/inactive states | <Badge variant="secondary">Inactive</Badge> |
outline | Border only, subtle emphasis | <Badge variant="outline">Default</Badge> |
Note: Badge info uses bg-info/10 text-info — a distinct informational blue. For primary-branded soft emphasis, use Button variant soft or primary-light Badge variant instead.
Subtle / Light Badge Variants
For low-emphasis or inline status (e.g. inside tables, alongside solid badges), use the *-light Badge variants from @/shared/ui/badge:
| Variant | Usage |
|---|
success-light | Subtle success (e.g. inline “Active” in a table cell) |
warning-light | Subtle warning (e.g. “Expiring soon” without competing with primary content) |
destructive-light | Subtle error/danger (e.g. “Overdue” in a list) |
primary-light | Subtle primary/info (e.g. “New” or “In progress” inline) |
Use solid variants (success, warning, destructive, info) for standalone status badges (cards, headers, filters). Use -light when the badge should not compete visually with surrounding content.
Specialty Badge Variants
Additional Badge variants exist for non-status use cases:
| Variant | Usage |
|---|
accentGlow | Accent background with glow shadow effect (highlights, featured items) |
glass | Glass-morphism style (overlays, floating UI) |
ai | Gradient background for AI-powered features |
These are not for status indication — use the semantic/light variants above for statuses.
Status Badge Pattern
For domain statuses (e.g. invoice status, referral status, run status), map each status to a Badge variant and label, then render with the shared Badge or the shared StatusBadge component:
- Define a config:
statusConfig: Record<Status, { label: string; variant: BadgeVariant; icon?: LucideIcon }>.
- Render:
<Badge variant={config.variant}>{config.label}</Badge> or use <StatusBadge status={status} statusConfig={statusConfig} /> from @/shared/components/StatusBadge.
- Variant mapping: Use the canonical status-to-variant map in
@/shared/lib/status-variants: CANONICAL_STATUS_VARIANTS and getCanonicalVariant(status). New status badges SHOULD use these canonical mappings; existing badges will be migrated incrementally. Fallback semantic guidance: success (active, approved, completed), warning (pending, in progress, expiring), destructive (rejected, error, cancelled, failed), info (new, submitted), secondary (draft, inactive, closed).
See src/shared/components/StatusBadge.tsx for the shared component API and src/shared/lib/status-variants.ts for the canonical mapping.
When to Use Semantic vs Categorical Colors
Use Semantic Colors For:
- ✅ Status indicators (active, pending, error, complete)
- ✅ Validation feedback (valid, invalid, warning)
- ✅ Health indicators (healthy, at-risk, critical)
- ✅ Action feedback (success, failure)
- ✅ Priority levels (critical, high, medium, low)
Keep Categorical Colors For:
- 🎨 Data type indicators (string=blue, number=green, boolean=purple)
- 🎨 Core/module identifiers (hr=blue, rh=green, fa=amber)
- 🎨 Category distinctions that don’t imply status
- 🎨 Brand colors that must remain consistent
Utility Functions
Import from @/shared/lib/semantic-colors:
import { getStatusColors, type SemanticStatus } from '@/shared/lib/semantic-colors';
// Get all color classes for a status
const colors = getStatusColors('success');
// Returns: { bg, bgSubtle, text, border, full }
// Example usage
<div className={colors.bgSubtle}>
<span className={colors.text}>Success message</span>
</div>
Examples
Before (DON’T DO THIS)
// ❌ Hardcoded colors - violates design system
<Badge className="bg-green-100 text-green-800">Active</Badge>
<Badge className="bg-yellow-100 text-yellow-800">Pending</Badge>
<span className="text-red-600">Error message</span>
<div className="bg-blue-500/10 text-blue-700">Info</div>
After (DO THIS)
// ✅ Using semantic tokens and Badge variants
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<span className="text-destructive">Error message</span>
<div className="bg-info/10 text-info">Info</div>
Status Configuration Pattern
// ✅ Use variant mappings instead of className mappings
const statusVariants: Record<Status, BadgeVariant> = {
active: 'success',
pending: 'warning',
error: 'destructive',
draft: 'secondary',
};
// Usage
<Badge variant={statusVariants[status]}>{status}</Badge>
Icon Colors
// ❌ Wrong
<CheckCircle className="h-4 w-4 text-green-600" />
<AlertTriangle className="h-4 w-4 text-yellow-600" />
<XCircle className="h-4 w-4 text-red-600" />
// ✅ Correct
<CheckCircle className="h-4 w-4 text-success" />
<AlertTriangle className="h-4 w-4 text-warning" />
<XCircle className="h-4 w-4 text-destructive" />
Alert/Notification Styling
// ❌ Wrong
<Alert className="border-green-200 bg-green-50">
<CheckCircle className="h-4 w-4 text-green-600" />
</Alert>
// ✅ Correct
<Alert className="border-success/20 bg-success/10">
<CheckCircle className="h-4 w-4 text-success" />
</Alert>
Progress/Score Indicators
// ✅ Semantic color based on value
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-success';
if (score >= 50) return 'text-warning';
return 'text-destructive';
};
// ✅ Progress bar backgrounds
const getProgressBg = (percent: number) => {
if (percent >= 75) return 'bg-success';
if (percent >= 50) return 'bg-primary';
if (percent >= 25) return 'bg-warning';
return 'bg-destructive';
};
Dark Mode Considerations
When using semantic tokens, dark mode is handled automatically:
text-success → Works in both light and dark mode
bg-success/10 → Proper opacity in both modes
border-success/20 → Consistent borders
Avoid manual dark mode overrides like:
// ❌ Don't do this
className="bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400"
// ✅ Do this instead
className="bg-success/10 text-success"
Common Patterns Reference
| Component Type | Semantic Approach |
|---|
| Status Badge | Use variant prop |
| Icon color | Use text-{semantic} |
| Background highlight | Use bg-{semantic}/10 |
| Border accent | Use border-{semantic}/20 |
| Alert styling | Use bg-{semantic}/10 border-{semantic}/20 |
| Trend indicators | Up = text-success, Down = text-destructive |
| Form validation | Error = text-destructive, Warning = text-warning |
Chart Colors
Charts (Recharts, etc.) should use the chart palette for data series so they respect light/dark theme and stay consistent.
- CSS variables:
--chart-1 through --chart-8 are defined in src/index.css (light and dark).
- Tailwind:
chart.1 … chart.8 are mapped in src/index.css via Tailwind v4 @theme inline; use text-chart-1, bg-chart-2, etc. for UI elements that reference chart colors (e.g. legend labels).
- In chart config: Use
hsl(var(--chart-1)), … hsl(var(--chart-8)) for strokes/fills, or pass a ChartContainer config with theme: { light: 'hsl(var(--chart-n))', dark: '...' } so series colors are theme-aware.
- Do not use arbitrary hex or raw HSL in chart configs; use the design tokens above so charts adapt to dark mode.
Audit Checklist
Before committing, verify:
Domain-Specific Status Mappings
Document Statuses
| Status | Semantic |
|---|
published, approved | success |
draft, pending_review | warning |
archived | muted |
rejected | destructive |
Credential Statuses
| Status | Semantic |
|---|
active, verified | success |
expiring_soon, pending_verification | warning |
expired, revoked | destructive |
inactive | muted |
Workflow Statuses
| Status | Semantic |
|---|
completed, approved | success |
running, pending, waiting | warning |
failed, rejected, cancelled | destructive |
draft, paused | muted |
| Status | Semantic |
|---|
submitted, approved | success |
in_progress, pending_review | warning |
rejected, expired | destructive |
draft | muted |
Real-World Usage Examples
Employee Status Card
// ✅ CORRECT: Using semantic tokens
function EmployeeStatusCard({ status }: { status: string }) {
const statusConfig = {
active: { variant: 'success' as const, icon: CheckCircle, label: 'Active' },
pending: { variant: 'warning' as const, icon: Clock, label: 'Pending' },
inactive: { variant: 'muted' as const, icon: UserX, label: 'Inactive' },
terminated: { variant: 'destructive' as const, icon: XCircle, label: 'Terminated' },
};
const config = statusConfig[status] || statusConfig.inactive;
const Icon = config.icon;
return (
<Card className="border-success/20 bg-success/5">
<CardContent className="flex items-center gap-3 p-4">
<Icon className="h-5 w-5 text-success" />
<span className="font-medium">{config.label}</span>
</CardContent>
</Card>
);
}
// ✅ CORRECT: Semantic colors for validation
function FormField({ error, warning }: { error?: string; warning?: string }) {
return (
<div>
<Input />
{error && (
<p className="text-sm text-destructive mt-1 flex items-center gap-1">
<AlertCircle size={14} />
{error}
</p>
)}
{warning && (
<p className="text-sm text-warning mt-1 flex items-center gap-1">
<AlertTriangle size={14} />
{warning}
</p>
)}
</div>
);
}
Dashboard Metrics
// ✅ CORRECT: Semantic colors for trends
function MetricCard({ value, trend }: { value: number; trend: 'up' | 'down' | 'neutral' }) {
const trendConfig = {
up: { color: 'text-success', icon: TrendingUp },
down: { color: 'text-destructive', icon: TrendingDown },
neutral: { color: 'text-muted-foreground', icon: Minus },
};
const config = trendConfig[trend];
const Icon = config.icon;
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<span className="text-2xl font-bold">{value}</span>
<Icon className={`h-5 w-5 ${config.color}`} />
</div>
</CardContent>
</Card>
);
}
Alert Notifications
// ✅ CORRECT: Semantic alert styling
function AlertNotification({ type, message }: { type: 'success' | 'warning' | 'error'; message: string }) {
const config = {
success: { bg: 'bg-success/10', border: 'border-success/20', icon: CheckCircle, text: 'text-success' },
warning: { bg: 'bg-warning/10', border: 'border-warning/20', icon: AlertTriangle, text: 'text-warning' },
error: { bg: 'bg-destructive/10', border: 'border-destructive/20', icon: XCircle, text: 'text-destructive' },
};
const { bg, border, icon: Icon, text } = config[type];
return (
<Alert className={`${bg} ${border}`}>
<Icon className={`h-4 w-4 ${text}`} />
<AlertDescription className={text}>{message}</AlertDescription>
</Alert>
);
}
Table Row Status
// ✅ CORRECT: Status indicators in tables
function StatusCell({ status }: { status: string }) {
const statusMap: Record<string, { variant: BadgeVariant; label: string }> = {
approved: { variant: 'success', label: 'Approved' },
pending: { variant: 'warning', label: 'Pending' },
rejected: { variant: 'destructive', label: 'Rejected' },
draft: { variant: 'secondary', label: 'Draft' },
};
const config = statusMap[status] || statusMap.draft;
return <Badge variant={config.variant}>{config.label}</Badge>;
}
Token Catalog Reference
The canonical token list lives in src/index.css (:root + .dark declare HSL values; the @theme inline block exposes them as Tailwind v4 utilities). Use this section to look up which token to reach for; consult src/index.css for the exact HSL values.
Surfaces & roles (chrome)
| Token | Tailwind utility (examples) | Use |
|---|
--background | bg-background, text-background | Default page background |
--foreground (= --text-main) | text-foreground | Default body text |
--card | bg-card, text-card-foreground | Card surface |
--card-elevated | bg-card-elevated | Elevated card surface (slightly distinct from --card). Note: as of 2026-04 bg-secondary no longer aliases here — it resolves to --secondary (shadcn-canonical). Use bg-card-elevated explicitly when you want the elevated surface. |
--popover | bg-popover, text-popover-foreground | Popover/dropdown/menu backgrounds |
--muted | bg-muted, text-muted | De-emphasized chrome (cool gray light, darker slate dark) |
--muted-foreground | text-muted-foreground | Muted body text — the canonical “subtle text” utility |
--text-secondary | text-text-secondary | Mid-emphasis body text (between foreground and muted-foreground) |
--text-muted | text-text-muted | Lower-emphasis body text. Note: text-muted-foreground is preferred for component code |
--border | border-border, border | Default border color |
--input | border-input, bg-input | Form input border. Shares the --tenant-border-hsl triplet with --border per shadcn convention so a tenant border override flows to inputs automatically. |
--input-background | bg-input-background | Form input fill |
--switch-background | bg-switch-background | Switch off-state track |
--ring | ring-ring, focus-visible:ring-ring | Focus ring color |
Brand & action
| Token | Tailwind utility | Use |
|---|
--primary | bg-primary, text-primary | Brand primary (deep navy) — chrome accents, sidebar active |
--primary-hover | hover:bg-primary-hover | Primary hover state |
--primary-foreground (--primary-fg) | text-primary-foreground | Foreground on bg-primary |
--accent | bg-accent, text-accent | Brand accent (teal) — primary CTA color via Button variant="accent" |
--accent-hover | hover:bg-accent-hover | Accent hover state |
--accent-foreground (--accent-fg) | text-accent-foreground | Foreground on bg-accent |
--secondary (raw) | (consumed by PF-95 only) | Raw HSL value used by pf_tenant_themes.color_secondary. Note: the Tailwind utility bg-secondary resolves to --card-elevated (alias) — see “Token Aliases” in DESIGN_TOKEN_SYNC_POLICY.md. |
--secondary-foreground | text-secondary-foreground | Foreground on bg-secondary / bg-card-elevated |
Status
| Token | Subtle | Foreground | Notes |
|---|
--success | --success-subtle | --success-foreground | Active / approved / completed |
--warning | --warning-subtle | --warning-foreground | Pending / at-risk / expiring |
--info | --info-subtle | --info-foreground | In progress / informational |
--destructive | (use bg-destructive/10) | --destructive-foreground | Error / delete / critical |
--destructive-hover | — | — | Destructive button hover |
Use the Tailwind utilities bg-success, bg-success-subtle, text-success, text-success-foreground, border-success/20, etc. The *-subtle tokens are pre-tinted backgrounds calibrated for both light and dark mode — prefer them over bg-{token}/10 when you want a consistent, mode-aware tint.
Regression note: Prior to 2026-04-18, semanticColorClasses.info.full paired bg-info with text-primary-foreground. In dark mode --info flips to a light blue and --primary-foreground is also light → light-on-light, WCAG AA fail. Now uses text-info-foreground. Cover with the Vitest in tests/unit/shared/lib/semantic-colors.test.ts.
Special & utility
| Token | Tailwind utility | Use |
|---|
--overlay-scrim | bg-[hsl(var(--overlay-scrim))] | Modal/sheet/drawer backdrops (replaces ad-hoc bg-black/50) |
--shimmer-highlight | bg-[hsl(var(--shimmer-highlight))] | Skeleton/loader shimmer band (replaces ad-hoc via-white/20) |
--ai-accent | bg-[hsl(var(--ai-accent))], text-ai-accent | AI-feature accent (purple) — used by Badge variant="ai", accent-glow |
--glass-bg / --glass-border / --glow-opacity | .glass, .glass-card, .glass-frosted, etc. | Glassmorphism utilities (built into @layer utilities) |
--focus-ring-offset-color | (consumed by component CSS) | Outline offset color for high-contrast focus rings |
Density, Elevation & Encore v2
The .theme-encore-v2 class is an additive opt-in layer for visual rhythm and depth. It does not rename existing tokens, introduce PF-95 schema fields, or hardcode tenant colors; color-bearing surfaces still resolve through the semantic/PF-95 bridge above.
| Token | Use |
|---|
--density-card-padding, --density-card-padding-lg | Standard card padding for shared metric/list primitives |
--density-control-height, --density-touch-target | Control height/touch target floor; keep interactive targets at least 44px |
--elevation-card, --elevation-card-hover, --elevation-popover | Depth vocabulary for shared cards/popovers using semantic foreground alpha |
Current pilot consumers: StatCard and ListFilterBar. Prefer these tokens for new shared primitives instead of repeating p-4 sm:p-6, h-9, or bespoke shadow classes.
The platform sidebar has its own token slot so it can be themed independently of the main app shell.
| Token | Use |
|---|
--sidebar-background | Sidebar surface |
--sidebar-foreground | Sidebar text |
--sidebar-primary / --sidebar-primary-foreground | Active sidebar item |
--sidebar-accent / --sidebar-accent-foreground | Hover/secondary sidebar item |
--sidebar-border | Sidebar dividers |
--sidebar-ring | Sidebar focus ring |
Encore brand (reserved)
| Token | Use |
|---|
--encore-navy, --encore-slate, --encore-teal, --encore-teal-light, --encore-teal-mid, --encore-gray, --encore-light, --encore-gold | Reserved for marketing/brand surfaces only (login chrome, brand splash, PDF letterheads, marketing emails). Do not use in product UI without DS owner approval; product UI uses the semantic tokens above so PF-95 tenant theming overrides take effect. See DESIGN_TOKEN_SYNC_POLICY.md. |
Module Identity Tokens
Each domain core has a dedicated identity color used for navigation accents, dashboard widget headers, and badge tinting. Tokens follow the pattern --module-{short-code}, --module-{short-code}-hover, --module-{short-code}-subtle.
Active short codes
| Short code | Module | Tailwind utility example |
|---|
pf | Platform Foundation | bg-module-pf, bg-module-pf-subtle, border-module-pf |
cl | Clinical | bg-module-cl, bg-module-cl-subtle, border-module-cl |
pm | Practice Management | bg-module-pm, bg-module-pm-subtle, border-module-pm |
fm | Fleet Management | bg-module-fm, bg-module-fm-subtle, border-module-fm |
rh | Recovery Housing | bg-module-rh, bg-module-rh-subtle, border-module-rh |
it | IT & Security | bg-module-it, bg-module-it-subtle, border-module-it |
hr | Human Resources | bg-module-hr, bg-module-hr-subtle, border-module-hr |
lo | Learning & Organizational | bg-module-lo, bg-module-lo-subtle, border-module-lo |
ce | Community Engagement | bg-module-ce, bg-module-ce-subtle, border-module-ce |
fa | Finance & Accounting | bg-module-fa, bg-module-fa-subtle, border-module-fa |
gr | Governance & Risk | bg-module-gr, bg-module-gr-subtle, border-module-gr |
fw | Forms & Workflow | bg-module-fw, bg-module-fw-subtle, border-module-fw |
Reserved (do not use yet)
| Short code | Status | Notes |
|---|
th | Reserved | Pre-allocated for a future module; declared in src/index.css but no consumer yet |
qo | Reserved | Same |
da | Reserved | Same |
rpt | Reserved | Same |
Do not consume these in product UI without DS owner approval. See DESIGN_TOKEN_SYNC_POLICY.md.
Deprecated legacy aliases
| Alias | Use instead | Notes |
|---|
--module-clinical* | --module-cl* | Alias-only, no consumers in src/; will be removed in next DS major |
--module-finance* | --module-fa* | Same |
--module-operations* | --module-fw* | Same |
--module-people* | --module-hr* | Same |
Recommended usage
- Sidebar / nav row accent: 4px left border using
border-l-4 border-module-{id} for the active module. Reinforces a “color = module” mental model across the 12-core ERP.
- Dashboard widget header underline:
border-b border-module-{id} so each module’s hub feels visually anchored.
- Module badges:
bg-module-{id}-subtle text-module-{id} for tags that identify which module owns an entity (e.g. cross-cutting reports list).
- Do NOT use module colors for status/state — use semantic status tokens (
success, warning, destructive, info) for that.
Chart Palette
Charts (Recharts, SVG, custom) MUST use the chart token palette so series colors stay consistent across modules and adapt to dark mode.
Tokens
| Token | Tailwind utility | Approx. light hue | Approx. dark hue |
|---|
--chart-1 | text-chart-1, bg-chart-1, fill-chart-1, stroke-chart-1 | Sky blue 49% | Sky blue 55% |
--chart-2 | text-chart-2, bg-chart-2, … | Teal 40% | Teal 50% |
--chart-3 | text-chart-3, … | Violet 66% | Violet 72% |
--chart-4 | text-chart-4, … | Amber 50% | Amber 55% |
--chart-5 | text-chart-5, … | Pink 60% | Pink 66% |
--chart-6 | text-chart-6, … | Emerald 39% | Emerald 45% |
--chart-7 | text-chart-7, … | Orange 53% | Orange 58% |
--chart-8 | text-chart-8, … | Indigo 67% | Indigo 72% |
Mandatory rules
- Always use
hsl(var(--chart-N)) in chart configs (Recharts stroke / fill, SVG <rect fill={...}>, etc.). Never hardcode hex literals or raw HSL strings — they break dark mode and tenant rebrand.
- Use Tailwind utilities (
text-chart-1, bg-chart-2, …) for legend swatches, KPI tile accents, etc.
- Cap series at 8 categories before introducing a “(other)” bucket. Beyond 8, sequential blending or repetition produces unreadable charts.
- Pair with
ChartContainer from shadcn-charts (src/shared/ui/chart.tsx) when possible; pass theme: { light: 'hsl(var(--chart-N))', dark: '...' } so series colors remain theme-aware.
Colorblind safety
Audit status: First formal audit completed 2026-04-18 via npm run audit:chart-palette-colorblind. Method: Brettel-Viénot-Mollon (single-projection) simulation under deuteranopia, protanopia, and tritanopia, with pairwise CIE76 ΔE in CIELAB across all 28 category pairs.
Result: ⚠ The current palette is not safe for color-only category encoding for colorblind users. 9 fail pairs (ΔE < 10) and 25 warn pairs (ΔE 10–25) across both modes × 3 profiles. Full report: reports/audits/chart-palette-colorblind-audit.json.
Mandatory rule: Charts MUST pair the chart palette color with at least one of: a category label, a marker shape, a pattern fill, or a series-name annotation. Color is for recognition, not for category identity. Never rely on color alone to distinguish data series.
Known problem pairs
| Pair | Light min ΔE | Worst profile | Notes |
|---|
chart-2 (teal) ↔ chart-6 (emerald) | 3.8 | tritanopia | Indistinguishable in tritanopia (both collapse to teal); 18.2 in deuteranopia |
chart-2 (teal) ↔ chart-5 (pink) | 5.4 | deuteranopia | Both collapse to gray |
chart-1 (sky) ↔ chart-3 (violet) | 5.8 | deuteranopia | Both collapse to mid-blue |
chart-4 (amber) ↔ chart-7 (orange) | 8.6 | deuteranopia | Both collapse to yellow-brown |
chart-1 (sky) ↔ chart-2 (teal) | 9.6 | tritanopia | Both collapse to cyan |
Action items (deferred to future palette-revision spec)
- Replace one of
chart-2 / chart-6 to break the tritanopia teal/emerald collapse (e.g. swap chart-6 to a yellow-green or olive).
- Replace one of
chart-4 / chart-7 to break the amber/orange collapse (e.g. swap chart-7 to a brown or burgundy).
- Replace one of
chart-1 / chart-3 to break the sky/violet collapse (e.g. swap chart-3 to a magenta with more red than blue).
- Re-run
npm run audit:chart-palette-colorblind --strict after each swap; goal is 0 fail pairs before wiring strict mode into validate:governance.
Why ship the audit before fixing the palette? The current palette is in production and changing series colors retrospectively would re-skin every chart in the platform. The audit + mandatory pairing rule mitigates the risk today; the swap is a coordinated visual-regression PR that needs DS owner sign-off.
Tenant Theming (PF-95) Mapping Table
PF-95 (Tenant White-Labeling) lets each org override a subset of platform colors. The mapping below documents which platform tokens are tenant-customizable today and which remain platform-controlled. The primary admin UI is BrandingSettingsPage (src/platform/theming/pages/BrandingSettingsPage.tsx), which writes to pf_tenant_themes; on load, TenantThemeProvider injects --tenant-* overrides at :root.
PF-95 field (ThemeColors) | Resolves to (runtime) | Tailwind utility | Notes |
|---|
colorPrimary | --primary | bg-primary, text-primary | |
colorSecondary | --secondary (raw HSL) | (consumed by PF-95 + custom CSS) | Known limitation: the Tailwind utility bg-secondary resolves to --card-elevated via @theme inline alias. PF-95 sets the raw --secondary value but bg-secondary may not visually update. See DESIGN_TOKEN_SYNC_POLICY.md. |
colorAccent | --accent | bg-accent, text-accent | Brand CTA color |
colorBackground | --background | bg-background | Page background |
colorSurface | --card-elevated | bg-card-elevated | PF-95 calls this “surface”; runtime calls it card-elevated |
colorText | --text-main, --foreground | text-foreground | |
colorTextMuted | --text-muted | text-text-muted (or text-muted-foreground) | The canonical Tailwind utility for de-emphasized body text is text-muted-foreground — --text-muted is the underlying value |
colorDestructive | --destructive | text-destructive, bg-destructive | |
colorSuccess | --success | text-success, bg-success | |
colorWarning | --warning | text-warning, bg-warning | |
Gaps (not tenant-customizable today)
These tokens exist at runtime but are not exposed to tenant overrides. To customize they require a PF-95 v2 expansion (separate spec):
| Token family | Examples | Why it matters |
|---|
--info / --info-subtle / --info-foreground | text-info, bg-info-subtle | Tenants cannot rebrand the informational blue |
--accent-hover, --primary-hover | hover:bg-accent-hover | Hover states keep platform default even when accent is overridden |
--module-{id}* | bg-module-cl-subtle, etc. | Tenant cannot rebrand the 12 module identity colors |
--chart-1 … --chart-8 | text-chart-1, bg-chart-2 | Tenant cannot rebrand the data-viz palette |
--sidebar-* | bg-sidebar, bg-sidebar-accent | Tenant cannot independently rebrand the sidebar shell |
--muted, --muted-foreground | bg-muted, text-muted-foreground | De-emphasized chrome stays platform-default |
What tenants always get for free
- Tenant primary / accent are mode-aware: the
--tenant-* override applies to both light and dark mode at :root; the platform handles dark-mode adaptation downstream.
- Contrast validation:
validateThemeContrast (in src/platform/theming/utils/contrast.ts) enforces WCAG 2.1 AA on text-on-background pairs and 3:1 large-text on accent/destructive at save time.
- Single active theme per org is enforced by partial unique index + DB trigger.
Compliance Theming (PF-91-EN-01) Mapping
PF-91-EN-01 (White-Label Compliance Dashboards) propagates PF-95 tenant theming to compliance surfaces (/settings/compliance/*) and edge-function exports.
- Frontend:
useComplianceTheme() (src/platform/compliance/) reads the active tenant theme and injects --tenant-* CSS custom properties on the compliance page container. useComplianceContrastValidator flags any tenant color that fails WCAG AA on the regulatory dashboard and shows a <ContrastFallbackIndicator> to admins.
- Immutable text: Apply the
compliance-text-immutable class (src/platform/compliance/compliance-immutable.css) to regulatory or legal text that must never re-skin to a tenant brand. The class forces platform-default colors via !important.
- Edge function:
_shared/compliance-contrast.ts (Deno) exports PLATFORM_DEFAULTS (HEX) which mirrors DEFAULT_THEME_COLORS (HEX) in src/platform/theming/types.ts. The generate-compliance-evidence edge function uses these for evidence-package metadata (branded: true/false, contrast_validated).
- Parity: All three sources (
src/index.css, DEFAULT_THEME_COLORS, PLATFORM_DEFAULTS) MUST resolve to the same semantic palette. The npm run audit:token-parity script (Phase 4 of the color-system review) will enforce this contract.
Audit Checklist (extended)
Before committing UI changes, verify (in addition to the original checklist above):
Token Coverage Gate
The script scripts/audit/verify-shadcn-tokens.mjs (npm: audit:shadcn-tokens,
chained into validate:governance) statically asserts that every
shadcn-canonical theming token is declared in both the :root and .dark
blocks of src/index.css. Drift fails the build with a grouped diff of which
tokens are missing in which block.
Audited tokens (33):
- Surface + foreground pairs:
background/foreground, card/card-foreground,
popover/popover-foreground, primary/primary-foreground,
secondary/secondary-foreground, muted/muted-foreground,
accent/accent-foreground, destructive/destructive-foreground
- Singles:
border, input, ring
- Chart palette:
chart-1 … chart-5
- Sidebar:
sidebar-background, sidebar-foreground, sidebar-primary,
sidebar-primary-foreground, sidebar-accent, sidebar-accent-foreground,
sidebar-border, sidebar-ring
- Radius:
radius
Project extensions (success/warning/info and their *-foreground pairs,
--encore-*, --module-*, --tenant-*) are intentionally not covered by
this gate — they’re enforced by audit:token-parity and the PF-95 mapping
table above. To extend the gate, edit REQUIRED_TOKENS in the script and add
the matching declarations to src/index.css.
constitution.md §6.7 - UI/UX Guardrails
docs/development/UI_UX_STANDARDS.md - Full UI/UX Standards
docs/development/DESIGN_TOKEN_SYNC_POLICY.md - Token governance, parity contract, deprecated/reserved tokens
src/index.css - Canonical token definitions (:root, .dark, @theme inline)
src/shared/lib/semantic-colors.ts - Utility functions
src/shared/ui/badge.tsx - Badge component with variants
src/platform/theming/ - PF-95 tenant theming (types, hooks, components)
src/platform/compliance/ - PF-91-EN-01 white-label compliance dashboards
tests/unit/shared/lib/semantic-colors.test.ts - Regression tests for semantic class set