Skip to content

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):

Space Grotesk: 500, 700
DM Sans: 400, 500
JetBrains Mono: 400

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)

transform: scale(0.97);
transition: transform var(--dur-instant) var(--ease-in);

No hover-only effects should fire on touch — use @media (hover: hover) to gate transform/shadow.

Focus-visible state

outline: 2px solid var(--color-accent-primary);
outline-offset: 3px;

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-raised at 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

.game-shell {
  padding-bottom: max(env(safe-area-inset-bottom, 0px), 8px);
}

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, round linecap, round linejoin.
  • Color: inherit (set on parent element via color CSS property; icons use currentColor).
  • 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.css as first stylesheet
  • Apply --color-surface-base to <body> background
  • Set color-scheme: dark on :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-motion global collapse rule