From 08914539127ebccc863b3a5c28a882e224748138 Mon Sep 17 00:00:00 2001 From: Zakaria Date: Sat, 6 Jun 2026 23:40:47 -0400 Subject: [PATCH] Update: more science and kid friendly --- .DS_Store | Bin 0 -> 6148 bytes index.html | 145 ++++++++----- script.js | 504 +++++++++++++++++++-------------------------- styles.css | 583 +++++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 759 insertions(+), 473 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 + - - - Conway's Game of Life - + + + Life Lab: Conway's Game of Life + -
-
-

Conway's Game of Life - for Sami

-

A cellular automaton simulation with WebHook

-
- -
-
- Generation: - 0 -
-
- Population: - 0 -
-
- Births: - 0 -
-
- Deaths: - 0 -
+
+ + +
+
+
+
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.
  • +
+
+ +
+
+
+

LIVE CELL GRID - TAP OR CLICK TO DRAW

+ +
+
+ +
+

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