Skip to main content

Design System


title: Design System owner: vizu last-updated: 2026-05-16 supersedes: docs/frontend/DESIGN-SYSTEM.md (2026-02-25 — teal palette, retired) status: canonical

Bilko Design System

RETIRED: The previous DESIGN-SYSTEM.md documented a teal (#00E5A0) palette. That document is wrong and archived. The canonical brand is plum (#8B6BBF). See docs/branding/branding/bilko-brand-guidelines.md for the authoritative brand source.


1. Color Palette — Canonical Plum

Source of truth: bilko-brand-guidelines.md (2026-05-16). Tokens below are the Tailwind 4 @theme mapping.

CSS Custom Properties (@theme block in globals.css)

@theme {
  /* Primary plum scale */
  --color-primary: #8b6bbf; /* Plum Primary — CTAs, active states, links */
  --color-primary-hover: #5b3e8a; /* Deep Plum — hover on primary elements */
  --color-primary-light: #c4a8e0; /* Light Plum — subtle backgrounds, chips */
  --color-primary-subtle: #ede5f8; /* Plum tint — focus rings, hover surfaces */

  /* Accent */
  --color-accent: #f2c87a; /* Gold — highlights, accent dots, icons */
  --color-accent-hover: #e8ae4a; /* Gold dark — hover on accent elements */

  /* Surface / Background */
  --color-surface: #f9f7fc; /* Light Lavender — preferred background */
  --color-surface-dark: #14111f; /* Dark mode background */
  --color-bg: #ffffff; /* Pure white — card backgrounds, modals */

  /* Text */
  --color-text-primary: #231c33; /* Dark plum-toned text — on light backgrounds */
  --color-text-secondary: #6b7280; /* Mid gray — labels, secondary copy */
  --color-text-light: #e4def0; /* On dark backgrounds */
  --color-text-muted: #9ca3af; /* Muted, disabled states */

  /* Semantic */
  --color-success: #22c55e;
  --color-warning: #f59e0b;
  --color-error: #ef4444;
  --color-info: #3b82f6;

  /* Border */
  --color-border: #e5e1ef; /* Plum-tinted border */
  --color-border-strong: #c4a8e0; /* Stronger border for focused elements */

  /* Sidebar */
  --color-sidebar-bg: #14111f; /* Dark plum sidebar */
  --color-sidebar-text: #e4def0;
  --color-sidebar-active: #8b6bbf;

  /* Chart colors */
  --color-chart-1: #8b6bbf;
  --color-chart-2: #f2c87a;
  --color-chart-3: #5b3e8a;
  --color-chart-4: #c4a8e0;
  --color-chart-5: #ede5f8;
}

Color Rules (from brand guidelines)

  • Never use --color-primary (#8B6BBF) as body text on white — WCAG contrast fails at small sizes (3.6:1).
  • Gold Accent (#F2C87A) is for highlights only — never as a primary CTA color.
  • Surface (#F9F7FC) is preferred over pure white for page backgrounds.
  • Focus rings must use --color-primary-hover (#5B3E8A) at minimum 3:1 contrast against adjacent color (WCAG 2.4.11).

Retired Colors

The following colors appeared in the old DESIGN-SYSTEM.md and are explicitly retired. Do not use:

Retired hex Label in old doc Reason
#00E5A0 Primary teal-green Wrong brand
#00B380 Primary dark teal Wrong brand
#33EBB3 Primary light teal Wrong brand
#111113 Sidebar dark Replaced by #14111F

2. Typography

Fonts: National Park (headings) · Work Sans (body, UI) · DM Mono (numbers, data, code)

Type Scale

@theme {
  --font-heading: 'National Park', system-ui, sans-serif;
  --font-body: 'Work Sans', system-ui, sans-serif;
  --font-mono: 'DM Mono', 'Courier New', monospace;

  --text-xs: 0.75rem; /* 12px — fine print */
  --text-sm: 0.875rem; /* 14px — labels, secondary */
  --text-base: 1rem; /* 16px — body copy */
  --text-lg: 1.125rem; /* 18px — large body, lead */
  --text-xl: 1.25rem; /* 20px — small headings */
  --text-2xl: 1.5rem; /* 24px — H3 */
  --text-3xl: 2rem; /* 32px — H2 */
  --text-4xl: 3rem; /* 48px — H1 landing */
}

Usage Rules

  • Headings: National Park Bold. Use for h1h3 and the logo wordmark.
  • Body: Work Sans Regular/SemiBold. All UI copy, labels, table cells.
  • Mono: DM Mono Regular. Invoice numbers, amounts, account codes, code blocks.
  • Minimum body size: 16px (1rem). Never below 12px (WCAG 1.4.4).

3. Spacing System

Base unit: 8px. All spacing values are multiples of 8px. Never use arbitrary pixel values.

@theme {
  --spacing-xs: 4px; /* 0.5 × base */
  --spacing-sm: 8px; /* 1 × base */
  --spacing-md: 16px; /* 2 × base */
  --spacing-lg: 24px; /* 3 × base */
  --spacing-xl: 32px; /* 4 × base */
  --spacing-2xl: 48px; /* 6 × base */
  --spacing-3xl: 64px; /* 8 × base */
}

4. Border Radius

@theme {
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-full: 9999px; /* pills, badges */
}

5. Shadows

@theme {
  --shadow-card: 0 1px 3px rgba(35, 28, 51, 0.08), 0 1px 2px rgba(35, 28, 51, 0.06);
  --shadow-modal: 0 4px 24px rgba(35, 28, 51, 0.16);
  --shadow-dropdown: 0 2px 8px rgba(35, 28, 51, 0.12);
}

6. Dark Mode Strategy

Bilko uses CSS prefers-color-scheme for automatic dark mode, togglable via a data-theme attribute on <html>.

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #14111f;
    --color-surface: #1e1929;
    --color-text-primary: #e4def0;
    --color-border: #3a2f55;
  }
}

[data-theme='dark'] {
  --color-bg: #14111f;
  --color-surface: #1e1929;
  --color-text-primary: #e4def0;
  --color-border: #3a2f55;
}

OPEN QUESTION OQ-3: Dark mode is not implemented in apps/web/ as of 2026-05-16. The token system above is the planned architecture. Timeline for implementation is not assigned.


7. shadcn/ui Variant API via cva

All shadcn/ui components use class-variance-authority (cva) for variant composition. The cn() utility merges Tailwind classes with conflict resolution via tailwind-merge.

Button — verified cva definition (from components/ui/button.tsx)

import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const buttonVariants = cva(
  // Base styles — always applied
  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-11 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
        icon: 'h-11 w-11',
      },
    },
    defaultVariants: { variant: 'default', size: 'default' },
  },
)

Adding a Custom Variant

// Extend buttonVariants for a "plum-outline" variant
const buttonVariants = cva(/* base */, {
  variants: {
    variant: {
      // ... existing variants
      'plum-outline': 'border-2 border-primary text-primary hover:bg-primary-subtle',
    },
  },
})

cn() Usage Rule

Always compose class lists with cn() — never with template literals or string concatenation:

// Correct
<div className={cn('base-class', condition && 'conditional-class', className)} />

// Wrong
<div className={`base-class ${condition ? 'conditional-class' : ''} ${className}`} />