Appearance
Design System
Last Updated: 2026-08-19
Color Architecture (3 Layers)
Layer A — Neutral UI (mode-dependent, theme-independent)
Defined in src/lib/palettes.ts as NEUTRAL_LIGHT / NEUTRAL_DARK. Applied via CSS custom properties. Never hardcode hex — always use CSS vars.
| CSS Variable | Light | Dark | Usage |
|---|---|---|---|
--bg | #F5F7FA | #090E16 | Page background |
--surface | #FFFFFF | #111A27 | Card / panel bg |
--border | #E2E8F0 | #243143 | All borders |
--text-primary | #182230 | #F1F5F9 | Headings, values |
--text-secondary | #667085 | #A7B2C3 | Labels, captions |
--text-muted | #98A2B3 | #718096 | Hints, disabled |
Layer B — Theme Identity (palette-driven)
10 palettes × light/dark variants. Set via applyPalette(key, mode). Default palette: aurora.
| CSS Variable | Purpose |
|---|---|
--primary | Buttons, active states, links |
--primary-hover | Button hover |
--primary-soft | Soft badge backgrounds |
--accent | Section borders, active pills, highlights |
--palette-gradient-from/mid/to | Sidebar gradient, login page left panel |
--palette-accent | Sidebar selected indicator, chart accents |
Available palettes: aurora, hermina, ocean, slate, cobalt, sunset, rose, forest, midnight, sapphire
ts
import { applyPalette } from '@/lib/palettes';
applyPalette('aurora', 'light'); // sets all CSS varsLayer C — Data Visualization
12-color palette, theme-reordered. Charts use --chart-1 through --chart-12, never --primary.
ts
import { getChartColor } from '@/lib/colors';
const color = getChartColor(index); // cycles 0-11Status Colors
Fixed semantic colors — consistent across all palettes and modes.
| Token | Light | Dark | When to use |
|---|---|---|---|
--success | #16A34A | #4ADE80 | >= 95% of target |
--info | #2563EB | #60A5FA | 80-95% of target |
--warning | #D97706 | #FBBF24 | < 80% of target |
--danger | #DC2626 | #F87171 | Critical only (BOR < 60%, safety events) |
ts
import { getStatusColor, getStatusLabel } from '@/lib/colors';
const color = getStatusColor(achievementPct); // 0-100Rule: Never use --danger for "below target" — use --warning. Red is reserved for emergencies.
Typography
Hero KPI values: 34px, weight 700, tracking -0.02em, tabular-nums
Supporting KPIs: 22px, weight 700, tracking -0.01em
Section titles: 13-14px, weight 600
Helper / caption: 11-12px, color var(--text-muted)Apply className="tabular-nums" on all numeric KPI displays (defined in global.css).
4-Zone Dashboard Layout
Every dashboard page follows this structure:
Zone 1 — HERO 1-3 large cards, primary question answered at a glance
Zone 2 — SUPPORTING 3-5 compact KPI tiles explaining hero metrics
Zone 3 — PRIMARY CHART One main decision chart + optional side chart (same height)
Zone 4 — ACTION ZONE Filtered lists, branch alerts, actionable itemsGrid
tsx
// Hero (3 cols)
<Row gutter={[12, 12]}>
<Col xs={24} sm={8}><HeroCard /></Col>
</Row>
// Charts (flexible, same row = same height)
<Row gutter={[12, 12]}>
<Col xs={24} lg={14}><Card><BigChart height={280} /></Card></Col>
<Col xs={24} lg={10}><Card><SideChart height={280} /></Card></Col>
</Row>Chart Heights
| Use case | Height |
|---|---|
| Default side-by-side | 280px |
| Clinical / funnel | 300px |
| Large detail | 320px |
| Full-width bar | 360px |
Chart Rules
- Max 12 items — never render all 54 branches
- Reference lines for targets via
targetValueprop - Status coloring via
getStatusColor(pct)— never ad-hoc hex - Dark mode tooltips — always use CSS vars:tsx
<div style={{ background: 'var(--tooltip-bg)', color: 'var(--tooltip-text)', border: '1px solid var(--tooltip-border)', borderRadius: 8, padding: '8px 12px', }}>
Section Headers
tsx
<div style={{
borderLeft: '3px solid var(--primary)',
paddingLeft: 10,
fontWeight: 600,
color: 'var(--text-primary)',
}}>
Section Title
</div>Status Badge
tsx
<span style={{
background: statusColor + '18', // 10% opacity hex suffix
color: statusColor,
borderRadius: 20,
padding: '2px 10px',
fontSize: 12,
fontWeight: 600,
}}>
{achievement.toFixed(0)}% of target
</span>Delta Indicators
tsx
<span style={{ fontSize: 12, color: deltaUp === higherIsBetter ? 'var(--success)' : 'var(--warning)' }}>
{deltaUp ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
{' '}{Math.abs(delta).toFixed(1)}% vs last month
</span>Login Page
Two-panel layout:
- Left panel — full-height gradient (
--palette-gradient-from/mid/to), Hermina logo, "Hermina 360" title, "BUSINESS INTELLIGENCE" subtitle, radial glow highlights using--primaryand--palette-accent - Right panel — 480px wide,
--surfacebg,border-radius: 24px 0 0 24px, vertically centered form, Google SSO button
The gradient follows the active palette automatically — no hardcoded hex on the login page.
Dark Mode
Triggered via data-theme="dark" on <html>. Applied by useThemeStore.toggle().
CSS-only defaults defined in src/styles/global.css under [data-theme="dark"] block (active before JS fires). Runtime values overridden by applyPalette().
Components use CSS vars throughout — no conditional mode === 'dark' checks needed in component styles.
Palette System
Stored in useAppearanceStore (hermina360-appearance localStorage key). Default: aurora.
ts
// Aurora palette (default)
gradientStart: '#4338CA'
gradientMid: '#7C3AED'
gradientEnd: '#06B6D4'
primary: '#7C3AED'
accent: '#06B6D4'Change palette:
tsx
const { paletteKey, setPalette } = useAppearanceStore();
setPalette('ocean'); // persists to localStorageAccessibility
- WCAG AA: 4.5:1 contrast minimum
- Colorblind-safe: Protanopia, Deuteranopia, Tritanopia tested
- Touch targets >= 44px
- Test: Chrome DevTools -> Rendering -> "Emulate vision deficiencies"