Design System: games.justinwiebelhaus.com¶
Version 1.0 — Designer handoff for ui agent.
1. Design Direction¶
Theme: Warm Midnight. A curated indie games hub that reads as personal and intentional — not an arcade, not an app store, not a game jam dump. The palette is dark but warm (tinted blacks, not pure) with a single high-energy primary accent. Each game gets its own accent slot so the hub reads as a collection of distinct works.
Principles: - Dark mode first. Games live on dark canvases; the hub should feel continuous with that. - Warmth over coldness. Slightly warm-shifted darks and an amber/vermillion accent family signal "handcrafted" not "enterprise." - Mobile-first sizing. Every spec is written for 375px viewport first; tablet/desktop breakpoints expand outward. - Restraint on animation. Motion tokens are defined but used sparingly — the games are the spectacle.
2. Palette¶
Base surfaces¶
| Token | Hex | Role |
|---|---|---|
--color-surface-base |
#0F0E17 |
Page background, game canvas bg |
--color-surface-raised |
#1A1827 |
Elevated panels, nav bar |
--color-surface-card |
#201E2E |
Game cards, modal overlays |
--color-surface-inset |
#161424 |
Input fields, inset wells |
--color-border |
#2E2C40 |
Dividers, card borders |
--color-border-subtle |
#1F1D2B |
Very subtle separators |
Ink (text)¶
| Token | Hex | Role |
|---|---|---|
--color-ink-primary |
#FFFFFE |
Headlines, active labels |
--color-ink-secondary |
#A7A5B0 |
Body copy, descriptions |
--color-ink-muted |
#5C5A6E |
Disabled, placeholder |
--color-ink-inverse |
#0F0E17 |
Text on bright accent fills |
Brand accents¶
| Token | Hex | Role |
|---|---|---|
--color-accent-primary |
#F2542D |
Primary CTA, active states, focus rings |
--color-accent-primary-dim |
#3D1810 |
Accent background tint, subtle fills |
--color-accent-secondary |
#A0C4FF |
Cool counterpoint, links, secondary highlights |
--color-accent-secondary-dim |
#162035 |
Secondary bg tint |
Semantic¶
| Token | Hex | Role |
|---|---|---|
--color-success |
#6BCB77 |
Win states, positive feedback |
--color-success-dim |
#112618 |
Success background tint |
--color-warning |
#FFD166 |
Caution, timer warnings |
--color-warning-dim |
#2E2408 |
Warning background tint |
--color-danger |
#FF6B6B |
Explosions, fail states |
--color-danger-dim |
#2E0F0F |
Danger background tint |
Per-game accent slots¶
Each game card and its shell top bar pulls its accent from its slot. These are single hues; tint/dim variants are computed at --game-accent + alpha(0.12) for fill use.
| Game | Token | Hex |
|---|---|---|
| Gravity Sculptor | --game-accent-gravity-sculptor |
#7B61FF |
| Sync Swarm | --game-accent-sync-swarm |
#FFE66D |
| Pressure Cook | --game-accent-pressure-cook |
#FF6B6B |
| Crowd Ripple | --game-accent-crowd-ripple |
#4ECDC4 |
| Signal Weave | --game-accent-signal-weave |
#A8DADC |
| Resonance Draw | --game-accent-resonance-draw |
#C77DFF |
| Echo Chain | --game-accent-echo-chain |
#43AA8B |
| Brushfire Forest | --game-accent-brushfire-forest |
#F4845F |
| Shadow Stack | --game-accent-shadow-stack |
#B0C4DE |
| Fault City | --game-accent-fault-city |
#C9ADA7 |
| Pixel Seed | --game-accent-pixel-seed |
#52B788 |
| Dark Sonar | --game-accent-dark-sonar |
#00B4D8 |
| Mood Creature | --game-accent-mood-creature |
#FF9F1C |
| Blob Merge | --game-accent-blob-merge |
#E040FB |
| Chrono Thread | --game-accent-chrono-thread |
#F4D03F |
On card hover, the card's top border shifts from --color-border to its --game-accent-* value at full opacity.
3. Typography¶
Google Fonts import (one request, subset to latin):
URL string for the <link> tag:
https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=DM+Sans:wght@400;500&family=JetBrains+Mono&display=swap
Font stacks¶
| Token | Value |
|---|---|
--font-display |
'Space Grotesk', system-ui, sans-serif |
--font-body |
'DM Sans', system-ui, sans-serif |
--font-mono |
'JetBrains Mono', ui-monospace, monospace |
Type scale (mobile-first; clamp for fluid scaling)¶
| Token | Mobile | Desktop clamp |
|---|---|---|
--text-xs |
11px |
clamp(11px, 1.1vw, 12px) |
--text-sm |
13px |
clamp(13px, 1.3vw, 14px) |
--text-base |
15px |
clamp(15px, 1.5vw, 16px) |
--text-lg |
18px |
clamp(18px, 1.8vw, 20px) |
--text-xl |
22px |
clamp(22px, 2.2vw, 26px) |
--text-2xl |
28px |
clamp(28px, 3vw, 36px) |
--text-3xl |
36px |
clamp(36px, 4vw, 52px) |
--text-hero |
44px |
clamp(44px, 6vw, 80px) |
Usage rules¶
- Hub page
<h1>("Play Something Different"):--text-hero,--font-display, weight 700,--color-ink-primary. - Game card title:
--text-lg,--font-display, weight 700. - Card genre tag badge:
--text-xs,--font-body, weight 500, uppercase, letter-spacing 0.08em. - Game shell title in top bar:
--text-base,--font-display, weight 700, centered. - In-game instructions / HUD:
--text-sm,--font-body, weight 400. - Scores, timers, counters:
--font-mono, weight 400. - Body copy on hub:
--text-base,--font-body, weight 400,--color-ink-secondary, line-height 1.6.
4. Card Grid Spec¶
Grid layout¶
| Breakpoint | Columns | Gap | Card max-width |
|---|---|---|---|
| 0px (default) | 1 | --space-4 (16px) |
100% |
| 480px | 2 | --space-5 (24px) |
— |
| 960px | 3 | --space-5 (24px) |
— |
| 1440px | 4 | --space-6 (32px) |
— |
Implementation: display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--space-5);
Card anatomy¶
+----------------------------------+
| | <- accent border top: 2px solid var(--game-accent-*)
| GAME THUMBNAIL AREA | <- height: 56% of card; Canvas or static colored bg
| (Canvas preview or gradient) |
| |
+----------------------------------+
| [GENRE TAG] | <- --text-xs, badge pill, --color-border bg
| Game Title | <- --text-lg, --font-display, 700
| One-line pitch text | <- --text-sm, --color-ink-secondary
| [>] | <- play icon button, --color-accent-primary
+----------------------------------+
Card dimensions:
- Aspect ratio: none enforced on card; thumbnail area is fixed 180px mobile, 200px tablet+.
- Border-radius: --radius-lg (16px).
- Background: --color-surface-card.
- Border: 1px solid --color-border; top border 2px solid --game-accent-*.
- Padding (content area): --space-4 (16px).
Hover state (pointer devices)¶
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px var(--game-accent-*);
transition: transform var(--dur-fast) var(--ease-spring),
box-shadow var(--dur-fast) var(--ease-out);
The play icon transitions: opacity: 0 at rest, opacity: 1 on hover.
Tap state (touch devices)¶
No hover-only effects should fire on touch — use @media (hover: hover) to gate transform/shadow.
Focus-visible state¶
5. Game Shell Chrome¶
The shell wraps every individual game page (/games/<slug>/index.html).
Top bar¶
- Height: 48px +
env(safe-area-inset-top, 0px)padding-top. - Background:
--color-surface-raisedat 92% opacity,backdrop-filter: blur(12px). - Border-bottom: 1px solid
--color-border. - Layout: three-column flex. Left: back icon (32x32 tap target). Center: game title. Right: fullscreen icon (32x32 tap target).
- On narrow screens (< 360px), title truncates with
text-overflow: ellipsis. - The top bar accent stripe: a 3px line at the very top of the bar in
--game-accent-*. This is the only place per-game color appears in the chrome (apart from<meta name="theme-color">).
Bottom safe area¶
The game canvas should fill 100dvh minus the top bar height. Use height: calc(100dvh - 48px - env(safe-area-inset-top, 0px)).
Back navigation¶
Back arrow returns to ../../index.html (hub). No full-page reload state is needed. Confirm prompt only if the game has unsaved progress (individual game's responsibility to wire this).
Fullscreen toggle¶
Uses document.documentElement.requestFullscreen(). Icon toggles between expand and compress states. On iOS Safari, fullscreen API is unavailable — hide the icon if document.fullscreenEnabled === false.
Score / HUD positioning¶
In-game HUDs should position inside --space-3 (12px) from the game canvas edges, never overlapping the shell top bar. Games are responsible for their own HUD; the shell only provides the chrome wrapper.
6. Iconography Style¶
- Grid: 24px bounding box.
- Stroke: 1.5px,
roundlinecap,roundlinejoin. - Color: inherit (set on parent element via
colorCSS property; icons usecurrentColor). - Fill: none (outline style only, no filled icons).
- Touch targets: minimum 44x44px tap target wrapper around every icon button; icon itself is 24px inside.
- Inline SVG, not an icon font or external sprite. Each icon is hand-authored, no dependency on any icon library.
Core icon set needed by ui agent:
| Name | Usage |
|---|---|
icon-back |
Shell top bar, back to hub |
icon-fullscreen |
Shell top bar, enter fullscreen |
icon-minimize |
Shell top bar, exit fullscreen |
icon-play |
Card CTA |
icon-grid |
Hub nav if paginated |
icon-search |
Hub filter bar |
7. Motion Tokens¶
| Token | Value | Use |
|---|---|---|
--dur-instant |
80ms |
Tap feedback, toggle flips |
--dur-fast |
150ms |
Card hover, icon transitions |
--dur-base |
250ms |
Page-level fades, modal entrance |
--dur-slow |
400ms |
Reveal animations, route transitions |
--dur-crawl |
600ms |
Onboarding / first-load sequences |
--ease-out |
cubic-bezier(0.0, 0.0, 0.2, 1) |
Most exits and reveals |
--ease-in |
cubic-bezier(0.4, 0.0, 1.0, 1.0) |
Elements leaving screen |
--ease-in-out |
cubic-bezier(0.4, 0.0, 0.2, 1) |
Cross-fades, looping |
--ease-spring |
cubic-bezier(0.34, 1.56, 0.64, 1) |
Card hover lift, modal pop |
--ease-bounce |
cubic-bezier(0.68, -0.55, 0.265, 1.55) |
Score counters, badge pop-in |
prefers-reduced-motion: all transition/animation durations should collapse to 0.01ms when the media query matches. Apply this globally:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
8. Spacing Scale¶
Base unit: 4px.
| Token | Value |
|---|---|
--space-1 |
4px |
--space-2 |
8px |
--space-3 |
12px |
--space-4 |
16px |
--space-5 |
24px |
--space-6 |
32px |
--space-7 |
48px |
--space-8 |
64px |
--space-9 |
96px |
--space-10 |
128px |
9. Border Radius¶
| Token | Value | Use |
|---|---|---|
--radius-sm |
4px |
Tags, small badges |
--radius-md |
8px |
Buttons, inputs |
--radius-lg |
16px |
Cards, modals |
--radius-xl |
24px |
Bottom sheets, large panels |
--radius-pill |
9999px |
Genre tag badges, chips |
10. Hub Page Layout¶
[ Hub top nav: site wordmark left | "15 Games" pill right ]
[ Hero: 60vh, big headline + subtitle ]
[ Filter bar: genre chips — All | Arcade | Puzzle | Physics | Audio | Word | Drawing | Strategy | Generative ]
[ Card grid: auto-fill, minmax(280px, 1fr) ]
[ Footer: author credit, link to justinwiebelhaus.com ]
Hub top nav height: 56px. Sticky on scroll with same backdrop-blur as game shell bar.
Hero headline copy (placeholder for ui agent): "Play Something Different." Sub: "15 original browser games. No installs. No accounts."
Filter chips: --radius-pill, border 1px solid --color-border, active state fills with --color-accent-primary + --color-ink-inverse text.
11. Thumbnail Placeholder Strategy¶
Until real screenshots exist, each card's thumbnail area uses a generated gradient derived from the game's accent color:
background: linear-gradient(135deg, var(--game-accent-<slug>) 0%, color-mix(in srgb, var(--game-accent-<slug>), #0F0E17 70%) 100%);
Center the game's first emoji or an icon in --text-3xl size over this gradient as a visual hook. Once the reviewer agent captures real screenshots (step s6), swap background for <img>.
12. Deliverables Checklist for ui Agent¶
- Import Google Fonts link tag (Space Grotesk 500/700 + DM Sans 400/500 + JetBrains Mono 400)
- Import
design-tokens.cssas first stylesheet - Apply
--color-surface-baseto<body>background - Set
color-scheme: darkon:root - Wire
--game-accent-<slug>on each card via inline style or data attribute - Gate card hover lift behind
@media (hover: hover) - Pad shell top bar with
env(safe-area-inset-top) - Pad shell bottom with
max(env(safe-area-inset-bottom), 8px) - Add
prefers-reduced-motionglobal collapse rule