Color

Our color system uses OKLCH — a perceptually uniform color space that ensures consistent lightness across hues. Colors are defined as CSS custom properties and mapped to Tailwind utility classes via shadcn's token layer.

Brand Palette

Three signature hues embody our duality. Green for Life, violet/pink for Virtual, and blue as the Bridge between them. These appear in our gradient title, wavy background, and accent moments.

Life
oklch(0.65 0.20 145)
Life Soft
oklch(0.80 0.12 145)
Life Muted
oklch(0.45 0.10 145)
Virtual
oklch(0.60 0.22 300)
Virtual Soft
oklch(0.75 0.14 300)
Virtual Muted
oklch(0.40 0.12 300)
Bridge
oklch(0.60 0.20 220)
Bridge Soft
oklch(0.75 0.12 220)
Accent Warm
oklch(0.70 0.19 340)
Gradient Signature. The brand gradient flows Blue → Pink → Green at 120°. It appears on the app title, the primary action's aura, and the app canvas. This gradient is the single most recognizable visual element of VirtualLife.
Brand Gradient — 120° · Blue → Pink → Green

Semantic Tokens (Dark Mode)

Semantic tokens are the bridge between brand colors and component usage. They are defined as CSS custom properties and consumed through shadcn's Tailwind mapping. Every component references semantics, never raw values.

Token Preview OKLCH Value Tailwind Class Usage
--background oklch(0.21 0.04 266) bg-background App canvas, main content area
--foreground oklch(0.93 0.013 256) text-foreground Primary body text
--card oklch(0.28 0.04 260) bg-card Card surfaces (used at /10 opacity)
--primary oklch(0.68 0.158 277) bg-primary Primary buttons, active states, links
--secondary oklch(0.34 0.033 261) bg-secondary Secondary buttons, less prominent actions
--muted oklch(0.24 0.038 260) bg-muted Subdued backgrounds, disabled fields
--muted-foreground oklch(0.71 0.019 261) text-muted-foreground Secondary text, placeholders, captions
--accent oklch(0.37 0.031 260) bg-accent Hover highlights, sidebar active bg
--destructive oklch(0.64 0.208 25) bg-destructive Delete actions, error states
--border oklch(0.45 0.026 257) border-border Card edges, dividers, input borders
--ring oklch(0.68 0.158 277) ring-ring Focus rings on interactive elements
--sidebar oklch(0.21 0.04 266) bg-sidebar Navigation sidebar, title bar, status bar

Semantic Tokens (Light Mode)

Light mode tokens are applied via the html.light class. They maintain the same semantic names but shift values for readability on light backgrounds. Primary colors become darker; surfaces become lighter.

Token Preview OKLCH Value Tailwind Class Usage
--background oklch(0.97 0.005 265) bg-background App canvas, main content area
--foreground oklch(0.18 0.04 265) text-foreground Primary body text
--card oklch(0.95 0.005 265) bg-card Card surfaces
--primary oklch(0.52 0.20 277) bg-primary Primary buttons, active states, links
--secondary oklch(0.90 0.010 260) bg-secondary Secondary buttons, less prominent actions
--muted oklch(0.94 0.008 260) bg-muted Subdued backgrounds, disabled fields
--muted-foreground oklch(0.40 0.04 257) text-muted-foreground Secondary text, placeholders, captions
--accent oklch(0.92 0.008 260) bg-accent Hover highlights, sidebar active bg
--destructive oklch(0.55 0.22 25) bg-destructive Delete actions, error states
--border oklch(0.78 0.015 257) border-border Card edges, dividers, input borders
--ring oklch(0.52 0.20 277) ring-ring Focus rings on interactive elements
--sidebar oklch(0.94 0.018 272) bg-sidebar Navigation sidebar, title bar, status bar

Status Colors

Status colors are consistent across modes — they're designed to be accessible on both dark and light backgrounds. In light mode, darker variants (--success, --warning) are used for better contrast against white surfaces.

Status Dark Dark OKLCH Light Light OKLCH
Success oklch(0.723 0.219 150) oklch(0.45 0.18 150)
Error oklch(0.64 0.208 25) oklch(0.55 0.22 25)
Warning oklch(0.828 0.189 84) oklch(0.65 0.18 84)
Info oklch(0.68 0.158 277) oklch(0.52 0.20 277)

Surface Scale

Surfaces layer from darkest (canvas) to lightest (elevated card) in dark mode, and from lightest to elevated in light mode. Each step is subtle — about 7% lightness change — creating depth without hard edges.

Surface Dark Dark OKLCH Light Light OKLCH Usage
Surface 0 oklch(0.13 0.04 265) oklch(0.97 0.005 265) Canvas
Surface 1 oklch(0.21 0.04 266) oklch(0.94 0.018 272) Background / Sidebar
Surface 2 oklch(0.28 0.04 260) oklch(0.90 0.010 260) Card
Surface 3 oklch(0.34 0.03 261) oklch(0.86 0.012 261) Elevated

Typography

Our type system uses TT Rounds Neue — a geometric rounded sans-serif by TypeType Foundry — as the primary typeface. It pairs with a monospaced face for technical precision and a serif for rare prose moments. The system supports theme-level font overrides.

Primary · Sans-Serif
The quick brown fox jumps over the lazy dog
ABCDEFGHIJKLMNOPQRSTUVWXYZ · abcdefghijklmnopqrstuvwxyz · 0123456789
Light 300 Regular 400 Medium 500 Semibold 600 Bold 700 ExtraBold 800
TT Rounds Neue is the primary typeface. It's a geometric rounded sans-serif with a warm, friendly character that matches VirtualLife's personality — playful yet professional. Variable font loaded locally (100–900 weight axis). Fallback chain: Montserrat → Corbel → Nunito → system-ui → sans-serif.

Licensing: TT Rounds Neue is free for personal/trial use. Commercial deployment requires a license from TypeType Foundry.
Theme overrides. Each theme can specify its own font stack. Solar Dusk uses Oxanium (a futuristic geometric face). Caffeine uses the system UI stack. The DLS tokens remain the same — only the values change per theme. The CSS variable --font-sans is the single source of truth. The default VirtualLife theme uses TT Rounds Neue; other themes fall back to Montserrat.
Monospace
const imagine = (prompt) => ai.generate(prompt);
ABCDEFGHIJKLMNOPQRSTUVWXYZ · abcdefghijklmnopqrstuvwxyz · 0123456789 · {} [] () <> = + - * / | \ _ @ # $ %
JetBrains Mono is used for code snippets, technical values, token names, and the version number badge. Fallback: Cascadia Code → Fira Code → ui-monospace → monospace.
Serif (Display / Prose)
Where human creativity meets artificial imagination
ABCDEFGHIJKLMNOPQRSTUVWXYZ · abcdefghijklmnopqrstuvwxyz · 0123456789
Source Serif 4 is reserved for long-form prose content and special display moments. It is rarely used in the app UI but available for rich content areas. Fallback: Georgia → Times New Roman → serif.

Type Scale

Base size is 0.825rem (~13.2px) — slightly smaller than the browser default to increase information density without sacrificing readability. The scale uses --text-scaling for user preferences and Tailwind's built-in classes.

Page Title
text-lg · 600 · tight
Generate Images
Section Title
text-base · 600
Model Settings
Body
text-sm · 400
Enter a detailed description of the image you want to create.
Caption
text-xs · 400 · muted
Generated 2 minutes ago · Flux1.dev
Label
text-sm · 500
Prompt
Badge / Pill
text-xs · 600
READY
Token / Code
mono · text-xs
--primary: oklch(0.68 0.16 277)

Prose Headings (Rich Content)

Element Size Weight Margin
h1 1.5em 700 (Bold) 0.67em top/bottom
h2 1.3em 600 (Semibold) 0.83em
h3 1.17em 600 1em
h4 1em 600 1.33em
p inherit 400 0.5em

Wordmark

The wordmark is a fixed asset, not a style you compose per product. Two faces carry one continuous colour sweep: a humanist Virtual against a geometric Life, washed by a slow tide of pink into green.

VirtualLife VirtualLife display size · app-bar size (--text-xl)

Anatomy

Three colours, each in exactly one contiguous zone, read left to right: pink, then foam, then green. Green lands on “Life” and never appears on the left. The zones drift, but they cannot trade places — a colour that reappears on the far side of the word is the failure mode this construction exists to prevent.

The gradient and the background clip live on the parent element. Both words paint through one background box, so the tide runs continuously across the join. Moving the gradient onto the two children is the single mistake that breaks the mark: it restarts the sweep at “Life” and you get two marks sitting next to each other.

Type pairing

“Virtual” is Lato, a humanist face; “Life” is TT Rounds Neue, geometric and rounded. The name's own contrast, made visible. This is the only place in the system where two faces meet inside one line — do not borrow the pairing for UI text.

No optical size bump — deliberately. Measured at weight 700, Lato and TT Rounds Neue have cap heights of 72 and 71 units per 100px em, and an identical x-height of 51. They set level at the same size, so the join needs no correction.

This is worth stating because the previous pairing did need one. “Virtual” was set in Corbel, whose cap height is only 65 against TT Rounds Neue's 71 — a 9% shortfall that made the first word read a size smaller than the second, so .vl-wordmark-virtual carried a font-size: 1.07em correction. That bump belonged to Corbel, not to the lockup: applied to Lato it would push “Virtual” 7% too large. If the humanist face is ever swapped again, measure the two cap heights before assuming any value.
Why Lato and not Corbel. Corbel ships with Windows and Office and is not licensed for webfont embedding, so it could only ever render on machines that already had it installed — roughly, Windows users. Lato is under the SIL Open Font License: it can be embedded and self-hosted freely, so every visitor sees the same mark. It is also the closer metric match, as above. Where the mark still must be pixel-identical regardless of font loading — an OG image, an app icon, a printed piece — export it as outlined SVG paths.

Motion

omTide walks the whole boundary across the word and back over 22 seconds. It is ambient, not a loading state: nothing about it should read as progress or invite waiting. The two stop positions are registered custom properties so they interpolate; their initial values are the resting frame, which means the mark is complete and correct with no animation running at all.

Under prefers-reduced-motion the tide keeps running, switched to omTideCalm: about a third of the travel, at 40s instead of 22s. It is not stopped, because nothing in this animation moves — the letterforms are fixed and only the colour beneath them shifts. The vestibular trigger the setting exists to defuse is displacement through space, not hue, and this file already treats looping animations that way elsewhere: calm them, don't kill them. Surfaces that genuinely need stillness opt in with .vl-wordmark-static, which stops it outright.

Usage

<span class="vl-wordmark">
  <span class="vl-wordmark-virtual">Virtual</span><span class="vl-wordmark-life">Life</span>
</span>

Size it by setting font-size on .vl-wordmark; everything else scales from there. It is one line by design and never wraps. Add .vl-wordmark-static for screenshots, email and print.

Not the brand gradient. The wordmark runs --gradient-wordmark, which is a different recipe from --gradient-brand (the 120° blue → pink → green used for the primary CTA aura and the canvas). They are not interchangeable. The wordmark is the one place the pink and green appear as separated zones rather than a blend.

Spacing

Spacing follows Tailwind's 4px base grid. We favor a constrained set of values for consistency: the 4-8-12-16-24-32-48 scale handles virtually all layout needs.

1 · 4px
0.25rem
2 · 8px
0.5rem
3 · 12px
0.75rem
4 · 16px
1rem
6 · 24px
1.5rem
8 · 32px
2rem
12 · 48px
3rem

Spacing Usage Guidelines

Context Tailwind Value When to Use
Inline element gap gap-2 8px Icon + text, badge contents, tight groups
Form field gap gap-2 8px Label to input, switch to label
Card internal padding p-4 16px Standard card content padding
Section vertical gap gap-4 16px Between cards, between form groups
Page padding px-4 pt-4 16px Section wrapper padding
Grid image gap gap-4 16px Gallery grid, catalog grid
Dialog padding p-6 24px Modal dialogs and popovers
Toolbar compact px-3 py-1 12px × 4px Status bar, compact toolbar rows
Consistency rule: Within a single container, use one gap value. Mixed spacing inside the same flex/grid creates visual noise. The 4px grid ensures everything aligns, even when nested.

Border Radius

Rounded corners are fundamental to VirtualLife's soft, approachable feel. The base radius (--radius: 0.625rem) ripples through the system via calc() expressions. Themes can override the base to shift the entire system's roundness.

sm
0.375rem · 6px
md
0.5rem · 8px
lg (base)
0.625rem · 10px
xl
0.875rem · 14px
2xl
1rem · 16px
full
9999px · pill

Radius Assignment

Element Radius Tailwind
Buttons (default) md rounded-md
Input fields md rounded-md
Cards xl rounded-xl
Dialogs lg rounded-lg
Badges / Pills full rounded-full
Images (generated) lg or xl rounded-lg
Main content inset 2xl (top-left only) border-top-left-radius: 16px
Radial menu buttons full rounded-full
Status dots full rounded-full
Tab list lg rounded-lg
Scrollbar thumb 4px Custom CSS

Elevation

Elevation is communicated through shadow depth and backdrop blur. In dark mode, shadows are more subtle and surface lightness shifts do most of the layering work. The glass effect replaces heavy drop shadows for most surfaces.

Level 0
No shadow
Level 1
shadow-sm
Level 2
shadow-md
Level 3
shadow-lg
Level 4
shadow-xl
Level Used For Shadow
0 Flat surfaces, inline elements none
1 (sm) Buttons, cards at rest, inputs 0 1px 2px oklch(0 0 0/0.15)
2 (md) Toasts, popovers, dropdowns 0 4px 12px oklch(0 0 0/0.15)
3 (lg) Image tiles on hover, dialogs 0 15px 40px -10px oklch(0 0 0/0.50)
4 (xl) Image tile active hover, fullscreen overlay Multi-layer composite

Glass & Transparency

Glass morphism is VirtualLife's signature surface treatment. Translucent backgrounds combined with backdrop blur create depth and visual interest while letting the wavy background pattern breathe through the interface.

Glass Light

The standard card surface. Most of the background shows through, with just enough blur to maintain text legibility.

bg-card/10 backdrop-blur-sm
Glass Medium

For interactive panels, filter bars, overlays on images. More opacity means more contrast for denser content.

bg-card/40 backdrop-blur-md
Glass Heavy

For sticky headers, group labels, badges on images. Nearly opaque but still part of the glass family.

bg-card/80 backdrop-blur-sm

Glass Recipes

Recipe Opacity Blur Border Used For
Glass Light bg-card/10 backdrop-blur-sm border-border/50 Standard cards (VLCard)
Glass Medium bg-card/40 backdrop-blur-md border-border Image overlays, filter panels, edit panels
Glass Heavy bg-card/80 backdrop-blur-sm border-border/50 Sticky headers, group labels
Glass Scrim bg-black/50 none none Image status overlays (loading/error)
Glass Overlay bg-black/80 none none Dialog backdrop, fullscreen overlay
Performance note: backdrop-filter: blur() is GPU-accelerated in Chromium-based renderers but can cause compositing overhead on dense grids. Use backdrop-blur-sm (4px blur) as the default; reserve backdrop-blur-md (12px blur) for surfaces that overlay rich content like images.

The app canvas

The wave fan is the system default behind every content area, not a decoration you opt into. Three stacked layers: the eleven-path wave fan, two low-opacity brand auras bleeding in from opposite corners, and Surface 0 underneath. It is available as one token — --canvas-app — and that is how content areas get it.

The wave pattern is VirtualLife's most distinctive visual element. It represents the flowing connection between human and AI creativity — organic curves rendered with digital precision.

Specimens

--canvas-app · the default
--canvas-app-loud · specimen & marketing shots

Tokens

Token Contents Use
--canvas-waves The wave fan alone — 11 cubic-Bézier paths, 1422 × 800 viewBox, baked as a data-URI SVG Overlaying a background you already have
--canvas-auras Two radial gradients: violet low-left, green high-right, 4–5% alpha Colour without pattern
--canvas-app Waves over auras over --surface-0 The default. Every content area
--canvas-app-loud Deeper opacity ramp, stronger auras Specimen cards, marketing shots — never product UI

Rules

Canvas behind content, solid chrome around it. Content areas take background: var(--canvas-app). The app bar, the destination rail and the navigator panel stay solid --surface-1 — that contrast is what makes the content frame read as the place the work happens. No photography, no heavy texture, and never two canvases in one screen.
Felt more than seen. If the waves compete with content for attention the opacity is too high. Glass panels float above the canvas and their blur softens it further; the composed tokens are re-declared per theme scope, so the light-mode fan darkens instead of glowing.

Theming

VirtualLife supports swappable themes that override the semantic token layer without changing component structure. Each theme is a JSON file with light/dark CSS variable overrides plus optional font and radius changes.

Theme Architecture

// theme.json structure
{
  "name": "Clean Slate",
  "cssVars": {
    "theme": { // Shared (fonts, radius)
      "--font-sans": "TT Rounds Neue, sans-serif",
      "--radius": "0.5rem"
    },
    "light": { // Light mode overrides
      "--background": "oklch(0.98 0.003 248)",
      "--primary": "oklch(0.59 0.204 277)"
    },
    "dark": { // Dark mode overrides
      "--background": "oklch(0.21 0.04 266)",
      "--primary": "oklch(0.68 0.158 277)"
    }
  }
}

Available Themes

Clean Slate
Default · TT Rounds Neue
Northern Lights
Aurora · Plus Jakarta Sans
Solar Dusk
Warm · Oxanium
Caffeine
Monochrome · System UI
Modern Minimal
Cool · Inter
Amber Minimal
Warm · Inter
Theme rules: Themes change values, never structure. A theme must provide all semantic tokens for both light and dark modes. Components never reference theme-specific values directly — they only consume semantic tokens like bg-primary. This guarantees every theme works with every component.

Light & Dark Mode

Dark mode is the primary design surface — optimized for immersive creative work where generated images pop against deep backgrounds. Light mode is a first-class alternative, not an afterthought. The .dark class on <html> toggles the active variable set. Each theme provides both dictionaries.