Skip to main content

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-sansvar(--tenant-font-family, var(--font-body))
    • font-headingvar(--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

MeaningHardcoded (DON’T USE)Semantic Token
Success/Approved/Activegreen-*, emerald-*success, text-success, bg-success/10
Warning/Pending/Cautionyellow-*, amber-*, orange-*warning, text-warning, bg-warning/10
Error/Rejected/Dangerred-*destructive, text-destructive, bg-destructive/10
Info/In Progressblue-*info, text-info, bg-info/10
Neutral/Inactivegray-*, 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:
VariantUsageExample
successPositive states (active, approved, complete)<Badge variant="success">Active</Badge>
warningCaution states (pending, at-risk, expiring)<Badge variant="warning">Pending</Badge>
destructiveError/danger states (rejected, error, critical)<Badge variant="destructive">Error</Badge>
infoInformational states (in progress, new)<Badge variant="info">In Progress</Badge>
secondaryNeutral/inactive states<Badge variant="secondary">Inactive</Badge>
outlineBorder 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:
VariantUsage
success-lightSubtle success (e.g. inline “Active” in a table cell)
warning-lightSubtle warning (e.g. “Expiring soon” without competing with primary content)
destructive-lightSubtle error/danger (e.g. “Overdue” in a list)
primary-lightSubtle 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:
VariantUsage
accentGlowAccent background with glow shadow effect (highlights, featured items)
glassGlass-morphism style (overlays, floating UI)
aiGradient 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:
  1. Define a config: statusConfig: Record<Status, { label: string; variant: BadgeVariant; icon?: LucideIcon }>.
  2. Render: <Badge variant={config.variant}>{config.label}</Badge> or use <StatusBadge status={status} statusConfig={statusConfig} /> from @/shared/components/StatusBadge.
  3. 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 TypeSemantic Approach
Status BadgeUse variant prop
Icon colorUse text-{semantic}
Background highlightUse bg-{semantic}/10
Border accentUse border-{semantic}/20
Alert stylingUse bg-{semantic}/10 border-{semantic}/20
Trend indicatorsUp = text-success, Down = text-destructive
Form validationError = 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.1chart.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:
  • No direct Tailwind colors (green-*, red-*, yellow-*, etc.)
  • All status indicators use Badge variants
  • Icon colors use semantic tokens
  • Alert/notification backgrounds use semantic tokens
  • Progress indicators use semantic colors
  • Light/dark mode both work correctly

Domain-Specific Status Mappings

Document Statuses

StatusSemantic
published, approvedsuccess
draft, pending_reviewwarning
archivedmuted
rejecteddestructive

Credential Statuses

StatusSemantic
active, verifiedsuccess
expiring_soon, pending_verificationwarning
expired, revokeddestructive
inactivemuted

Workflow Statuses

StatusSemantic
completed, approvedsuccess
running, pending, waitingwarning
failed, rejected, cancelleddestructive
draft, pausedmuted

Form Statuses

StatusSemantic
submitted, approvedsuccess
in_progress, pending_reviewwarning
rejected, expireddestructive
draftmuted

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>
  );
}

Form Validation Messages

// ✅ 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)

TokenTailwind utility (examples)Use
--backgroundbg-background, text-backgroundDefault page background
--foreground (= --text-main)text-foregroundDefault body text
--cardbg-card, text-card-foregroundCard surface
--card-elevatedbg-card-elevatedElevated 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.
--popoverbg-popover, text-popover-foregroundPopover/dropdown/menu backgrounds
--mutedbg-muted, text-mutedDe-emphasized chrome (cool gray light, darker slate dark)
--muted-foregroundtext-muted-foregroundMuted body text — the canonical “subtle text” utility
--text-secondarytext-text-secondaryMid-emphasis body text (between foreground and muted-foreground)
--text-mutedtext-text-mutedLower-emphasis body text. Note: text-muted-foreground is preferred for component code
--borderborder-border, borderDefault border color
--inputborder-input, bg-inputForm input border. Shares the --tenant-border-hsl triplet with --border per shadcn convention so a tenant border override flows to inputs automatically.
--input-backgroundbg-input-backgroundForm input fill
--switch-backgroundbg-switch-backgroundSwitch off-state track
--ringring-ring, focus-visible:ring-ringFocus ring color

Brand & action

TokenTailwind utilityUse
--primarybg-primary, text-primaryBrand primary (deep navy) — chrome accents, sidebar active
--primary-hoverhover:bg-primary-hoverPrimary hover state
--primary-foreground (--primary-fg)text-primary-foregroundForeground on bg-primary
--accentbg-accent, text-accentBrand accent (teal) — primary CTA color via Button variant="accent"
--accent-hoverhover:bg-accent-hoverAccent hover state
--accent-foreground (--accent-fg)text-accent-foregroundForeground 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-foregroundtext-secondary-foregroundForeground on bg-secondary / bg-card-elevated

Status

TokenSubtleForegroundNotes
--success--success-subtle--success-foregroundActive / approved / completed
--warning--warning-subtle--warning-foregroundPending / at-risk / expiring
--info--info-subtle--info-foregroundIn progress / informational
--destructive(use bg-destructive/10)--destructive-foregroundError / delete / critical
--destructive-hoverDestructive 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

TokenTailwind utilityUse
--overlay-scrimbg-[hsl(var(--overlay-scrim))]Modal/sheet/drawer backdrops (replaces ad-hoc bg-black/50)
--shimmer-highlightbg-[hsl(var(--shimmer-highlight))]Skeleton/loader shimmer band (replaces ad-hoc via-white/20)
--ai-accentbg-[hsl(var(--ai-accent))], text-ai-accentAI-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.
TokenUse
--density-card-padding, --density-card-padding-lgStandard card padding for shared metric/list primitives
--density-control-height, --density-touch-targetControl height/touch target floor; keep interactive targets at least 44px
--elevation-card, --elevation-card-hover, --elevation-popoverDepth 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.
TokenUse
--sidebar-backgroundSidebar surface
--sidebar-foregroundSidebar text
--sidebar-primary / --sidebar-primary-foregroundActive sidebar item
--sidebar-accent / --sidebar-accent-foregroundHover/secondary sidebar item
--sidebar-borderSidebar dividers
--sidebar-ringSidebar focus ring

Encore brand (reserved)

TokenUse
--encore-navy, --encore-slate, --encore-teal, --encore-teal-light, --encore-teal-mid, --encore-gray, --encore-light, --encore-goldReserved 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 codeModuleTailwind utility example
pfPlatform Foundationbg-module-pf, bg-module-pf-subtle, border-module-pf
clClinicalbg-module-cl, bg-module-cl-subtle, border-module-cl
pmPractice Managementbg-module-pm, bg-module-pm-subtle, border-module-pm
fmFleet Managementbg-module-fm, bg-module-fm-subtle, border-module-fm
rhRecovery Housingbg-module-rh, bg-module-rh-subtle, border-module-rh
itIT & Securitybg-module-it, bg-module-it-subtle, border-module-it
hrHuman Resourcesbg-module-hr, bg-module-hr-subtle, border-module-hr
loLearning & Organizationalbg-module-lo, bg-module-lo-subtle, border-module-lo
ceCommunity Engagementbg-module-ce, bg-module-ce-subtle, border-module-ce
faFinance & Accountingbg-module-fa, bg-module-fa-subtle, border-module-fa
grGovernance & Riskbg-module-gr, bg-module-gr-subtle, border-module-gr
fwForms & Workflowbg-module-fw, bg-module-fw-subtle, border-module-fw

Reserved (do not use yet)

Short codeStatusNotes
thReservedPre-allocated for a future module; declared in src/index.css but no consumer yet
qoReservedSame
daReservedSame
rptReservedSame
Do not consume these in product UI without DS owner approval. See DESIGN_TOKEN_SYNC_POLICY.md.

Deprecated legacy aliases

AliasUse insteadNotes
--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
  • 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

TokenTailwind utilityApprox. light hueApprox. dark hue
--chart-1text-chart-1, bg-chart-1, fill-chart-1, stroke-chart-1Sky blue 49%Sky blue 55%
--chart-2text-chart-2, bg-chart-2, …Teal 40%Teal 50%
--chart-3text-chart-3, …Violet 66%Violet 72%
--chart-4text-chart-4, …Amber 50%Amber 55%
--chart-5text-chart-5, …Pink 60%Pink 66%
--chart-6text-chart-6, …Emerald 39%Emerald 45%
--chart-7text-chart-7, …Orange 53%Orange 58%
--chart-8text-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

PairLight min ΔEWorst profileNotes
chart-2 (teal) ↔ chart-6 (emerald)3.8tritanopiaIndistinguishable in tritanopia (both collapse to teal); 18.2 in deuteranopia
chart-2 (teal) ↔ chart-5 (pink)5.4deuteranopiaBoth collapse to gray
chart-1 (sky) ↔ chart-3 (violet)5.8deuteranopiaBoth collapse to mid-blue
chart-4 (amber) ↔ chart-7 (orange)8.6deuteranopiaBoth collapse to yellow-brown
chart-1 (sky) ↔ chart-2 (teal)9.6tritanopiaBoth 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 utilityNotes
colorPrimary--primarybg-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--accentbg-accent, text-accentBrand CTA color
colorBackground--backgroundbg-backgroundPage background
colorSurface--card-elevatedbg-card-elevatedPF-95 calls this “surface”; runtime calls it card-elevated
colorText--text-main, --foregroundtext-foreground
colorTextMuted--text-mutedtext-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--destructivetext-destructive, bg-destructive
colorSuccess--successtext-success, bg-success
colorWarning--warningtext-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 familyExamplesWhy it matters
--info / --info-subtle / --info-foregroundtext-info, bg-info-subtleTenants cannot rebrand the informational blue
--accent-hover, --primary-hoverhover:bg-accent-hoverHover 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-8text-chart-1, bg-chart-2Tenant cannot rebrand the data-viz palette
--sidebar-*bg-sidebar, bg-sidebar-accentTenant cannot independently rebrand the sidebar shell
--muted, --muted-foregroundbg-muted, text-muted-foregroundDe-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):
  • No raw hex in chart configs — use hsl(var(--chart-N))
  • New chart with categorical color encoding pairs each color with a label, shape, or pattern (per the colorblind audit findings above)
  • Module colors use short-code tokens (--module-cl), not legacy aliases (--module-clinical)
  • Tenant-customizable surfaces use semantic tokens — not encore brand or hex literals — so PF-95 overrides take effect
  • If you introduce a new --tenant-* consumer, add it to the PF-95 Mapping Table above
  • If you change any --chart-N value, re-run npm run audit:chart-palette-colorblind and update the Known problem pairs table
  • If you add a new shadcn-canonical token, register it in REQUIRED_TOKENS in scripts/audit/verify-shadcn-tokens.mjs AND define it under both :root and .dark in src/index.css. Run npm run audit:shadcn-tokens to confirm.

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-1chart-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