clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

feat: add playable emoji canvas interface

Dami committed 98b4918
commit
98b4918bde6f9d9169fe68d8ce889549b875b28a
parent
1a5995de9cdc056da115090df3532b76c54af603
author
Dami <dami.etoile@gmail.com>
4 files changed, +129 -0
Aindex.html33+++++++++++
Asrc/main.js93++++++++++++++++++++++++++++++
Asrc/overrides.css1+
Asrc/style.css2+
A index.html +33 -0
@@ -0,0 +1,33 @@
1+<!doctype html>2+<html lang="en">3+ <head>4+ <meta charset="UTF-8" />5+ <meta name="viewport" content="width=device-width, initial-scale=1" />6+ <meta name="theme-color" content="#f5efe4" />7+ <title>Miau Is True — rule garden</title>8+ </head>9+ <body>10+ <main class="app-shell">11+ <header class="topbar">12+ <div class="brand-mark" aria-label="Miau Is True"><span>✳</span><div><strong>MIAU IS TRUE</strong><small>rule garden / 01</small></div></div>13+ <div class="top-actions"><span class="status-dot" id="status-dot"></span><span id="status-label" role="status" aria-live="polite">READY TO EXPLORE</span><button class="icon-button" id="sound-btn" title="Toggle sound" aria-label="Toggle sound">◖</button><button class="icon-button" id="fullscreen-btn" title="Fullscreen (F)" aria-label="Toggle fullscreen">⛶</button></div>14+ </header>15+ <div class="app-layout">16+ <section class="play-column">17+ <div class="level-strip"><div><span class="eyebrow">CURRENT GARDEN</span><h1 id="level-title">Loading…</h1></div><div class="level-nav"><button class="soft-button" id="prev-level" aria-label="Previous level">←</button><span id="level-index">01 / 03</span><button class="soft-button" id="next-level" aria-label="Next level">→</button></div></div>18+ <div class="board-wrap"><canvas id="game-canvas" width="960" height="720" tabindex="0" aria-label="Puzzle board" aria-describedby="board-description"></canvas><pre id="board-description" class="sr-only"></pre><div class="win-toast" id="win-toast" hidden><span>✦</span><strong>RULES ALIGNED</strong><small>the garden is listening</small><button class="toast-button" id="continue-btn">CONTINUE →</button></div></div>19+ <div class="under-board"><div class="legend"><span><i class="legend-swatch you"></i> YOU</span><span><i class="legend-swatch word"></i> WORD</span><span><i class="legend-swatch object"></i> OBJECT</span></div><div class="move-counter"><span>STEPS</span><strong id="move-count">0</strong><button class="action-button" id="undo-btn" disabled>UNDO <kbd>Z</kbd></button><button class="action-button" id="restart-btn">RESTART <kbd>R</kbd></button></div></div>20+ <div class="mobile-controls" aria-label="Directional controls"><button data-dir="up">↑</button><div><button data-dir="left">←</button><button data-dir="down">↓</button><button data-dir="right">→</button></div></div>21+ </section>22+ <aside class="sidebar">23+ <section class="panel rules-panel"><div class="panel-heading"><div><span class="eyebrow">ACTIVE SYSTEM</span><h2>Rules in play</h2></div><span class="pulse"></span></div><div id="rules-list" class="rules-list"></div><p class="panel-note" id="hint-copy"></p></section>24+ <section class="panel controls-panel"><div class="panel-heading"><div><span class="eyebrow">FIELD NOTES</span><h2>Controls</h2></div></div><div class="key-row"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd><span>or arrow keys <em>move</em></span></div><div class="key-row"><kbd>Z</kbd><span>undo last step</span><kbd>R</kbd><span>restart garden</span></div></section>25+ <section class="panel levels-panel"><div class="panel-heading"><div><span class="eyebrow">CHOOSE A PATH</span><h2>Gardens</h2></div></div><div id="level-list" class="level-list"></div></section>26+ <section class="panel mod-panel"><div class="panel-heading"><div><span class="eyebrow">MAKE IT YOURS</span><h2>Mod loader</h2></div><span class="mod-badge">CJS</span></div><p>Load a local CommonJS level pack and grow the vocabulary.</p><label class="file-button">Import module<input id="mod-input" type="file" accept=".js,.cjs" /></label><div id="mod-status" class="mod-status" role="status">No module loaded</div></section>27+ </aside>28+ </div>29+ <footer><span>move words to rewrite the world</span><span>© 2026 / open garden</span></footer>30+ </main>31+ <script type="module" src="/src/main.js"></script>32+ </body>33+</html>
A src/main.js +93 -0
@@ -0,0 +1,93 @@
1+import { createGame } from './engine.js';2+import { levels } from './levels.js';3+import { loadMod } from './mods.js';4+import { prepare, layout } from '@chenglou/pretext';5+import './style.css';6+import './overrides.css';7+8+const canvas = document.querySelector('#game-canvas');9+const ctx = canvas.getContext('2d');10+const levelTitle = document.querySelector('#level-title');11+const levelIndex = document.querySelector('#level-index');12+const levelList = document.querySelector('#level-list');13+const rulesList = document.querySelector('#rules-list');14+const moveCount = document.querySelector('#move-count');15+const hintCopy = document.querySelector('#hint-copy');16+const winToast = document.querySelector('#win-toast');17+const statusLabel = document.querySelector('#status-label');18+const statusDot = document.querySelector('#status-dot');19+const boardDescription = document.querySelector('#board-description');20+const soundButton = document.querySelector('#sound-btn');21+const undoButton = document.querySelector('#undo-btn');22+const restartButton = document.querySelector('#restart-btn');23+const continueButton = document.querySelector('#continue-btn');24+let levelNo = 0;25+let game;26+let lastState;27+let modLevels = [];28+let modPalette = {};29+let soundOn = true;30+let hintPrepared;31+let feedback = '';32+let feedbackTimer;33+34+const glyphs = { MIAU:'🐱', CAT:'🐱', BABA:'🐱', FLAG:'⚑', ROCK:'●', WALL:'▦', WATER:'≈', GRASS:'✿', STAR:'✦', KEY:'⚿', DOOR:'▣', KEKE:'🐾' };35+const colors = { YOU:'#f06d5f', PUSH:'#efb34a', STOP:'#625e88', WIN:'#2d9c88', SINK:'#6f9ca8', DEFEAT:'#b45273' };36+const escapeHTML = value => String(value ?? '').replace(/[&<>"']/g, character => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[character]));37+38+function builtInLevels() { return Array.isArray(levels) ? levels : (levels?.levels || []); }39+function currentLevels() { return builtInLevels().concat(modLevels); }40+function activePalette() { return levelNo >= builtInLevels().length ? modPalette : {}; }41+function loadLevel(index) {42+ const list = currentLevels(); if (!list.length) return;43+ feedback = '';44+ levelNo = (index + list.length) % list.length; game = createGame(list[levelNo]); lastState = game.getState();45+ levelTitle.textContent = lastState.title; levelIndex.textContent = `${String(levelNo + 1).padStart(2,'0')} / ${String(list.length).padStart(2,'0')}`;46+ hintCopy.textContent = lastState.hint || 'Find the sentence hidden in the garden.';47+ hintPrepared = prepare(hintCopy.textContent, '12px Space Grotesk');48+ hintCopy.style.minHeight = `${Math.max(18, layout(hintPrepared, 270, 18).height)}px`;49+ winToast.hidden = !lastState.won;50+ renderLevelList(); updateUI(); render();51+}52+function renderLevelList() {53+ levelList.innerHTML = currentLevels().map((level, i) => `<button class="level-choice ${i === levelNo ? 'active' : ''}" data-level="${i}"><span>${String(i + 1).padStart(2,'0')}</span><strong>${escapeHTML(level.title || `Garden ${i + 1}`)}</strong><small>${escapeHTML(level.hint || '')}</small></button>`).join('');54+ levelList.querySelectorAll('[data-level]').forEach(button => button.addEventListener('click', () => loadLevel(Number(button.dataset.level))));55+}56+function updateUI() {57+ lastState = game.getState(); moveCount.textContent = lastState.moves; undoButton.disabled = !game.canUndo;58+ rulesList.innerHTML = lastState.rules.length ? lastState.rules.map(rule => `<div class="rule-chip"><span class="rule-subject">${escapeHTML(rule.subject)}</span><b>IS</b><span class="rule-predicate" style="--rule-color:${colors[rule.predicate] || '#6e685e'}">${escapeHTML(rule.predicate)}</span></div>`).join('') : '<div class="empty-rules">Move the word tiles to wake a rule.</div>';59+ statusLabel.textContent = feedback || (lastState.won ? 'GARDEN COMPLETE' : (lastState.objects.some(object => game.properties(object).includes('YOU')) ? 'READY TO EXPLORE' : 'NO YOU RULE ACTIVE'));60+ statusDot.classList.toggle('won', lastState.won); statusDot.classList.toggle('blocked', Boolean(feedback)); winToast.hidden = !lastState.won;61+ boardDescription.textContent = `${lastState.title}. ${lastState.width} columns by ${lastState.height} rows. ${lastState.moves} moves. ${lastState.won ? 'Won.' : 'Playing.'} Active rules: ${lastState.rules.map(rule => rule.text).join('; ') || 'none'}. Entities: ${lastState.objects.map(object => `${object.kind === 'word' ? `word ${object.text}` : object.noun} at column ${object.x + 1}, row ${object.y + 1}`).join('; ')}.`;62+}63+function drawRounded(x, y, w, h, r, fill, stroke) { ctx.beginPath(); ctx.roundRect(x, y, w, h, r); ctx.fillStyle = fill; ctx.fill(); if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = 1; ctx.stroke(); } }64+function render() {65+ if (!game) return; lastState = game.getState(); const W = canvas.width, H = canvas.height; const pad = 28; const cell = Math.min((W - pad * 2) / lastState.width, (H - pad * 2) / lastState.height); const ox = (W - cell * lastState.width) / 2, oy = (H - cell * lastState.height) / 2;66+ ctx.clearRect(0,0,W,H); ctx.fillStyle='#fbf7ef'; ctx.fillRect(0,0,W,H);67+ ctx.strokeStyle='rgba(102,91,72,.12)'; ctx.lineWidth=1;68+ for(let x=0;x<=lastState.width;x++){ctx.beginPath();ctx.moveTo(ox+x*cell,oy);ctx.lineTo(ox+x*cell,oy+lastState.height*cell);ctx.stroke()}69+ for(let y=0;y<=lastState.height;y++){ctx.beginPath();ctx.moveTo(ox,oy+y*cell);ctx.lineTo(ox+lastState.width*cell,oy+y*cell);ctx.stroke()}70+ const stacks = new Map(); lastState.objects.forEach(object => { const cellKey = `${object.x},${object.y}`; if (!stacks.has(cellKey)) stacks.set(cellKey, []); stacks.get(cellKey).push(object); });71+ const sorted=[...lastState.objects].sort((a,b)=>(a.kind==='word'?1:0)-(b.kind==='word'?1:0));72+ sorted.forEach(object => { const stack=stacks.get(`${object.x},${object.y}`); const stackIndex=stack.indexOf(object); const stackOffset=(stack.length>1 ? (stackIndex-(stack.length-1)/2)*cell*.11 : 0); const x=ox+object.x*cell+stackOffset, y=oy+object.y*cell-stackOffset*.35; const isWord=object.kind==='word'; const name=object.noun || object.text; const props=game.properties(object); const accent=([...props].map(p=>colors[p]).find(Boolean)||'#766f64');73+ if(isWord){ drawRounded(x+cell*.08,y+cell*.18,cell*.84,cell*.62,cell*.11,'#fffdf8',accent); ctx.fillStyle=accent; ctx.font=`700 ${Math.max(14,cell*.2)}px ui-monospace, monospace`; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(object.text,x+cell/2,y+cell*.49); ctx.fillStyle='rgba(80,70,55,.25)'; ctx.font=`600 ${Math.max(8,cell*.09)}px ui-monospace, monospace`; ctx.fillText('WORD',x+cell/2,y+cell*.84);74+ } else { const emoji=activePalette()[name] || glyphs[name]; ctx.shadowColor=props.includes('YOU') ? 'rgba(240,109,95,.38)' : 'transparent'; ctx.shadowBlur=props.includes('YOU') ? 10 : 0; ctx.font=`${Math.max(24,cell*.52)}px "Apple Color Emoji","Segoe UI Emoji",sans-serif`; ctx.textAlign='center'; ctx.textBaseline='middle'; if (emoji) ctx.fillText(emoji,x+cell/2,y+cell*.52); else { ctx.fillStyle=accent; ctx.font=`700 ${Math.max(13,cell*.18)}px 'DM Mono',monospace`; ctx.fillText(name,x+cell/2,y+cell*.52); } ctx.shadowBlur=0; ctx.fillStyle=accent; ctx.font=`700 ${Math.max(8,cell*.1)}px ui-monospace,monospace`; ctx.fillText(name,x+cell/2,y+cell*.87); if(props.length){ctx.strokeStyle=accent;ctx.lineWidth=2;ctx.beginPath();ctx.arc(x+cell/2,y+cell/2,cell*.36,0,Math.PI*2);ctx.stroke()} if(stack.length>1){ctx.fillStyle='#fffaf2';ctx.beginPath();ctx.arc(x+cell*.82,y+cell*.2,cell*.12,0,Math.PI*2);ctx.fill();ctx.fillStyle=accent;ctx.font=`700 ${Math.max(9,cell*.1)}px 'DM Mono',monospace`;ctx.fillText(String(stack.length),x+cell*.82,y+cell*.2)}}75+ });76+}77+function playSound(name) { if (!soundOn) return; const audio = new Audio(`/audio/${name}.ogg`); audio.volume = name === 'move' ? .18 : .24; audio.play().catch(() => {}); }78+function showFeedback(message) { feedback = message; clearTimeout(feedbackTimer); feedbackTimer = setTimeout(() => { feedback = ''; updateUI(); }, 1000); updateUI(); }79+function accepted(result) { return typeof result === 'object' && result !== null ? result.accepted : result; }80+function move(dx,dy) { if (!game || game.getState().won) return; const result=game.step(dx,dy); if(accepted(result)){feedback=''; const changedRules=result && typeof result==='object' && (result.rulesAdded?.length || result.rulesRemoved?.length); playSound(changedRules ? 'rule' : 'move'); if(game.getState().won) playSound('win'); updateUI();render()} else showFeedback('BLOCKED · try another sentence'); }81+function restart(){game?.reset(); feedback=''; playSound('rule'); updateUI(); render()}82+function undo(){if(accepted(game?.undo())){feedback=''; playSound('rule'); updateUI();render()}}83+document.addEventListener('keydown', event => { if (['INPUT','TEXTAREA','SELECT'].includes(event.target?.tagName) || event.target?.isContentEditable) return; const key=event.key.toLowerCase(); if(['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d','z','r','f'].includes(key)) event.preventDefault(); if(key==='arrowup'||key==='w')move(0,-1); if(key==='arrowdown'||key==='s')move(0,1); if(key==='arrowleft'||key==='a')move(-1,0); if(key==='arrowright'||key==='d')move(1,0); if(key==='z')undo(); if(key==='r')restart(); if(key==='f')toggleFullscreen();});84+document.querySelectorAll('[data-dir]').forEach(button=>button.addEventListener('click',()=>({up:move.bind(null,0,-1),down:move.bind(null,0,1),left:move.bind(null,-1,0),right:move.bind(null,1,0)}[button.dataset.dir])()));85+document.querySelector('#prev-level').addEventListener('click',()=>loadLevel(levelNo-1)); document.querySelector('#next-level').addEventListener('click',()=>loadLevel(levelNo+1));86+function toggleFullscreen(){if(!document.fullscreenElement) document.documentElement.requestFullscreen?.(); else document.exitFullscreen?.()}87+document.querySelector('#fullscreen-btn').addEventListener('click',toggleFullscreen);88+undoButton.addEventListener('click', undo); restartButton.addEventListener('click', restart); continueButton.addEventListener('click', () => loadLevel(levelNo + 1));89+soundButton.addEventListener('click', () => { soundOn = !soundOn; soundButton.textContent = soundOn ? '◖' : '○'; soundButton.setAttribute('aria-label', soundOn ? 'Mute sound' : 'Enable sound'); });90+document.querySelector('#mod-input').addEventListener('change',async event=>{const file=event.target.files?.[0]; if(!file) return; const status=document.querySelector('#mod-status'); try { const mod=loadMod(await file.text(),{filename:file.name}); modLevels=mod.levels; modPalette=mod.palette || {}; status.textContent=`${mod.manifest.name} · ${mod.levels.length} garden${mod.levels.length===1?'':'s'} loaded`; status.title=''; loadLevel(currentLevels().length-modLevels.length); } catch(error) { status.textContent=error.errors?.[0] || error.message; status.title=error.message; }});91+window.render_game_to_text=()=>JSON.stringify({coordinateSystem:'origin top-left; x right, y down',level:lastState?.title||'',mode:lastState?.won?'won':'play',moves:lastState?.moves||0,grid:{width:lastState?.width||0,height:lastState?.height||0},rules:lastState?.rules||[],objects:lastState?.objects?.map(({id,kind,noun,text,x,y})=>({id,kind,noun,text,x,y}))||[]});92+window.advanceTime=ms=>{render();return ms};93+loadLevel(0);
A src/overrides.css +1 -0
A src/style.css +2 -0
@@ -0,0 +1,2 @@
1+@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap');2+:root{font-family:'Space Grotesk',system-ui,sans-serif;color:#302e2a;background:#e9e1d4;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;background:radial-gradient(circle at 12% 8%,#f8f2e9 0 20%,transparent 46%),#e9e1d4}.app-shell{max-width:1480px;margin:auto;padding:28px 38px 22px}.topbar{height:57px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #c9bca9}.brand-mark{display:flex;align-items:center;gap:13px;letter-spacing:.06em}.brand-mark>span{font-size:25px;color:#e76556}.brand-mark strong{display:block;font-size:14px;line-height:1.1}.brand-mark small{display:block;margin-top:3px;font:10px 'DM Mono',monospace;letter-spacing:.13em;color:#817768}.top-actions{display:flex;align-items:center;gap:9px;color:#807464;font:10px 'DM Mono',monospace;letter-spacing:.08em}.status-dot{width:7px;height:7px;border-radius:50%;background:#e6af47;box-shadow:0 0 0 4px #f2e8d3}.status-dot.won{background:#2d9c88}.icon-button,.soft-button{border:1px solid #d0c3b2;background:#f5eee4;color:#776f64;border-radius:8px;cursor:pointer}.icon-button{font-size:17px;width:32px;height:30px;margin-left:14px}.app-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:30px;padding-top:28px}.level-strip{display:flex;align-items:flex-end;justify-content:space-between;padding:0 2px 18px}.eyebrow{font:10px 'DM Mono',monospace;letter-spacing:.13em;color:#9b8c7a}.level-strip h1{font-size:28px;line-height:1;margin:7px 0 0;font-weight:600;letter-spacing:-.04em}.level-nav{display:flex;align-items:center;gap:11px;font:10px 'DM Mono',monospace;color:#847969}.soft-button{width:29px;height:27px;font-size:16px}.board-wrap{position:relative;border:1px solid #cabdac;border-radius:15px;background:#fbf7ef;padding:14px;box-shadow:0 11px 26px rgba(103,80,53,.07)}canvas{display:block;width:100%;height:auto;max-height:calc(100vh - 270px);border-radius:8px}.under-board{display:flex;justify-content:space-between;align-items:center;padding:17px 3px 0;color:#857a6c;font:10px 'DM Mono',monospace;letter-spacing:.08em}.legend{display:flex;gap:18px}.legend span{display:flex;align-items:center;gap:6px}.legend-swatch{width:7px;height:7px;border-radius:50%;display:inline-block;background:#766f64}.legend-swatch.you{background:#e76556}.legend-swatch.word{background:#e5af47}.legend-swatch.object{background:#6f9ca8}.move-counter{display:flex;gap:10px;align-items:center}.move-counter strong{font:500 16px 'DM Mono',monospace;color:#302e2a}.sidebar{display:flex;flex-direction:column;gap:15px}.panel{background:#f6efe5;border:1px solid #d6c8b5;border-radius:12px;padding:19px 19px 18px;box-shadow:0 5px 14px rgba(104,83,59,.035)}.panel-heading{display:flex;justify-content:space-between;align-items:flex-start}.panel h2{font-size:17px;line-height:1;margin:7px 0 0;letter-spacing:-.03em}.pulse{width:8px;height:8px;background:#e6af47;border-radius:50%;box-shadow:0 0 0 5px #f0dfc5;margin:5px}.rules-list{display:flex;flex-wrap:wrap;gap:7px;margin:17px 0 13px}.rule-chip{display:flex;align-items:center;gap:7px;padding:8px 9px;border:1px solid #ded2c1;border-radius:7px;background:#fffaf2;font:11px 'DM Mono',monospace}.rule-chip b{font-size:9px;color:#ab9c89}.rule-predicate{color:var(--rule-color);font-weight:500}.empty-rules{font:11px 'DM Mono',monospace;color:#9e9180;padding:13px 0}.panel-note{margin:0;font-size:12px;line-height:1.55;color:#8c7f70}.key-row{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:14px;font:11px 'DM Mono',monospace;color:#766c60}.key-row span{margin-left:4px}.key-row em{font-style:normal;color:#aa9b88}.key-row kbd{display:inline-flex;align-items:center;justify-content:center;width:24px;height:23px;background:#fffaf2;border:1px solid #d7cab8;border-radius:5px;font-size:10px;color:#6c645a}.level-list{margin-top:12px;display:grid;gap:4px}.level-choice{display:grid;grid-template-columns:28px 1fr;grid-template-rows:16px 15px;column-gap:7px;text-align:left;border:0;border-left:2px solid transparent;background:transparent;padding:8px 7px;color:#5e574d;cursor:pointer;border-radius:0 6px 6px 0}.level-choice span{grid-row:1/3;font:11px 'DM Mono',monospace;color:#b2a28d;padding-top:2px}.level-choice strong{font-size:12px;font-weight:500}.level-choice small{font-size:10px;color:#a19382;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.level-choice.active{background:#efe4d4;border-left-color:#e76556}.level-choice.active span{color:#e76556}.mod-panel p{font-size:11px;line-height:1.5;color:#897d70;margin:13px 0}.file-button{display:inline-flex;align-items:center;justify-content:center;border:1px solid #d1c1ac;background:#fff9ef;border-radius:7px;padding:8px 11px;font:10px 'DM Mono',monospace;color:#6b6257;cursor:pointer}.file-button input{display:none}.mod-badge{font:9px 'DM Mono',monospace;color:#e76556;border:1px solid #ebbfad;border-radius:4px;padding:4px 5px}.mod-status{font:10px 'DM Mono',monospace;color:#a29483;margin-top:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.win-toast{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:grid;gap:3px;text-align:center;background:rgba(48,46,42,.94);color:#fffaf2;border-radius:10px;padding:18px 23px;box-shadow:0 10px 24px rgba(48,46,42,.2)}.win-toast span{font-size:22px;color:#e6b14d}.win-toast strong{font-size:14px;letter-spacing:.05em}.win-toast small{font:10px 'DM Mono',monospace;color:#d5c7b5}.mobile-controls{display:none}footer{display:flex;justify-content:space-between;border-top:1px solid #c9bca9;margin-top:30px;padding-top:14px;color:#9b8e7d;font:9px 'DM Mono',monospace;letter-spacing:.08em}@media(max-width:850px){.app-shell{padding:18px 15px}.topbar{height:48px}.top-actions>span:not(.status-dot){display:none}.app-layout{display:block;padding-top:18px}.sidebar{margin-top:20px}.level-strip h1{font-size:23px}.under-board{padding-top:12px}.mobile-controls{display:flex;flex-direction:column;align-items:center;gap:5px;margin:17px auto 0}.mobile-controls>button,.mobile-controls div button{width:45px;height:37px;border:1px solid #cbbda9;background:#f7efe4;color:#655d51;border-radius:7px;font-size:18px}.mobile-controls div{display:flex;gap:5px}.legend{gap:9px}.legend span{font-size:9px}footer{margin-top:23px}.sidebar{gap:11px}.panel{padding:16px}.controls-panel{display:none}}@media(min-width:851px){.play-column{min-width:0}}