+
+
Family science-project showcase
+
A tiny universe where patterns wake up.
+
Click cells to build a creature, press play, and watch simple rules turn into crawlers, fireworks, traffic jams, and quiet little islands.
-
-
-
+
+ - 2-3Living cells stay alive when they have two or three neighbors nearby.
+ - 3Empty spots are born when exactly three neighbors gather around them.
+ - 0-1Lonely cells fade; crowded cells fade too. Balance makes the best patterns.
+
+
+
+
+
+
+
+
+
+
Tip: pause the universe, draw a few cells, then press play. The board wraps around at the edges like a donut-shaped planet.
-
-
-
-
-
-
+
+
+
+
+
+
Mission control
+
Designed for fast experimenting: change one thing, run it, then compare what happens.
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Start with random cells, then watch for shapes that travel, blink, or disappear. Ask: did the universe get calmer or wilder?
+
+
+
+
+
Generation0
+
Population0
+
Births0
+
Fades0
+
+
+ Try this together
+
+ - Prediction round: draw ten cells and guess if the population will grow or shrink after five steps.
+ - Pattern hunt: look for blinkers, tiny spaceships, and stable islands that refuse to move.
+ - Color lab: switch themes and ask which one makes movement easiest to spot from across the room.
+
+
+
+
+
+
+
diff --git a/script.js b/script.js
index 1383342..a46711a 100644
--- a/script.js
+++ b/script.js
@@ -1,299 +1,209 @@
-// Initialize the game when the page loads
-document.addEventListener('DOMContentLoaded', () => {
- // Create game with 80x60 grid and 10px cells
- const game = new GameOfLife(80, 60, 10);
+const canvas = document.getElementById('lifeCanvas');
+const ctx = canvas.getContext('2d');
+const cols = 80;
+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 {
- constructor(width, height, cellSize = 10) {
- this.width = width;
- this.height = height;
- this.cellSize = cellSize;
- this.grid = this.createGrid();
- this.nextGrid = this.createGrid();
- this.running = false;
- this.generation = 0;
- this.births = 0;
- this.deaths = 0;
- this.animationId = null;
- this.speed = 10;
-
- // Canvas setup
- this.canvas = document.getElementById('gameCanvas');
- this.ctx = this.canvas.getContext('2d');
- this.canvas.width = width * cellSize;
- this.canvas.height = height * cellSize;
-
- // Event listeners
- this.setupEventListeners();
- this.setupPresets();
-
- // 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();
- }
-}
\ No newline at end of file
+document.getElementById('playBtn').addEventListener('click', () => running ? pause() : play());
+document.getElementById('stepBtn').addEventListener('click', () => { pause(); step(); updateLesson('One generation advanced. Did the population go up, down, or stay balanced?'); });
+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(); });
+document.getElementById('randomBtn').addEventListener('click', () => randomize(0.28));
+document.getElementById('sparkBtn').addEventListener('click', sparkles);
+document.getElementById('speed').addEventListener('input', event => {
+ speed = Number(event.target.value);
+ document.getElementById('speedLabel').textContent = speed;
+});
+document.querySelectorAll('.theme').forEach(button => button.addEventListener('click', () => setTheme(button.dataset.theme)));
+
+if (!CanvasRenderingContext2D.prototype.roundRect) {
+ CanvasRenderingContext2D.prototype.roundRect = function roundRect(x, y, width, height, radius) {
+ this.moveTo(x + radius, y);
+ this.arcTo(x + width, y, x + width, y + height, radius);
+ this.arcTo(x + width, y + height, x, y + height, radius);
+ this.arcTo(x, y + height, x, y, radius);
+ this.arcTo(x, y, x + width, y, radius);
+ return this;
+ };
+}
+
+randomize(0.18);
diff --git a/styles.css b/styles.css
index 5d7dc51..74ceba1 100644
--- a/styles.css
+++ b/styles.css
@@ -1,164 +1,495 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
+:root {
+ --bg: oklch(98% 0.005 250);
+ --surface: oklch(100% 0 0);
+ --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 {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
- color: white;
- min-height: 100vh;
- padding: 20px;
+ margin: 0;
+ min-height: 100vh;
+ background:
+ radial-gradient(circle at 16% 12%, color-mix(in oklch, var(--accent), transparent 84%), transparent 30rem),
+ 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 {
- max-width: 1200px;
- margin: 0 auto;
- text-align: center;
+button, input { font: inherit; }
+
+.page {
+ width: min(1440px, calc(100% - 32px));
+ margin: 0 auto;
+ padding: 24px 0 48px;
}
-header {
- margin-bottom: 20px;
- padding: 20px;
- background: rgba(0, 0, 0, 0.3);
- border-radius: 15px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- backdrop-filter: blur(10px);
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 14px 16px;
+ 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 {
- font-size: 2.5rem;
- margin-bottom: 10px;
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ max-width: 780px;
+ margin: 24px 0 18px;
+ font-family: var(--font-display);
+ font-size: clamp(42px, 7vw, 86px);
+ line-height: 0.96;
+ letter-spacing: -0.045em;
}
-p {
- font-size: 1.1rem;
- opacity: 0.9;
+.lede {
+ max-width: 54ch;
+ margin: 0;
+ color: var(--muted);
+ font-size: clamp(17px, 1.5vw, 20px);
+ line-height: 1.55;
+ text-wrap: pretty;
}
-.stats-panel {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- gap: 20px;
- margin-bottom: 20px;
- padding: 15px;
- background: rgba(0, 0, 0, 0.2);
- border-radius: 15px;
- font-size: 1.1rem;
+.rules {
+ display: grid;
+ gap: 10px;
+ margin-top: 32px;
+ padding: 0;
+ list-style: none;
+ color: var(--fg);
+ position: relative;
+ z-index: 1;
+}
+
+.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 {
- display: flex;
- flex-direction: column;
- align-items: center;
+ min-height: 98px;
+ padding: 14px;
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ background: var(--surface);
}
-.stat-label {
- font-weight: 600;
- margin-bottom: 5px;
- color: #4facfe;
+.stat span {
+ display: block;
+ color: var(--muted);
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
}
-.controls {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
- gap: 15px;
- margin-bottom: 30px;
- padding: 20px;
- background: rgba(0, 0, 0, 0.2);
- border-radius: 15px;
+.stat strong {
+ display: block;
+ margin-top: 12px;
+ font-family: var(--font-mono);
+ font-size: clamp(24px, 3vw, 40px);
+ line-height: 1;
+ letter-spacing: -0.04em;
}
-button {
- padding: 12px 24px;
- font-size: 1rem;
- 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);
+.prompt-card {
+ margin-top: 10px;
+ padding: 18px;
}
-button:hover {
- background: rgba(255, 255, 255, 0.25);
- transform: translateY(-2px);
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+.prompt-list {
+ display: grid;
+ gap: 10px;
+ padding: 0;
+ margin: 14px 0 0;
+ list-style: none;
}
-button:active {
- transform: translateY(0);
+.prompt-list li {
+ 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 {
- display: flex;
- align-items: center;
- gap: 10px;
- background: rgba(255, 255, 255, 0.1);
- padding: 10px 20px;
- border-radius: 50px;
+.prompt-list strong {
+ color: var(--fg);
}
-.speed-control label {
- font-weight: 500;
+.explain {
+ 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 {
- width: 150px;
+@media (max-width: 1020px) {
+ .hero,
+ .dashboard { grid-template-columns: 1fr; }
+ .intro { min-height: auto; }
+ .canvas-wrap { min-height: 360px; }
}
-.canvas-container {
- display: flex;
- justify-content: center;
- margin-bottom: 30px;
+@media (max-width: 680px) {
+ .page { width: min(100% - 20px, 1440px); padding-top: 10px; }
+ .topbar { align-items: flex-start; border-radius: 16px; }
+ .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 {
- background: rgba(0, 0, 0, 0.2);
- border-radius: 8px;
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
}
-
-.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;
- }
-}
\ No newline at end of file