Update: more science and kid friendly

This commit is contained in:
Zakaria
2026-06-06 23:40:47 -04:00
parent 15c3a0df80
commit 0891453912
4 changed files with 759 additions and 473 deletions
Vendored
BIN
View File
Binary file not shown.
+95 -50
View File
@@ -1,60 +1,105 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Conway's Game of Life</title> <title>Life Lab: Conway's Game of Life</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<div class="container"> <main class="page">
<header> <nav class="topbar" aria-label="Life Lab navigation">
<h1>Conway's Game of Life - for Sami </h1> <div class="brand">
<p>A cellular automaton simulation with WebHook</p> <div class="mark" aria-hidden="true">L</div>
</header> <div>
<strong>Life Lab</strong>
<div class="stats-panel"> <span>Conway's Game of Life for curious families</span>
<div class="stat">
<span class="stat-label">Generation:</span>
<span id="generation">0</span>
</div>
<div class="stat">
<span class="stat-label">Population:</span>
<span id="population">0</span>
</div>
<div class="stat">
<span class="stat-label">Births:</span>
<span id="births">0</span>
</div>
<div class="stat">
<span class="stat-label">Deaths:</span>
<span id="deaths">0</span>
</div>
</div> </div>
</div>
<div class="controls"> <div class="status-pill"><i class="dot" aria-hidden="true"></i><span id="runStatus">Paused - ready to experiment</span></div>
<button id="startBtn">Start</button> </nav>
<button id="pauseBtn">Pause</button>
<button id="resetBtn">Reset</button> <section class="hero" aria-labelledby="title">
<button id="randomBtn">Random</button> <div class="intro">
<div class="speed-control"> <div>
<label for="speed">Speed:</label> <div class="eyebrow">Family science-project showcase</div>
<input type="range" id="speed" min="1" max="20" value="10"> <h1 id="title">A tiny universe where patterns wake up.</h1>
</div> <p class="lede">Click cells to build a creature, press play, and watch simple rules turn into crawlers, fireworks, traffic jams, and quiet little islands.</p>
</div> </div>
<ul class="rules" aria-label="Game of Life rules">
<div class="canvas-container"> <li><b>2-3</b><span><strong>Living cells stay alive</strong> when they have two or three neighbors nearby.</span></li>
<canvas id="gameCanvas"></canvas> <li><b>3</b><span><strong>Empty spots are born</strong> when exactly three neighbors gather around them.</span></li>
<li><b>0-1</b><span><strong>Lonely cells fade</strong>; crowded cells fade too. Balance makes the best patterns.</span></li>
</ul>
</div>
<section class="lab-card" aria-label="Interactive Game of Life board">
<div class="board-shell">
<div class="board-header">
<p class="board-title">LIVE CELL GRID - TAP OR CLICK TO DRAW</p>
<button class="button primary" id="randomBtn" type="button">Seed a tiny universe</button>
</div>
<div class="canvas-wrap">
<canvas id="lifeCanvas" width="960" height="720" aria-label="Conway's Game of Life cell board"></canvas>
</div>
<p class="caption">Tip: pause the universe, draw a few cells, then press play. The board wraps around at the edges like a donut-shaped planet.</p>
</div> </div>
</section>
<div class="presets"> </section>
<button data-preset="glider">Glider</button>
<button data-preset="spaceship">Spaceship</button> <section class="dashboard" aria-label="Simulation controls and learning prompts">
<button data-preset="pulsar">Pulsar</button> <div class="panel">
<button data-preset="gosper">Gosper Glider Gun</button> <h2>Mission control</h2>
<p class="caption">Designed for fast experimenting: change one thing, run it, then compare what happens.</p>
<div class="control-grid">
<button class="button primary" id="playBtn" type="button">Play</button>
<button class="button" id="stepBtn" type="button">Step once</button>
<button class="button" id="clearBtn" type="button">Clear board</button>
<button class="button" id="sparkBtn" type="button">Make sparkles</button>
</div> </div>
</div>
<div class="slider-row">
<script src="script.js"></script> <label for="speed"><span>Universe speed</span><strong><span id="speedLabel">8</span>/12</strong></label>
<input id="speed" type="range" min="1" max="12" value="8">
</div>
<div class="theme-row" aria-label="Cell color themes">
<button class="theme" type="button" data-theme="sprout" aria-pressed="true">
<span class="swatches"><i class="swatch-sprout-a"></i><i class="swatch-sprout-b"></i></span>
<strong>Sprout</strong><span>science green</span>
</button>
<button class="theme" type="button" data-theme="nebula" aria-pressed="false">
<span class="swatches"><i class="swatch-nebula-a"></i><i class="swatch-nebula-b"></i></span>
<strong>Nebula</strong><span>arcade space</span>
</button>
<button class="theme" type="button" data-theme="sunny" aria-pressed="false">
<span class="swatches"><i class="swatch-sunny-a"></i><i class="swatch-sunny-b"></i></span>
<strong>Sunny</strong><span>friendly sparks</span>
</button>
</div>
<p class="explain" id="lessonText">Start with random cells, then watch for shapes that travel, blink, or disappear. Ask: did the universe get calmer or wilder?</p>
</div>
<div>
<div class="stats-grid" aria-label="Live simulation statistics">
<div class="stat"><span>Generation</span><strong id="generation">0</strong></div>
<div class="stat"><span>Population</span><strong id="population">0</strong></div>
<div class="stat"><span>Births</span><strong id="births">0</strong></div>
<div class="stat"><span>Fades</span><strong id="deaths">0</strong></div>
</div>
<section class="prompt-card">
<h2>Try this together</h2>
<ul class="prompt-list">
<li><strong>Prediction round:</strong> draw ten cells and guess if the population will grow or shrink after five steps.</li>
<li><strong>Pattern hunt:</strong> look for blinkers, tiny spaceships, and stable islands that refuse to move.</li>
<li><strong>Color lab:</strong> switch themes and ask which one makes movement easiest to spot from across the room.</li>
</ul>
</section>
</div>
</section>
</main>
<script src="script.js"></script>
</body> </body>
</html> </html>
+207 -297
View File
@@ -1,299 +1,209 @@
// Initialize the game when the page loads const canvas = document.getElementById('lifeCanvas');
document.addEventListener('DOMContentLoaded', () => { const ctx = canvas.getContext('2d');
// Create game with 80x60 grid and 10px cells const cols = 80;
const game = new GameOfLife(80, 60, 10); const rows = 60;
const cellSize = canvas.width / cols;
let grid = makeGrid();
let running = false;
let generation = 0;
let births = 0;
let deaths = 0;
let timer = null;
let speed = 8;
const themeTokens = {
sprout: ['oklch(58% 0.16 145)', 'oklch(72% 0.14 170)'],
nebula: ['oklch(60% 0.18 285)', 'oklch(70% 0.14 235)'],
sunny: ['oklch(70% 0.15 65)', 'oklch(74% 0.12 35)']
};
function makeGrid() {
return Array.from({ length: rows }, () => Array(cols).fill(0));
}
function randomize(chance = 0.28) {
grid = grid.map(row => row.map(() => (Math.random() < chance ? 1 : 0)));
generation = 0;
births = 0;
deaths = 0;
updateLesson('A fresh universe appeared. Press Play and watch which clusters survive the first few generations.');
updateStats();
draw();
}
function sparkles() {
grid = makeGrid();
const centerX = Math.floor(cols / 2);
const centerY = Math.floor(rows / 2);
const points = [[0,0], [1,0], [-1,0], [0,1], [0,-1], [2,1], [-2,-1], [4,0], [4,1], [4,-1], [-4,0], [-4,1], [-4,-1], [7,2], [8,2], [7,3], [8,3]];
points.forEach(([x, y]) => { grid[centerY + y][centerX + x] = 1; });
generation = 0;
births = 0;
deaths = 0;
updateLesson('Sparkles loaded. Step slowly and look for parts that blink while other parts float away.');
updateStats();
draw();
}
function neighbors(x, y) {
let total = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const nx = (x + dx + cols) % cols;
const ny = (y + dy + rows) % rows;
total += grid[ny][nx];
}
}
return total;
}
function step() {
const next = makeGrid();
births = 0;
deaths = 0;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const n = neighbors(x, y);
const alive = grid[y][x] === 1;
const nextAlive = alive ? (n === 2 || n === 3) : n === 3;
next[y][x] = nextAlive ? 1 : 0;
if (!alive && nextAlive) births++;
if (alive && !nextAlive) deaths++;
}
}
grid = next;
generation++;
updateStats();
draw();
}
function draw() {
const styles = getComputedStyle(document.documentElement);
const cellA = styles.getPropertyValue('--cell-a').trim();
const cellB = styles.getPropertyValue('--cell-b').trim();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = styles.getPropertyValue('--surface').trim();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = styles.getPropertyValue('--grid').trim();
ctx.lineWidth = 0.7;
for (let x = 0; x <= cols; x++) {
ctx.beginPath();
ctx.moveTo(x * cellSize, 0);
ctx.lineTo(x * cellSize, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= rows; y++) {
ctx.beginPath();
ctx.moveTo(0, y * cellSize);
ctx.lineTo(canvas.width, y * cellSize);
ctx.stroke();
}
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, cellA);
gradient.addColorStop(1, cellB);
ctx.fillStyle = gradient;
ctx.shadowColor = cellA;
ctx.shadowBlur = 8;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
if (grid[y][x]) {
const inset = 1.4;
ctx.beginPath();
ctx.roundRect(x * cellSize + inset, y * cellSize + inset, cellSize - inset * 2, cellSize - inset * 2, 3);
ctx.fill();
}
}
}
ctx.shadowBlur = 0;
}
function population() {
return grid.reduce((sum, row) => sum + row.reduce((rowSum, cell) => rowSum + cell, 0), 0);
}
function updateStats() {
document.getElementById('generation').textContent = generation;
document.getElementById('population').textContent = population();
document.getElementById('births').textContent = births;
document.getElementById('deaths').textContent = deaths;
}
function updateLesson(text) {
document.getElementById('lessonText').textContent = text;
}
function play() {
running = true;
document.getElementById('playBtn').textContent = 'Pause';
document.getElementById('runStatus').textContent = 'Running - universe is alive';
loop();
}
function pause() {
running = false;
document.getElementById('playBtn').textContent = 'Play';
document.getElementById('runStatus').textContent = 'Paused - ready to experiment';
if (timer) window.clearTimeout(timer);
}
function loop() {
if (!running) return;
step();
timer = window.setTimeout(() => requestAnimationFrame(loop), 620 - speed * 45);
}
function setTheme(name) {
const [a, b] = themeTokens[name];
document.documentElement.style.setProperty('--cell-a', a);
document.documentElement.style.setProperty('--cell-b', b);
document.documentElement.style.setProperty('--accent', a);
document.querySelectorAll('.theme').forEach(button => {
button.setAttribute('aria-pressed', String(button.dataset.theme === name));
});
updateLesson(name === 'sprout' ? 'Sprout mode keeps the science-lab look: calm, clear, and easy to read.' : name === 'nebula' ? 'Nebula mode feels more arcade-like. It is great for spotting moving patterns.' : 'Sunny mode is warm and playful for younger scientists presenting to family.');
draw();
}
canvas.addEventListener('pointerdown', event => {
pause();
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = Math.floor((event.clientX - rect.left) * scaleX / cellSize);
const y = Math.floor((event.clientY - rect.top) * scaleY / cellSize);
if (x >= 0 && x < cols && y >= 0 && y < rows) {
grid[y][x] = grid[y][x] ? 0 : 1;
births = 0;
deaths = 0;
updateLesson('You drew on the board. Press Step once to test your prediction before running it fast.');
updateStats();
draw();
}
}); });
class GameOfLife { document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play());
constructor(width, height, cellSize = 10) { document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); });
this.width = width; document.getElementById('clearBtn').addEventListener('click', () => { pause(); grid = makeGrid(); generation = 0; births = 0; deaths = 0; updateLesson('The board is clear. Draw a creature, then test if it survives.'); updateStats(); draw(); });
this.height = height; document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28));
this.cellSize = cellSize; document.getElementById('sparkBtn').addEventListener('click', sparkles);
this.grid = this.createGrid(); document.getElementById('speed').addEventListener('input', event => {
this.nextGrid = this.createGrid(); speed = Number(event.target.value);
this.running = false; document.getElementById('speedLabel').textContent = speed;
this.generation = 0; });
this.births = 0; document.querySelectorAll('.theme').forEach(button => button.addEventListener('click', () => setTheme(button.dataset.theme)));
this.deaths = 0;
this.animationId = null; if (!CanvasRenderingContext2D.prototype.roundRect) {
this.speed = 10; CanvasRenderingContext2D.prototype.roundRect = function roundRect(x, y, width, height, radius) {
this.moveTo(x + radius, y);
// Canvas setup this.arcTo(x + width, y, x + width, y + height, radius);
this.canvas = document.getElementById('gameCanvas'); this.arcTo(x + width, y + height, x, y + height, radius);
this.ctx = this.canvas.getContext('2d'); this.arcTo(x, y + height, x, y, radius);
this.canvas.width = width * cellSize; this.arcTo(x, y, x + width, y, radius);
this.canvas.height = height * cellSize; return this;
};
// Event listeners }
this.setupEventListeners();
this.setupPresets(); randomize(0.18);
// Initial draw
this.draw();
this.updateStats();
}
createGrid() {
return Array(this.height).fill().map(() => Array(this.width).fill(0));
}
randomize() {
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
this.grid[y][x] = Math.random() > 0.7 ? 1 : 0;
}
}
this.generation = 0;
this.draw();
}
setupEventListeners() {
// Start/Pause/Reset buttons
document.getElementById('startBtn').addEventListener('click', () => this.start());
document.getElementById('pauseBtn').addEventListener('click', () => this.pause());
document.getElementById('resetBtn').addEventListener('click', () => this.reset());
document.getElementById('randomBtn').addEventListener('click', () => this.randomize());
// Speed control
document.getElementById('speed').addEventListener('input', (e) => {
this.speed = parseInt(e.target.value);
});
// Canvas click to toggle cells
this.canvas.addEventListener('click', (e) => {
if (this.running) return;
const rect = this.canvas.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / this.cellSize);
const y = Math.floor((e.clientY - rect.top) / this.cellSize);
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this.grid[y][x] = this.grid[y][x] ? 0 : 1;
this.draw();
}
});
}
setupPresets() {
document.querySelector('[data-preset="glider"]').addEventListener('click', () => {
this.reset();
// Glider pattern
this.grid[5][5] = 1;
this.grid[6][6] = 1;
this.grid[6][7] = 1;
this.grid[5][7] = 1;
this.grid[4][7] = 1;
this.draw();
});
document.querySelector('[data-preset="spaceship"]').addEventListener('click', () => {
this.reset();
// Lightweight spaceship
this.grid[10][10] = 1;
this.grid[10][13] = 1;
this.grid[11][9] = 1;
this.grid[11][13] = 1;
this.grid[12][9] = 1;
this.grid[12][10] = 1;
this.grid[12][11] = 1;
this.grid[12][12] = 1;
this.draw();
});
document.querySelector('[data-preset="pulsar"]').addEventListener('click', () => {
this.reset();
// Pulsar pattern
const pulsarPoints = [
[2,4],[2,5],[2,6],[2,10],[2,11],[2,12],
[4,2],[4,7],[4,9],[4,14],
[5,2],[5,7],[5,9],[5,14],
[6,2],[6,7],[6,9],[6,14],
[7,4],[7,5],[7,6],[7,10],[7,11],[7,12],
[9,4],[9,5],[9,6],[9,10],[9,11],[9,12],
[10,2],[10,7],[10,9],[10,14],
[11,2],[11,7],[11,9],[11,14],
[12,2],[12,7],[12,9],[12,14],
[14,4],[14,5],[14,6],[14,10],[14,11],[14,12]
];
pulsarPoints.forEach(([y, x]) => {
this.grid[y][x] = 1;
});
this.draw();
});
document.querySelector('[data-preset="gosper"]').addEventListener('click', () => {
this.reset();
// Gosper glider gun
const gunPoints = [
[5,1],[5,2],[6,1],[6,2],
[5,11],[6,11],[7,11],
[4,12],[8,12],
[3,13],[9,13],
[3,14],[9,14],
[6,15],
[4,16],[8,16],
[5,17],[6,17],[7,17],
[6,18],
[3,21],[4,21],[5,21],
[3,22],[4,22],[5,22],
[2,23],[6,23],
[1,25],[2,25],[6,25],[7,25],
[3,35],[4,35],[3,36],[4,36]
];
gunPoints.forEach(([y, x]) => {
this.grid[y][x] = 1;
});
this.draw();
});
}
countNeighbors(x, y) {
let count = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const nx = (x + dx + this.width) % this.width;
const ny = (y + dy + this.height) % this.height;
count += this.grid[ny][nx];
}
}
return count;
}
update() {
// Reset counters
this.births = 0;
this.deaths = 0;
// Compute next generation
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const neighbors = this.countNeighbors(x, y);
const cell = this.grid[y][x];
let nextState;
// Apply Conway's Game of Life rules
if (cell === 1) {
nextState = (neighbors === 2 || neighbors === 3) ? 1 : 0;
if (nextState === 0) this.deaths++;
} else {
nextState = (neighbors === 3) ? 1 : 0;
if (nextState === 1) this.births++;
}
this.nextGrid[y][x] = nextState;
}
}
// Swap grids
[this.grid, this.nextGrid] = [this.nextGrid, this.grid];
this.generation++;
// Update stats display
this.updateStats();
}
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw live cells with gradient
const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, this.canvas.height);
gradient.addColorStop(0, '#4facfe');
gradient.addColorStop(1, '#00f2fe');
this.ctx.fillStyle = gradient;
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
if (this.grid[y][x] === 1) {
this.ctx.fillRect(
x * this.cellSize,
y * this.cellSize,
this.cellSize - 1,
this.cellSize - 1
);
}
}
}
// Draw grid lines
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
this.ctx.lineWidth = 0.5;
// Vertical lines
for (let x = 0; x <= this.width; x++) {
this.ctx.beginPath();
this.ctx.moveTo(x * this.cellSize, 0);
this.ctx.lineTo(x * this.cellSize, this.canvas.height);
this.ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= this.height; y++) {
this.ctx.beginPath();
this.ctx.moveTo(0, y * this.cellSize);
this.ctx.lineTo(this.canvas.width, y * this.cellSize);
this.ctx.stroke();
}
}
updateStats() {
document.getElementById('generation').textContent = this.generation;
document.getElementById('population').textContent = this.getPopulation();
document.getElementById('births').textContent = this.births;
document.getElementById('deaths').textContent = this.deaths;
}
getPopulation() {
let count = 0;
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
count += this.grid[y][x];
}
}
return count;
}
gameLoop() {
this.update();
this.draw();
// Adjust speed (higher value = faster)
const speedFactor = (21 - this.speed) * 20;
setTimeout(() => {
if (this.running) {
this.animationId = requestAnimationFrame(() => this.gameLoop());
}
}, speedFactor);
}
start() {
if (!this.running) {
this.running = true;
this.gameLoop();
}
}
pause() {
this.running = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
reset() {
this.pause();
this.grid = this.createGrid();
this.nextGrid = this.createGrid();
this.generation = 0;
this.births = 0;
this.deaths = 0;
this.updateStats();
this.draw();
}
}
+457 -126
View File
@@ -1,164 +1,495 @@
* { :root {
margin: 0; --bg: oklch(98% 0.005 250);
padding: 0; --surface: oklch(100% 0 0);
box-sizing: border-box; --fg: oklch(22% 0.02 240);
--muted: oklch(50% 0.018 240);
--border: oklch(90% 0.008 240);
--accent: oklch(58% 0.16 145);
--font-display: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
--font-body: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
--cell-a: oklch(58% 0.16 145);
--cell-b: oklch(72% 0.14 170);
--cell-glow: color-mix(in oklch, var(--cell-a), white 38%);
--grid: color-mix(in oklch, var(--border), var(--fg) 8%);
} }
* { box-sizing: border-box; }
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); min-height: 100vh;
color: white; background:
min-height: 100vh; radial-gradient(circle at 16% 12%, color-mix(in oklch, var(--accent), transparent 84%), transparent 30rem),
padding: 20px; linear-gradient(135deg, color-mix(in oklch, var(--bg), white 60%), var(--bg));
color: var(--fg);
font-family: var(--font-body);
font-variant-numeric: tabular-nums;
} }
.container { button, input { font: inherit; }
max-width: 1200px;
margin: 0 auto; .page {
text-align: center; width: min(1440px, calc(100% - 32px));
margin: 0 auto;
padding: 24px 0 48px;
} }
header { .topbar {
margin-bottom: 20px; display: flex;
padding: 20px; align-items: center;
background: rgba(0, 0, 0, 0.3); justify-content: space-between;
border-radius: 15px; gap: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); padding: 14px 16px;
backdrop-filter: blur(10px); border: 1px solid var(--border);
border-radius: 18px;
background: color-mix(in oklch, var(--surface), transparent 12%);
box-shadow: 0 18px 50px color-mix(in oklch, var(--fg), transparent 92%);
backdrop-filter: blur(18px);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.mark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border: 1px solid color-mix(in oklch, var(--accent), var(--border) 45%);
border-radius: 13px;
background: color-mix(in oklch, var(--accent), white 86%);
color: var(--fg);
font-family: var(--font-mono);
font-weight: 800;
box-shadow: 0 0 0 5px color-mix(in oklch, var(--accent), transparent 88%);
}
.brand strong {
display: block;
font-family: var(--font-display);
font-size: 17px;
letter-spacing: -0.02em;
}
.brand span {
display: block;
color: var(--muted);
font-size: 13px;
margin-top: 2px;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 38px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
color: var(--muted);
font-family: var(--font-mono);
font-size: 12px;
white-space: nowrap;
}
.dot {
width: 9px;
height: 9px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 5px color-mix(in oklch, var(--accent), transparent 82%);
}
.hero {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(390px, 1.35fr);
gap: 22px;
align-items: stretch;
margin-top: 22px;
}
.intro,
.lab-card,
.panel,
.prompt-card {
border: 1px solid var(--border);
border-radius: 22px;
background: color-mix(in oklch, var(--surface), transparent 4%);
box-shadow: 0 24px 60px color-mix(in oklch, var(--fg), transparent 93%);
}
.intro {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 580px;
padding: clamp(24px, 4vw, 42px);
overflow: hidden;
position: relative;
}
.intro::after {
content: "";
position: absolute;
inset-inline-end: -110px;
inset-block-end: -120px;
width: 290px;
height: 290px;
border: 1px dashed color-mix(in oklch, var(--accent), transparent 40%);
border-radius: 50%;
opacity: 0.55;
}
.eyebrow {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 8px;
padding: 7px 10px;
border: 1px solid color-mix(in oklch, var(--accent), var(--border) 45%);
border-radius: 999px;
color: color-mix(in oklch, var(--accent), var(--fg) 18%);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
} }
h1 { h1 {
font-size: 2.5rem; max-width: 780px;
margin-bottom: 10px; margin: 24px 0 18px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); font-family: var(--font-display);
font-size: clamp(42px, 7vw, 86px);
line-height: 0.96;
letter-spacing: -0.045em;
} }
p { .lede {
font-size: 1.1rem; max-width: 54ch;
opacity: 0.9; margin: 0;
color: var(--muted);
font-size: clamp(17px, 1.5vw, 20px);
line-height: 1.55;
text-wrap: pretty;
} }
.stats-panel { .rules {
display: flex; display: grid;
justify-content: center; gap: 10px;
flex-wrap: wrap; margin-top: 32px;
gap: 20px; padding: 0;
margin-bottom: 20px; list-style: none;
padding: 15px; color: var(--fg);
background: rgba(0, 0, 0, 0.2); position: relative;
border-radius: 15px; z-index: 1;
font-size: 1.1rem; }
.rules li {
display: grid;
grid-template-columns: 36px 1fr;
gap: 12px;
align-items: start;
padding: 14px;
border: 1px solid var(--border);
border-radius: 16px;
background: color-mix(in oklch, var(--bg), var(--surface) 48%);
}
.rules b {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 12px;
background: color-mix(in oklch, var(--accent), white 84%);
color: color-mix(in oklch, var(--accent), black 30%);
font-family: var(--font-mono);
font-size: 13px;
}
.rules span {
color: var(--muted);
line-height: 1.45;
}
.lab-card {
padding: clamp(14px, 2vw, 22px);
min-width: 0;
}
.board-shell {
display: grid;
gap: 14px;
height: 100%;
}
.board-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.board-title {
margin: 0;
font-size: 15px;
color: var(--muted);
font-family: var(--font-mono);
}
.canvas-wrap {
display: grid;
place-items: center;
min-height: 450px;
border: 1px solid var(--border);
border-radius: 18px;
background:
linear-gradient(90deg, color-mix(in oklch, var(--grid), transparent 45%) 1px, transparent 1px),
linear-gradient(color-mix(in oklch, var(--grid), transparent 45%) 1px, transparent 1px),
color-mix(in oklch, var(--surface), var(--bg) 34%);
background-size: 28px 28px;
overflow: hidden;
position: relative;
}
.canvas-wrap::before {
content: "";
position: absolute;
inset: 10px;
border: 1px dashed color-mix(in oklch, var(--accent), transparent 70%);
border-radius: 14px;
pointer-events: none;
}
canvas {
width: min(100%, 820px);
height: auto;
max-height: 70vh;
aspect-ratio: 1.333;
border-radius: 14px;
background: color-mix(in oklch, var(--surface), var(--bg) 26%);
cursor: crosshair;
touch-action: manipulation;
}
.caption {
margin: 0;
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
.dashboard {
display: grid;
grid-template-columns: 1fr 1.15fr;
gap: 22px;
margin-top: 22px;
align-items: start;
}
.panel {
padding: 18px;
}
.panel h2,
.prompt-card h2 {
margin: 0 0 12px;
font-size: clamp(22px, 2.4vw, 34px);
line-height: 1.06;
letter-spacing: -0.03em;
}
.control-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 16px;
}
.button {
min-height: 48px;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
color: var(--fg);
font-weight: 750;
cursor: pointer;
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
}
.button:hover { transform: translateY(-1px); border-color: color-mix(in oklch, var(--accent), var(--border) 40%); }
.button:active { transform: translateY(0); }
.button.primary {
background: color-mix(in oklch, var(--accent), white 82%);
border-color: color-mix(in oklch, var(--accent), var(--border) 22%);
color: color-mix(in oklch, var(--fg), var(--accent) 22%);
}
.theme-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 10px;
}
.theme {
min-height: 76px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
cursor: pointer;
text-align: start;
}
.theme[aria-pressed="true"] {
border-color: var(--accent);
box-shadow: inset 0 0 0 1px var(--accent);
}
.swatches {
display: flex;
gap: 5px;
margin-bottom: 9px;
}
.swatches i {
width: 18px;
height: 18px;
border-radius: 6px;
border: 1px solid color-mix(in oklch, var(--border), var(--fg) 12%);
}
.swatch-sprout-a { background: oklch(58% 0.16 145); }
.swatch-sprout-b { background: oklch(72% 0.14 170); }
.swatch-nebula-a { background: oklch(60% 0.18 285); }
.swatch-nebula-b { background: oklch(70% 0.14 235); }
.swatch-sunny-a { background: oklch(70% 0.15 65); }
.swatch-sunny-b { background: oklch(74% 0.12 35); }
.theme strong { display: block; font-size: 13px; }
.theme span { display: block; color: var(--muted); font-size: 12px; margin-top: 3px; }
.slider-row {
display: grid;
gap: 10px;
margin-top: 16px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: color-mix(in oklch, var(--bg), var(--surface) 56%);
}
.slider-row label {
display: flex;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-family: var(--font-mono);
font-size: 12px;
}
input[type="range"] {
width: 100%;
accent-color: var(--accent);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
} }
.stat { .stat {
display: flex; min-height: 98px;
flex-direction: column; padding: 14px;
align-items: center; border: 1px solid var(--border);
border-radius: 16px;
background: var(--surface);
} }
.stat-label { .stat span {
font-weight: 600; display: block;
margin-bottom: 5px; color: var(--muted);
color: #4facfe; font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
} }
.controls { .stat strong {
display: flex; display: block;
justify-content: center; margin-top: 12px;
flex-wrap: wrap; font-family: var(--font-mono);
gap: 15px; font-size: clamp(24px, 3vw, 40px);
margin-bottom: 30px; line-height: 1;
padding: 20px; letter-spacing: -0.04em;
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
} }
button { .prompt-card {
padding: 12px 24px; margin-top: 10px;
font-size: 1rem; padding: 18px;
background: rgba(255, 255, 255, 0.15);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
} }
button:hover { .prompt-list {
background: rgba(255, 255, 255, 0.25); display: grid;
transform: translateY(-2px); gap: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); padding: 0;
margin: 14px 0 0;
list-style: none;
} }
button:active { .prompt-list li {
transform: translateY(0); padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: color-mix(in oklch, var(--bg), white 50%);
color: var(--muted);
line-height: 1.5;
} }
.speed-control { .prompt-list strong {
display: flex; color: var(--fg);
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.1);
padding: 10px 20px;
border-radius: 50px;
} }
.speed-control label { .explain {
font-weight: 500; margin: 14px 0 0;
padding: 14px;
border-inline-start: 4px solid var(--accent);
border-radius: 12px;
background: color-mix(in oklch, var(--accent), white 90%);
color: color-mix(in oklch, var(--fg), var(--accent) 15%);
line-height: 1.5;
} }
.speed-control input { @media (max-width: 1020px) {
width: 150px; .hero,
.dashboard { grid-template-columns: 1fr; }
.intro { min-height: auto; }
.canvas-wrap { min-height: 360px; }
} }
.canvas-container { @media (max-width: 680px) {
display: flex; .page { width: min(100% - 20px, 1440px); padding-top: 10px; }
justify-content: center; .topbar { align-items: flex-start; border-radius: 16px; }
margin-bottom: 30px; .status-pill { display: none; }
.intro, .lab-card, .panel, .prompt-card { border-radius: 18px; }
.control-grid, .theme-row, .stats-grid { grid-template-columns: 1fr; }
.canvas-wrap { min-height: 260px; }
h1 { font-size: clamp(38px, 13vw, 58px); }
} }
#gameCanvas { @media (prefers-reduced-motion: reduce) {
background: rgba(0, 0, 0, 0.2); *, *::before, *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
border-radius: 8px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
} }
.presets {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 15px;
padding: 20px;
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
}
.presets button {
background: rgba(255, 255, 255, 0.1);
}
.presets button:hover {
background: rgba(255, 255, 255, 0.2);
}
@media (max-width: 768px) {
.controls, .presets {
flex-direction: column;
align-items: center;
}
button {
width: 100%;
max-width: 300px;
}
.speed-control {
width: 100%;
max-width: 300px;
}
.stats-panel {
gap: 10px;
}
}