mini-games
Design System
Shared components, tokens, and patterns used across all games. Copy the CSS snippets directly into each game's
<style> block.
Colors
All game UIs are built on this dark slate palette. Use these hex values when writing per-game
<style> blocks.
Backgrounds
Text
Accent & Game Colors
Typography
Overlay title ยท 1.6rem bold
Game Over
Section heading ยท 1.5rem bold
How to Play
Stat value ยท 1.2rem monospace bold
4280
Stat label ยท 0.65rem uppercase spaced
Best Score
Body / info ยท 0.875rem ยท color #94a3b8
Flip cards to find matching pairs. Train your memory and beat the clock.
Controls hint ยท 0.75rem ยท color #64748b
Arrow keys ยท D-pad ยท or swipe
Global Components
Defined in src/css/main.css via Tailwind
@layer components. Available on all pages without additional CSS.
Buttons โ .btn-primary /
.btn-secondary
Used for CTAs on the homepage and marketing sections. Not the same as in-game buttons โ see Buttons below.
Card โ .card
<a href="/games/snake/" class="card p-6 group">...</a>
Badge โ .badge
<span class="badge bg-violet-500/20 text-violet-300 border-violet-500/30">Arcade</span>
Category & Difficulty
Applied via Eleventy filters categoryColor,
categoryLabel, and
difficultyColor defined in .eleventy.js.
Category badges
arcade โ bg-violet-500/20 text-violet-300 border-violet-500/30
puzzle โ bg-emerald-500/20 text-emerald-300 border-emerald-500/30
board โ bg-amber-500/20 text-amber-300 border-amber-500/30
word โ bg-sky-500/20 text-sky-300 border-sky-500/30
{{- in Nunjucks templates -}}
<span class="badge {{ game.category | categoryColor }}">
{{ game.category | categoryLabel }}
</span>
{{- in game.njk front matter / layout -}}
Difficulty indicators
<span class="text-xs {{ game.difficulty | difficultyColor }} font-medium capitalize">
{{ game.difficulty }}
</span>
Stat Cards
Used in score rows during gameplay. Copy the CSS block into each game's
<style>.
Standard โ .stat
.stat { text-align:center; background:#1e293b; border:1px solid #334155; border-radius:8px; padding:0.5rem 1rem; }
.stat-label { font-size:0.65rem; text-transform:uppercase; letter-spacing:0.1em; color:#94a3b8; }
.stat-value { font-size:1.2rem; font-weight:bold; color:#e2e8f0; font-family:monospace; }
Game-over summary โ .summary-stat
.summary-row { display:flex; gap:0.75rem; flex-wrap:wrap; justify-content:center; }
.summary-stat { text-align:center; background:#1e293b; border:1px solid #334155; border-radius:8px; padding:0.5rem 0.9rem; min-width:72px; }
.summary-label { font-size:0.65rem; text-transform:uppercase; letter-spacing:0.1em; color:#94a3b8; }
.summary-value { font-size:1.2rem; font-weight:bold; color:#e2e8f0; font-family:monospace; }
Same visual as .stat but with min-width:72px and slightly tighter padding. Used
exclusively in the #gameover-overlay.
D-pad
Virtual directional controller for touch devices. Required for Snake and Tetris; shown only on small screens via
@media (max-width: 640px).
2-row layout (2048)
3-row layout (Snake)
.dpad { display:grid; grid-template-columns:repeat(3,52px); grid-template-rows:repeat(2,52px); gap:6px; }
.dpad-btn { background:#1e293b; border:1px solid #334155; border-radius:8px; color:white; font-size:1.3rem;
cursor:pointer; display:flex; align-items:center; justify-content:center;
touch-action:manipulation; -webkit-tap-highlight-color:transparent; }
.dpad-btn:active { background:#334155; }
Overlays
Two separate overlays per canvas game. See Game Wrapper for positioning context.
Start overlay โ #overlay
Shown on page load. Background is semi-transparent (rgba(15,23,42,0.82)) so the live demo canvas is
visible behind it. The "Live Demo" label is hidden once the real game starts.
Game-over overlay โ #gameover-overlay
#gameover-overlay { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center;
justify-content:center; background:rgba(15,23,42,0.96); border-radius:4px; gap:0.75rem; }
#gameover-overlay h2 { font-size:1.6rem; font-weight:bold; color:white; }
Background is nearly opaque (rgba(15,23,42,0.96)) so the frozen last game frame shows through
slightly. Do NOT call startDemo() here. Use icon ๐ for loss, ๐ for win.
Info Section
Collapsible How to Play + FAQ placed below the game wrapper. Required in every game.
How to Play
Goal
Reach the 2048 tile by merging tiles with the same number.
Controls
- Arrow keys โ slide all tiles (desktop)
- Swipe โ swipe on the board (mobile)
FAQ
Is my best score saved?
Yes โ saved automatically in your browser's local storage.
Can I undo a move?
Not currently. Think carefully before each swipe!
.info-details { background:#1e293b; border:1px solid #334155; border-radius:8px;
margin-bottom:0.75rem; overflow:hidden; }
.info-details summary { padding:0.75rem 1rem; font-weight:600; color:#e2e8f0; cursor:pointer;
list-style:none; display:flex; align-items:center;
justify-content:space-between; user-select:none; }
.info-details summary::-webkit-details-marker { display:none; }
.info-details summary::after { content:"+"; font-size:1.1rem; color:#64748b; }
.info-details[open] summary::after { content:"โ"; }
.info-body { padding:0.75rem 1rem 1rem; color:#94a3b8; font-size:0.875rem;
line-height:1.6; border-top:1px solid #334155; }
.info-body h4 { color:#e2e8f0; font-weight:600; margin:0.75rem 0 0.25rem; font-size:0.875rem; }
.info-body h4:first-child { margin-top:0; }
.info-body ul { padding-left:1.25rem; margin:0.25rem 0; }
.info-body li { margin-bottom:0.25rem; }
Always include two <details> blocks: How to Play (Goal, Controls desktop +
mobile, Scoring/Levels) and FAQ (min. 3 questions).
Playing Cards
Shared card component used across Blackjack, Solitaire, and FreeCell. Copy the CSS block into each card game's
<style>.
Base card โ .playing-card
<!-- Black suit -->
<div class="playing-card">
<div class="pc-corner pc-top">
<div class="pc-rank">A</div>
<div class="pc-suit">โ </div>
</div>
<div class="pc-center">โ </div>
<div class="pc-corner pc-bottom">
<div class="pc-rank">A</div>
<div class="pc-suit">โ </div>
</div>
</div>
<!-- Red suit: add .red -->
<div class="playing-card red">...</div>
<!-- Face-down: add .face-down (no inner content needed) -->
<div class="playing-card face-down"></div>
Interactive states
.selected
.hinted
.foundation-ready
.playing-card.selected { border-color:#3b82f6; box-shadow:0 0 0 2px #3b82f6, 0 0 10px rgba(59,130,246,0.5); }
.playing-card.hinted { border-color:#fbbf24; animation:pc-hint-pulse 0.9s ease-in-out infinite; }
.playing-card.foundation-ready{ border-color:#22c55e; animation:pc-foundation-pulse 0.9s ease-in-out infinite; }
@keyframes pc-hint-pulse {
0%,100% { box-shadow:0 0 0 2px #fbbf24, 0 0 8px rgba(251,191,36,0.4); }
50% { box-shadow:0 0 0 3px #fbbf24, 0 0 16px rgba(251,191,36,0.7); }
}
@keyframes pc-foundation-pulse {
0%,100% { box-shadow:0 0 0 2px #22c55e, 0 0 8px rgba(34,197,94,0.4); }
50% { box-shadow:0 0 0 3px #22c55e, 0 0 16px rgba(34,197,94,0.7); }
}
Full CSS block (copy into game <style>)
.playing-card {
width: 56px; height: 82px;
background: white;
border: 1.5px solid #d1d5db;
border-radius: 6px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px 4px;
font-weight: bold;
font-family: monospace;
color: #1e293b;
cursor: pointer;
user-select: none;
flex-shrink: 0;
position: relative;
}
.playing-card.red { color: #dc2626; }
.playing-card.face-down {
background: repeating-linear-gradient(
135deg,
#1e40af 0px, #1e40af 4px,
#1d4ed8 4px, #1d4ed8 8px
);
border-color: #1e3a8a;
cursor: default;
}
.playing-card.selected { border-color:#3b82f6; box-shadow:0 0 0 1px #3b82f6, 0 0 10px rgba(59,130,246,0.5); }
.playing-card.hinted { border-color:#fbbf24; animation:pc-hint-pulse 0.9s ease-in-out infinite; }
.playing-card.foundation-ready { border-color:#22c55e; animation:pc-foundation-pulse 0.9s ease-in-out infinite; }
@keyframes pc-hint-pulse {
0%,100% { box-shadow:0 0 0 1px #fbbf24, 0 0 8px rgba(251,191,36,0.4); }
50% { box-shadow:0 0 0 2px #fbbf24, 0 0 16px rgba(251,191,36,0.7); }
}
@keyframes pc-foundation-pulse {
0%,100% { box-shadow:0 0 0 1px #22c55e, 0 0 8px rgba(34,197,94,0.4); }
50% { box-shadow:0 0 0 2px #22c55e, 0 0 16px rgba(34,197,94,0.7); }
}
.pc-corner { display:flex; flex-direction:column; align-items:flex-start; line-height:1; gap:1px; }
.pc-corner.pc-bottom { transform:rotate(180deg); align-self:flex-end; }
.pc-rank { font-size:0.75rem; }
.pc-suit { font-size:0.9rem; }
.pc-center { position:absolute; inset:0; display:flex; align-items:center;
justify-content:center; font-size:1.5rem; pointer-events:none; }
/* Size variants */
.playing-card.sm { width:40px; height:58px; border-radius:4px; padding:2px 3px; }
.playing-card.sm .pc-rank { font-size:0.6rem; }
.playing-card.sm .pc-suit { font-size:0.7rem; }
.playing-card.sm .pc-center { font-size:0.8rem; }
.playing-card.lg { width:72px; height:105px; border-radius:8px; padding:4px 5px; }
.playing-card.lg .pc-rank { font-size:1rem; }
.playing-card.lg .pc-suit { font-size:1.1rem; }
.playing-card.lg .pc-center { font-size:1.5rem; }
Size variants
.sm โ 40ร58
default โ 56ร82
.lg โ 72ร105
<div class="playing-card sm">...</div> <!-- 40ร58 px โ compact hand/Blackjack --> <div class="playing-card">...</div> <!-- 56ร82 px โ default --> <div class="playing-card lg">...</div> <!-- 72ร105 px โ large layout -->
All three sizes share aspect ratio โ 1:1.46. For fully fluid sizing (e.g. Solitaire computing
cardW from viewport width), override width and height inline and scale font
sizes with a CSS custom property or inline style rather than using these modifier classes.
Game Wrapper
Standard layout shell for all game pages. The canvas or grid goes inside a
.canvas-wrapper which positions overlays.
#game-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start; /* โ always flex-start, never center */
min-height: calc(100vh - 60px);
gap: 1rem;
padding: 1rem;
}
Control layout order inside #game-wrapper
The d-pad must sit immediately below the board โ never put a button between the board and the d-pad, as it creates visual distance on mobile.
Canvas wrapper โ .canvas-wrapper
.canvas-wrapper {
position: relative; /* anchors #overlay and #gameover-overlay */
touch-action: none;
line-height: 0; /* removes inline-block gap under canvas */
}
/* Canvas sizing (responsive) */
const SIZE = Math.min(480, window.innerWidth - 32);
canvas.width = SIZE;
canvas.height = SIZE;
const S = SIZE / 480; /* scale factor for all game constants */