{"project":{"id":"yFeMZbk","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"Crossy Voxel","visible":true,"contributors":"","githubRepo":"davidyarham/blnq-crossy-voxel","forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Untitled</title>\n  <link rel=\"stylesheet\" href=\"style.css\">\n<script src=\"https://unpkg.com/three@0.160.0/build/three.min.js\"></script>\n<script src=\"https://unpkg.com/lucide@latest/dist/umd/lucide.js\"></script>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap\" rel=\"stylesheet\">\n</head>\n<body>\n<main class=\"game-shell\">\n\t<section class=\"hud\" aria-label=\"Game status\">\n\t\t<div class=\"hud__brand\">\n\t\t\t<div class=\"hud__title-wrap\">\n\t\t\t\t<p class=\"hud__eyebrow\">Arcade hopper</p>\n\t\t\t\t<h1 class=\"hud__title\">Crossy Voxel</h1>\n\t\t\t</div>\n\t\t\t<button class=\"hud__button\" id=\"muteButton\" type=\"button\" aria-pressed=\"false\" aria-label=\"Toggle sound\">\n        <i data-lucide=\"volume-2\"></i>\n      </button>\n\t\t</div>\n\n\t\t<div class=\"hud__stats\">\n\t\t\t<article class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">Score</span>\n\t\t\t\t<strong class=\"stat-card__value\" id=\"scoreValue\">0</strong>\n\t\t\t</article>\n\t\t\t<article class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">Best</span>\n\t\t\t\t<strong class=\"stat-card__value\" id=\"bestValue\">0</strong>\n\t\t\t</article>\n\t\t\t<article class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">State</span>\n\t\t\t\t<strong class=\"stat-card__value stat-card__value--small\" id=\"stateValue\">Ready</strong>\n\t\t\t</article>\n\t\t</div>\n\t</section>\n\n\t<section class=\"game-stage\" aria-label=\"Crossy Voxel game area\">\n\t\t<div id=\"gameCanvas\" class=\"game-stage__canvas\"></div>\n\n\t\t<div class=\"overlay overlay--start\" id=\"startOverlay\">\n\t\t\t<div class=\"overlay__panel\">\n\t\t\t\t<p class=\"overlay__kicker\">Hop. Dodge. Drift.</p>\n\t\t\t\t<h2 class=\"overlay__title\">Make it absurdly far.</h2>\n\t\t\t\t<p class=\"overlay__text\">Cross roads, ride logs, avoid bumpers, and keep climbing for a bigger score.</p>\n\t\t\t\t<div class=\"overlay__actions\">\n\t\t\t\t\t<button class=\"overlay__button overlay__button--primary\" id=\"startButton\" type=\"button\">Start game</button>\n\t\t\t\t\t<button class=\"overlay__button\" id=\"howButton\" type=\"button\">How to play</button>\n\t\t\t\t</div>\n\t\t\t\t<ul class=\"overlay__tips\" aria-label=\"Controls\">\n\t\t\t\t\t<li><i data-lucide=\"arrow-up\"></i><span>WASD or arrow keys to move</span></li>\n\t\t\t\t\t<li><i data-lucide=\"smartphone\"></i><span>Swipe or use touch buttons on mobile</span></li>\n\t\t\t\t\t<li><i data-lucide=\"waves\"></i><span>Ride logs across rivers or splash out</span></li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"overlay overlay--hidden\" id=\"gameOverOverlay\" aria-hidden=\"true\">\n\t\t\t<div class=\"overlay__panel overlay__panel--compact\">\n\t\t\t\t<p class=\"overlay__kicker\">Game over</p>\n\t\t\t\t<h2 class=\"overlay__title\">Splat happens.</h2>\n\t\t\t\t<p class=\"overlay__text\" id=\"gameOverText\">You made it 0 rows.</p>\n\t\t\t\t<div class=\"overlay__actions\">\n\t\t\t\t\t<button class=\"overlay__button overlay__button--primary\" id=\"restartButton\" type=\"button\">Hop again</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"touch-controls\" aria-label=\"Touch controls\">\n\t\t\t<button class=\"touch-controls__button touch-controls__button--up\" data-move=\"up\" type=\"button\" aria-label=\"Move up\">\n        <i data-lucide=\"arrow-up\"></i>\n      </button>\n\t\t\t<button class=\"touch-controls__button touch-controls__button--left\" data-move=\"left\" type=\"button\" aria-label=\"Move left\">\n        <i data-lucide=\"arrow-left\"></i>\n      </button>\n\t\t\t<button class=\"touch-controls__button touch-controls__button--down\" data-move=\"down\" type=\"button\" aria-label=\"Move down\">\n        <i data-lucide=\"arrow-down\"></i>\n      </button>\n\t\t\t<button class=\"touch-controls__button touch-controls__button--right\" data-move=\"right\" type=\"button\" aria-label=\"Move right\">\n        <i data-lucide=\"arrow-right\"></i>\n      </button>\n\t\t</div>\n\t</section>\n</main>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"const gameRoot = document.getElementById('gameCanvas');\nconst scoreValue = document.getElementById('scoreValue');\nconst bestValue = document.getElementById('bestValue');\nconst stateValue = document.getElementById('stateValue');\nconst startOverlay = document.getElementById('startOverlay');\nconst gameOverOverlay = document.getElementById('gameOverOverlay');\nconst gameOverText = document.getElementById('gameOverText');\nconst startButton = document.getElementById('startButton');\nconst restartButton = document.getElementById('restartButton');\nconst howButton = document.getElementById('howButton');\nconst muteButton = document.getElementById('muteButton');\nconst touchButtons = [...document.querySelectorAll('[data-move]')];\n\nconst laneWidth = 2;\nconst laneDepth = 2;\nconst worldHalfWidth = 5;\nconst visibleBack = 4;\nconst visibleAhead = 16;\nconst laneCount = visibleBack + visibleAhead + 14;\n\nlet score = 0;\nlet best = Number(localStorage.getItem('crossy-voxel-best') || 0);\nlet gameState = 'ready';\nlet muted = false;\nlet moveQueue = [];\nlet lanes = [];\nlet animationId = null;\n\nbestValue.textContent = String(best);\n\nclass AudioEngine {\n\tconstructor() {\n\t\tthis.ctx = null;\n\t\tthis.master = null;\n\t\tthis.enabled = true;\n\t}\n\n\tinit() {\n\t\tif (this.ctx) return;\n\t\tconst AudioContext = window.AudioContext || window.webkitAudioContext;\n\t\tif (!AudioContext) {\n\t\t\tthis.enabled = false;\n\t\t\treturn;\n\t\t}\n\t\tthis.ctx = new AudioContext();\n\t\tthis.master = this.ctx.createGain();\n\t\tthis.master.gain.value = 0.18;\n\t\tthis.master.connect(this.ctx.destination);\n\t}\n\n\tresume() {\n\t\tthis.init();\n\t\tif (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();\n\t}\n\n\tsetMuted(value) {\n\t\tthis.enabled = !value;\n\t\tif (this.master) this.master.gain.value = value ? 0 : 0.18;\n\t}\n\n\tbeep({\n\t\tfrequency = 440,\n\t\tduration = 0.12,\n\t\ttype = 'square',\n\t\tvolume = 0.15,\n\t\tslide = 0,\n\t\twhen = 0\n\t}) {\n\t\tif (!this.ctx || !this.enabled) return;\n\t\tconst now = this.ctx.currentTime + when;\n\t\tconst osc = this.ctx.createOscillator();\n\t\tconst gain = this.ctx.createGain();\n\t\tosc.type = type;\n\t\tosc.frequency.setValueAtTime(frequency, now);\n\t\tif (slide) osc.frequency.linearRampToValueAtTime(frequency + slide, now + duration);\n\t\tgain.gain.setValueAtTime(0.0001, now);\n\t\tgain.gain.exponentialRampToValueAtTime(volume, now + 0.01);\n\t\tgain.gain.exponentialRampToValueAtTime(0.0001, now + duration);\n\t\tosc.connect(gain);\n\t\tgain.connect(this.master);\n\t\tosc.start(now);\n\t\tosc.stop(now + duration + 0.03);\n\t}\n\n\thop() {\n\t\tthis.beep({\n\t\t\tfrequency: 420,\n\t\t\tslide: 120,\n\t\t\tduration: 0.08,\n\t\t\ttype: 'square',\n\t\t\tvolume: 0.12\n\t\t});\n\t}\n\n\tscore() {\n\t\tthis.beep({\n\t\t\tfrequency: 540,\n\t\t\tduration: 0.08,\n\t\t\ttype: 'triangle',\n\t\t\tvolume: 0.1,\n\t\t\twhen: 0\n\t\t});\n\t\tthis.beep({\n\t\t\tfrequency: 760,\n\t\t\tduration: 0.12,\n\t\t\ttype: 'triangle',\n\t\t\tvolume: 0.1,\n\t\t\twhen: 0.06\n\t\t});\n\t}\n\n\thit() {\n\t\tthis.beep({\n\t\t\tfrequency: 180,\n\t\t\tslide: -90,\n\t\t\tduration: 0.28,\n\t\t\ttype: 'sawtooth',\n\t\t\tvolume: 0.14\n\t\t});\n\t}\n\n\tsplash() {\n\t\tthis.beep({\n\t\t\tfrequency: 260,\n\t\t\tslide: -160,\n\t\t\tduration: 0.18,\n\t\t\ttype: 'triangle',\n\t\t\tvolume: 0.13\n\t\t});\n\t\tthis.beep({\n\t\t\tfrequency: 180,\n\t\t\tslide: -100,\n\t\t\tduration: 0.24,\n\t\t\ttype: 'square',\n\t\t\tvolume: 0.1,\n\t\t\twhen: 0.03\n\t\t});\n\t}\n}\n\nconst audio = new AudioEngine();\n\nconst scene = new THREE.Scene();\nscene.fog = new THREE.Fog(0x15120f, 16, 42);\n\nconst camera = new THREE.PerspectiveCamera(52, 1, 0.1, 100);\ncamera.position.set(0, 12.8, 15.8);\ncamera.lookAt(0, 0, 6.5);\n\nconst renderer = new THREE.WebGLRenderer({\n\tantialias: true,\n\talpha: true\n});\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));\nrenderer.shadowMap.enabled = true;\nrenderer.shadowMap.type = THREE.PCFSoftShadowMap;\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.08;\ngameRoot.appendChild(renderer.domElement);\n\nconst ambientLight = new THREE.HemisphereLight(0xfff4dc, 0x4b3424, 1.45);\nscene.add(ambientLight);\n\nconst sunLight = new THREE.DirectionalLight(0xffd39a, 1.75);\nsunLight.position.set(12, 20, 10);\nsunLight.castShadow = true;\nsunLight.shadow.mapSize.width = 1024;\nsunLight.shadow.mapSize.height = 1024;\nsunLight.shadow.camera.left = -18;\nsunLight.shadow.camera.right = 18;\nsunLight.shadow.camera.top = 18;\nsunLight.shadow.camera.bottom = -18;\nscene.add(sunLight);\n\nconst fillLight = new THREE.DirectionalLight(0xb8d4ff, 0.35);\nfillLight.position.set(-10, 8, 12);\nscene.add(fillLight);\n\nconst rimLight = new THREE.DirectionalLight(0xffb36b, 0.28);\nrimLight.position.set(4, 6, -10);\nscene.add(rimLight);\n\nconst world = new THREE.Group();\nscene.add(world);\n\nconst materials = {\n\tgrass: new THREE.MeshToonMaterial({\n\t\tcolor: 0x6f9f45\n\t}),\n\tgrassAlt: new THREE.MeshToonMaterial({\n\t\tcolor: 0x83b65a\n\t}),\n\tgrassEdge: new THREE.MeshToonMaterial({\n\t\tcolor: 0x4f7332\n\t}),\n\troad: new THREE.MeshToonMaterial({\n\t\tcolor: 0x2f2a2c\n\t}),\n\tshoulder: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x4a4447\n\t}),\n\twater: new THREE.MeshToonMaterial({\n\t\tcolor: 0x2b7c78\n\t}),\n\twaterEdge: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x245d58\n\t}),\n\tlaneStripe: new THREE.MeshLambertMaterial({\n\t\tcolor: 0xf3d18f\n\t}),\n\tcurb: new THREE.MeshLambertMaterial({\n\t\tcolor: 0xb8aea3\n\t}),\n\tlog: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x8b5a34\n\t}),\n\tlogRing: new THREE.MeshLambertMaterial({\n\t\tcolor: 0xb27a4f\n\t}),\n\tturtle: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x5f9a62\n\t}),\n\tturtleShell: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x314a31\n\t}),\n\tcarA: new THREE.MeshToonMaterial({\n\t\tcolor: 0xf06a2b\n\t}),\n\tcarB: new THREE.MeshToonMaterial({\n\t\tcolor: 0xf6c453\n\t}),\n\tcarC: new THREE.MeshLambertMaterial({\n\t\tcolor: 0xcf4d1f\n\t}),\n\ttruck: new THREE.MeshToonMaterial({\n\t\tcolor: 0xcab59c\n\t}),\n\twindow: new THREE.MeshToonMaterial({\n\t\tcolor: 0xd7e7f3\n\t}),\n\tbumper: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x221c1d\n\t}),\n\twheel: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x181414\n\t}),\n\tplayer: new THREE.MeshToonMaterial({\n\t\tcolor: 0xf5eedf\n\t}),\n\tplayerWing: new THREE.MeshToonMaterial({\n\t\tcolor: 0xf4d26a\n\t}),\n\tplayerBeak: new THREE.MeshToonMaterial({\n\t\tcolor: 0xee8d31\n\t}),\n\tplayerDark: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x433329\n\t}),\n\teye: new THREE.MeshLambertMaterial({\n\t\tcolor: 0x211714\n\t})\n};\n\nfunction makeBox(w, h, d, material) {\n\tconst mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);\n\tmesh.castShadow = true;\n\tmesh.receiveShadow = true;\n\treturn mesh;\n}\n\nfunction addGrassTuft(parent, x, z) {\n\tconst tuft = makeBox(0.24, 0.08, 0.24, Math.random() > 0.5 ? materials.grassAlt : materials.grass);\n\ttuft.position.set(x, 0.02, z);\n\ttuft.castShadow = false;\n\tparent.add(tuft);\n}\n\nfunction createPlayer() {\n\tconst group = new THREE.Group();\n\n\tconst feet = makeBox(0.46, 0.1, 0.34, materials.playerBeak);\n\tfeet.position.set(0, 0.05, 0.1);\n\n\tconst body = makeBox(0.9, 0.62, 0.96, materials.player);\n\tbody.position.y = 0.46;\n\n\tconst chest = makeBox(0.56, 0.26, 0.48, materials.playerWing);\n\tchest.position.set(0, 0.45, 0.34);\n\n\tconst wingLeft = makeBox(0.18, 0.34, 0.56, materials.playerWing);\n\tconst wingRight = wingLeft.clone();\n\twingLeft.position.set(-0.5, 0.48, 0.04);\n\twingRight.position.set(0.5, 0.48, 0.04);\n\n\tconst tail = makeBox(0.34, 0.22, 0.24, materials.player);\n\ttail.position.set(0, 0.45, -0.54);\n\n\tconst head = makeBox(0.66, 0.58, 0.66, materials.player);\n\thead.position.set(0, 0.95, 0.06);\n\n\tconst beak = makeBox(0.34, 0.14, 0.32, materials.playerBeak);\n\tbeak.position.set(0, 0.88, 0.48);\n\n\tconst comb = makeBox(0.16, 0.12, 0.16, materials.playerWing);\n\tcomb.position.set(0, 1.32, 0.03);\n\n\tconst eyeLeft = makeBox(0.08, 0.08, 0.08, materials.eye);\n\tconst eyeRight = eyeLeft.clone();\n\teyeLeft.position.set(-0.16, 0.98, 0.36);\n\teyeRight.position.set(0.16, 0.98, 0.36);\n\n\tgroup.add(feet, body, chest, wingLeft, wingRight, tail, head, beak, comb, eyeLeft, eyeRight);\n\treturn group;\n}\n\nfunction createTree(offsetX, z) {\n\tconst tree = new THREE.Group();\n\tconst trunk = makeBox(0.28, 0.82, 0.28, materials.log);\n\ttrunk.position.y = 0.41;\n\tconst leavesBase = makeBox(0.92, 0.52, 0.92, materials.grassEdge);\n\tleavesBase.position.y = 0.96;\n\tconst leavesTop = makeBox(0.66, 0.46, 0.66, materials.grassAlt);\n\tleavesTop.position.y = 1.38;\n\ttree.add(trunk, leavesBase, leavesTop);\n\ttree.position.set(offsetX, 0, z);\n\tworld.add(tree);\n}\n\nfor (let i = 0; i < 18; i += 1) {\n\tconst x = (Math.random() > 0.5 ? -1 : 1) * (worldHalfWidth + 1.5 + Math.random() * 3.5);\n\tconst z = -4 + i * 1.6;\n\tcreateTree(x, z);\n}\n\nconst player = {\n\tgroup: createPlayer(),\n\tlane: 0,\n\txIndex: 0,\n\tworldX: 0,\n\tworldZ: 0,\n\ttargetX: 0,\n\ttargetZ: 0,\n\tmoving: false,\n\tmoveTime: 0,\n\trideVelocity: 0\n};\nworld.add(player.group);\nplayer.group.traverse((child) => {\n\tif (child.isMesh) {\n\t\tchild.castShadow = true;\n\t\tchild.receiveShadow = true;\n\t}\n});\n\nfunction laneToWorldZ(index) {\n\treturn index * laneDepth;\n}\n\nfunction xIndexToWorldX(index) {\n\treturn index * laneWidth;\n}\n\nfunction randomFrom(list) {\n\treturn list[Math.floor(Math.random() * list.length)];\n}\n\nfunction createGroundLane(index, type) {\n\tconst lane = new THREE.Group();\n\tlane.userData.type = type;\n\tlane.userData.index = index;\n\tlane.position.z = laneToWorldZ(index);\n\n\tconst width = (worldHalfWidth * 2 + 3) * laneWidth;\n\tconst baseMaterial = type === 'road' ? materials.road : type === 'water' ? materials.water : materials.grass;\n\tconst edgeMaterial = type === 'road' ? materials.shoulder : type === 'water' ? materials.waterEdge : materials.grassEdge;\n\n\tconst base = makeBox(width, 0.35, laneDepth, baseMaterial);\n\tbase.receiveShadow = true;\n\tbase.castShadow = false;\n\tbase.position.y = -0.18;\n\tlane.add(base);\n\n\tconst edgeA = makeBox(width, 0.16, 0.12, edgeMaterial);\n\tconst edgeB = edgeA.clone();\n\tedgeA.position.set(0, 0.03, -laneDepth * 0.5 + 0.06);\n\tedgeB.position.set(0, 0.03, laneDepth * 0.5 - 0.06);\n\tlane.add(edgeA, edgeB);\n\n\tif (type === 'grass') {\n\t\tfor (let i = 0; i < 14; i += 1) {\n\t\t\taddGrassTuft(lane, (Math.random() - 0.5) * (width - 1.4), (Math.random() - 0.5) * 1.3);\n\t\t}\n\t}\n\n\tif (type === 'road') {\n\t\tfor (let x = -worldHalfWidth - 1; x <= worldHalfWidth + 1; x += 2) {\n\t\t\tconst stripe = makeBox(0.9, 0.04, 0.18, materials.laneStripe);\n\t\t\tstripe.position.set(x * laneWidth * 0.5, 0.03, 0);\n\t\t\tstripe.castShadow = false;\n\t\t\tlane.add(stripe);\n\t\t}\n\t\tconst curbLeft = makeBox(0.26, 0.08, laneDepth, materials.curb);\n\t\tconst curbRight = curbLeft.clone();\n\t\tcurbLeft.position.set(-width * 0.5 + 0.13, 0.05, 0);\n\t\tcurbRight.position.set(width * 0.5 - 0.13, 0.05, 0);\n\t\tlane.add(curbLeft, curbRight);\n\t}\n\n\tif (type === 'water') {\n\t\tfor (let x = -worldHalfWidth; x <= worldHalfWidth; x += 1) {\n\t\t\tconst ripple = makeBox(0.72, 0.03, 0.14, materials.waterEdge);\n\t\t\tripple.position.set(x * laneWidth, 0.02, Math.random() * 0.8 - 0.4);\n\t\t\tripple.castShadow = false;\n\t\t\tlane.add(ripple);\n\t\t}\n\t}\n\n\treturn lane;\n}\n\nfunction createVehicle(length, color) {\n\tconst group = new THREE.Group();\n\tconst body = makeBox(length, 0.54, 0.98, color);\n\tbody.position.y = 0.38;\n\tconst cabin = makeBox(length * 0.58, 0.36, 0.68, materials.window);\n\tcabin.position.set(0, 0.8, -0.02);\n\tconst hood = makeBox(length * 0.32, 0.16, 0.86, color);\n\thood.position.set(length * 0.14, 0.62, 0);\n\tconst bumperFront = makeBox(0.08, 0.16, 0.9, materials.bumper);\n\tconst bumperBack = bumperFront.clone();\n\tbumperFront.position.set(length * 0.5 + 0.01, 0.28, 0);\n\tbumperBack.position.set(-length * 0.5 - 0.01, 0.28, 0);\n\tconst wheelOffsets = [-length * 0.34, length * 0.34];\n\twheelOffsets.forEach((offset) => {\n\t\tconst left = makeBox(0.24, 0.24, 0.22, materials.wheel);\n\t\tconst right = left.clone();\n\t\tleft.position.set(offset, 0.12, 0.4);\n\t\tright.position.set(offset, 0.12, -0.4);\n\t\tgroup.add(left, right);\n\t});\n\tgroup.add(body, cabin, hood, bumperFront, bumperBack);\n\treturn group;\n}\n\nfunction createLog(length, turtle = false) {\n\tconst group = new THREE.Group();\n\tconst material = turtle ? materials.turtle : materials.log;\n\tconst piece = makeBox(length, 0.34, 0.9, material);\n\tpiece.position.y = 0.2;\n\tgroup.add(piece);\n\n\tif (turtle) {\n\t\tconst shell = makeBox(length * 0.42, 0.18, 0.58, materials.turtleShell);\n\t\tshell.position.set(0, 0.44, 0);\n\t\tconst head = makeBox(0.18, 0.12, 0.18, materials.turtle);\n\t\thead.position.set(length * 0.3, 0.33, 0);\n\t\tgroup.add(shell, head);\n\t} else {\n\t\tconst ringA = makeBox(0.08, 0.24, 0.82, materials.logRing);\n\t\tconst ringB = ringA.clone();\n\t\tringA.position.set(-length * 0.5 + 0.08, 0.24, 0);\n\t\tringB.position.set(length * 0.5 - 0.08, 0.24, 0);\n\t\tgroup.add(ringA, ringB);\n\t}\n\n\treturn group;\n}\n\nfunction laneTypeForIndex(index) {\n\tif (index <= 1) return 'grass';\n\tconst roll = Math.random();\n\tif (roll < 0.36) return 'road';\n\tif (roll < 0.58) return 'water';\n\treturn 'grass';\n}\n\nfunction populateLane(lane) {\n\tlane.userData.obstacles = [];\n\tlane.userData.speed = 0;\n\tlane.userData.direction = 1;\n\tlane.userData.safeSpots = [];\n\n\tconst {\n\t\ttype,\n\t\tindex\n\t} = lane.userData;\n\n\tif (type === 'grass') {\n\t\tlane.userData.safeSpots = [];\n\t\tif (index > 2 && Math.random() > 0.34) {\n\t\t\tconst blockedSpots = new Set();\n\t\t\tconst count = Math.floor(Math.random() * 3) + 1;\n\t\t\twhile (blockedSpots.size < count) {\n\t\t\t\tblockedSpots.add(Math.floor(Math.random() * (worldHalfWidth * 2 + 1)) - worldHalfWidth);\n\t\t\t}\n\t\t\tblockedSpots.forEach((spot) => {\n\t\t\t\tconst tree = new THREE.Group();\n\t\t\t\tconst trunk = makeBox(0.28, 0.82, 0.28, materials.log);\n\t\t\t\ttrunk.position.y = 0.41;\n\t\t\t\tconst leavesBase = makeBox(0.92, 0.52, 0.92, materials.grassEdge);\n\t\t\t\tleavesBase.position.y = 0.96;\n\t\t\t\tconst leavesTop = makeBox(0.66, 0.46, 0.66, materials.grassAlt);\n\t\t\t\tleavesTop.position.y = 1.38;\n\t\t\t\ttree.add(trunk, leavesBase, leavesTop);\n\t\t\t\ttree.position.set(xIndexToWorldX(spot), 0, 0);\n\t\t\t\tlane.add(tree);\n\t\t\t\tlane.userData.safeSpots.push(spot);\n\t\t\t});\n\t\t}\n\t\treturn;\n\t}\n\n\tlane.userData.safeSpots = [];\n\tlane.userData.direction = Math.random() > 0.5 ? 1 : -1;\n\tlane.userData.speed = type === 'road' ? 2.2 + Math.random() * 3 : 1.2 + Math.random() * 1.55;\n\tconst minGap = type === 'road' ? 1.8 : 2.4;\n\tconst baseSpacing = type === 'road' ? 6.4 + Math.random() * 1.6 : 6.8 + Math.random() * 1.8;\n\tconst count = 4;\n\n\tfor (let i = 0; i < count; i += 1) {\n\t\tconst isTruck = type === 'road' && Math.random() > 0.7;\n\t\tconst length = type === 'road' ? (isTruck ? 2.35 : 1.5 + Math.random() * 0.42) : 2.05 + Math.random() * 1.45;\n\t\tconst mesh = type === 'road' ?\n\t\t\tcreateVehicle(length, randomFrom([materials.carA, materials.carB, materials.carC, materials.truck])) :\n\t\t\tcreateLog(length, Math.random() > 0.74);\n\n\t\tconst previous = lane.userData.obstacles[i - 1];\n\t\tconst startOffset = i * baseSpacing + Math.random() * 0.8;\n\t\tconst x = previous ?\n\t\t\tprevious.x + lane.userData.direction * ((previous.length * 0.5) + (length * 0.5) + minGap) :\n\t\t\tstartOffset * lane.userData.direction;\n\n\t\tconst obstacle = {\n\t\t\tmesh,\n\t\t\tlength,\n\t\t\tx,\n\t\t\tspeed: lane.userData.speed * lane.userData.direction,\n\t\t\ttype,\n\t\t\tsinkOffset: Math.random() * Math.PI * 2\n\t\t};\n\n\t\tmesh.position.set(obstacle.x, 0, 0);\n\t\tlane.add(mesh);\n\t\tlane.userData.obstacles.push(obstacle);\n\t}\n}\n\nfunction buildWorld() {\n\tlanes.forEach((lane) => world.remove(lane));\n\tlanes = [];\n\tfor (let i = -visibleBack; i < laneCount; i += 1) {\n\t\tconst type = laneTypeForIndex(i);\n\t\tconst lane = createGroundLane(i, type);\n\t\tpopulateLane(lane);\n\t\tlanes.push(lane);\n\t\tworld.add(lane);\n\t}\n}\n\nfunction getLane(index) {\n\treturn lanes.find((lane) => lane.userData.index === index);\n}\n\nfunction canOccupy(laneIndex, xIndex) {\n\tif (xIndex < -worldHalfWidth || xIndex > worldHalfWidth) return false;\n\tconst lane = getLane(laneIndex);\n\tif (!lane) return false;\n\tif (lane.userData.type !== 'grass') return true;\n\treturn !lane.userData.safeSpots.includes(xIndex);\n}\n\nfunction resetPlayer() {\n\tplayer.lane = 0;\n\tplayer.xIndex = 0;\n\tplayer.worldX = 0;\n\tplayer.worldZ = 0;\n\tplayer.targetX = 0;\n\tplayer.targetZ = 0;\n\tplayer.group.position.set(0, 0, 0);\n\tplayer.group.rotation.set(0, 0, 0);\n\tplayer.moving = false;\n\tplayer.moveTime = 0;\n\tplayer.rideVelocity = 0;\n}\n\nfunction updateScore(next) {\n\tscore = next;\n\tscoreValue.textContent = String(score);\n\tif (score > best) {\n\t\tbest = score;\n\t\tbestValue.textContent = String(best);\n\t\tlocalStorage.setItem('crossy-voxel-best', String(best));\n\t}\n}\n\nfunction setState(next) {\n\tgameState = next;\n\tstateValue.textContent = next.charAt(0).toUpperCase() + next.slice(1);\n}\n\nfunction queueMove(direction) {\n\tif (gameState !== 'playing') return;\n\tif (moveQueue.length > 2) return;\n\tmoveQueue.push(direction);\n}\n\nfunction tryMove(direction) {\n\tif (player.moving) return false;\n\tlet lane = player.lane;\n\tlet xIndex = player.xIndex;\n\tif (direction === 'up') lane += 1;\n\tif (direction === 'down') lane -= 1;\n\tif (direction === 'left') xIndex += 1;\n\tif (direction === 'right') xIndex -= 1;\n\tif (lane < 0 || !canOccupy(lane, xIndex)) return false;\n\n\tplayer.lane = lane;\n\tplayer.xIndex = xIndex;\n\tplayer.targetX = xIndexToWorldX(xIndex);\n\tplayer.targetZ = laneToWorldZ(lane);\n\tplayer.moving = true;\n\tplayer.moveTime = 0;\n\tplayer.group.rotation.y = direction === 'left' ? Math.PI * 0.5 : direction === 'right' ? -Math.PI * 0.5 : direction === 'down' ? Math.PI : 0;\n\taudio.hop();\n\n\tif (lane > score) {\n\t\tupdateScore(lane);\n\t\taudio.score();\n\t}\n\treturn true;\n}\n\nfunction triggerGameOver(reason) {\n\tif (gameState !== 'playing') return;\n\tsetState('down');\n\tgameOverText.textContent = reason === 'water' ? `You drifted ${score} rows before the splash.` : `You hopped ${score} rows before becoming road paint.`;\n\tgameOverOverlay.classList.remove('overlay--hidden');\n\tgameOverOverlay.setAttribute('aria-hidden', 'false');\n\tif (reason === 'water') audio.splash();\n\telse audio.hit();\n}\n\nfunction startGame() {\n\taudio.resume();\n\tstartOverlay.classList.add('overlay--hidden');\n\tgameOverOverlay.classList.add('overlay--hidden');\n\tgameOverOverlay.setAttribute('aria-hidden', 'true');\n\tbuildWorld();\n\tresetPlayer();\n\tupdateScore(0);\n\tmoveQueue = [];\n\tsetState('playing');\n}\n\nfunction animatePlayer(delta) {\n\tconst moveDuration = 0.16;\n\tif (player.moving) {\n\t\tplayer.moveTime += delta;\n\t\tconst t = Math.min(player.moveTime / moveDuration, 1);\n\t\tconst eased = 1 - Math.pow(1 - t, 3);\n\t\tplayer.worldX += (player.targetX - player.worldX) * Math.min(1, delta * 18);\n\t\tplayer.worldZ += (player.targetZ - player.worldZ) * Math.min(1, delta * 18);\n\t\tplayer.group.position.x = player.worldX;\n\t\tplayer.group.position.z = player.worldZ;\n\t\tplayer.group.position.y = Math.sin(eased * Math.PI) * 0.55;\n\t\tif (t >= 1) {\n\t\t\tplayer.moving = false;\n\t\t\tplayer.worldX = player.targetX;\n\t\t\tplayer.worldZ = player.targetZ;\n\t\t\tplayer.group.position.set(player.worldX, 0, player.worldZ);\n\t\t}\n\t} else {\n\t\tplayer.group.position.y = Math.sin(performance.now() * 0.008) * 0.04;\n\t\tif (moveQueue.length) tryMove(moveQueue.shift());\n\t}\n}\n\nfunction recycleLanes() {\n\tconst minNeeded = player.lane - visibleBack;\n\tconst maxNeeded = player.lane + visibleAhead;\n\tlanes.forEach((lane) => {\n\t\tif (lane.userData.index < minNeeded) {\n\t\t\tlane.userData.index = maxNeeded + Math.floor(Math.random() * 4) + 1;\n\t\t\tlane.position.z = laneToWorldZ(lane.userData.index);\n\t\t\tlane.userData.type = laneTypeForIndex(lane.userData.index);\n\t\t\twhile (lane.children.length) lane.remove(lane.children[0]);\n\t\t\tlane.userData.obstacles = [];\n\t\t\tlane.userData.safeSpots = [];\n\t\t\tconst rebuilt = createGroundLane(lane.userData.index, lane.userData.type);\n\t\t\tworld.remove(lane);\n\t\t\tlanes = lanes.map((entry) => entry === lane ? rebuilt : entry);\n\t\t\tpopulateLane(rebuilt);\n\t\t\tworld.add(rebuilt);\n\t\t}\n\t});\n}\n\nfunction updateLanes(delta) {\n\tlanes.forEach((lane) => {\n\t\tconst {\n\t\t\ttype,\n\t\t\tobstacles\n\t\t} = lane.userData;\n\t\tif (!obstacles || !obstacles.length) return;\n\t\tobstacles.forEach((obstacle) => {\n\t\t\tobstacle.x += obstacle.speed * delta;\n\t\t\tconst wrap = 15;\n\t\t\tif (obstacle.speed > 0 && obstacle.x > wrap) obstacle.x = -wrap;\n\t\t\tif (obstacle.speed < 0 && obstacle.x < -wrap) obstacle.x = wrap;\n\t\t\tobstacle.mesh.position.x = obstacle.x;\n\t\t\tif (type === 'water') {\n\t\t\t\tobstacle.mesh.position.y = Math.sin(performance.now() * 0.002 + obstacle.sinkOffset) * 0.05;\n\t\t\t}\n\t\t});\n\t});\n}\n\nfunction updateCamera(delta) {\n\tconst targetZ = player.worldZ - 3.8;\n\tcamera.position.z += (targetZ - camera.position.z) * Math.min(1, delta * 3.5);\n\tcamera.position.x += ((player.worldX * 0.2) - camera.position.x) * Math.min(1, delta * 2.8);\n\tcamera.position.y += (12.8 - camera.position.y) * Math.min(1, delta * 2.6);\n\tcamera.lookAt(player.worldX * 0.08, 0.8, player.worldZ + 2.8);\n}\n\nfunction checkCollisions(delta) {\n\tif (gameState !== 'playing') return;\n\tconst lane = getLane(player.lane);\n\tif (!lane) return;\n\n\tplayer.rideVelocity = 0;\n\tif (lane.userData.type === 'road') {\n\t\tfor (const obstacle of lane.userData.obstacles) {\n\t\t\tif (Math.abs(obstacle.x - player.worldX) < obstacle.length * 0.5 + 0.35) {\n\t\t\t\ttriggerGameOver('road');\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (lane.userData.type === 'water') {\n\t\tlet onLog = null;\n\t\tfor (const obstacle of lane.userData.obstacles) {\n\t\t\tif (Math.abs(obstacle.x - player.worldX) < obstacle.length * 0.5 + 0.15) {\n\t\t\t\tonLog = obstacle;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (!onLog && !player.moving) {\n\t\t\ttriggerGameOver('water');\n\t\t\treturn;\n\t\t}\n\n\t\tif (onLog && !player.moving) {\n\t\t\tplayer.rideVelocity = onLog.speed;\n\t\t\tplayer.worldX += onLog.speed * delta;\n\t\t\tplayer.targetX = player.worldX;\n\t\t\tplayer.group.position.x = player.worldX;\n\t\t\tconst clampedIndex = Math.round(player.worldX / laneWidth);\n\t\t\tplayer.xIndex = clampedIndex;\n\t\t\tif (player.worldX < xIndexToWorldX(-worldHalfWidth) - 0.8 || player.worldX > xIndexToWorldX(worldHalfWidth) + 0.8) {\n\t\t\t\ttriggerGameOver('water');\n\t\t\t}\n\t\t}\n\t}\n}\n\nlet previousTime = performance.now();\n\nfunction loop(now) {\n\tconst delta = Math.min((now - previousTime) / 1000, 0.033);\n\tpreviousTime = now;\n\tif (gameState === 'playing') {\n\t\tupdateLanes(delta);\n\t\tanimatePlayer(delta);\n\t\tcheckCollisions(delta);\n\t\trecycleLanes();\n\t} else {\n\t\tplayer.group.position.y = Math.sin(now * 0.006) * 0.05;\n\t}\n\tupdateCamera(delta);\n\trenderer.render(scene, camera);\n\tanimationId = requestAnimationFrame(loop);\n}\n\nfunction resize() {\n\tconst width = gameRoot.clientWidth;\n\tconst height = gameRoot.clientHeight;\n\trenderer.setSize(width, height);\n\tcamera.aspect = width / height;\n\tcamera.updateProjectionMatrix();\n}\n\nwindow.addEventListener('resize', resize);\n\nwindow.addEventListener('keydown', (event) => {\n\tconst key = event.key.toLowerCase();\n\tconst map = {\n\t\tarrowup: 'up',\n\t\tw: 'up',\n\t\tarrowdown: 'down',\n\t\ts: 'down',\n\t\tarrowleft: 'left',\n\t\ta: 'left',\n\t\tarrowright: 'right',\n\t\td: 'right'\n\t};\n\tif (map[key]) {\n\t\tevent.preventDefault();\n\t\tqueueMove(map[key]);\n\t}\n\tif (key === ' ' && gameState !== 'playing') {\n\t\tevent.preventDefault();\n\t\tstartGame();\n\t}\n});\n\nlet touchStart = null;\ngameRoot.addEventListener('pointerdown', (event) => {\n\ttouchStart = {\n\t\tx: event.clientX,\n\t\ty: event.clientY\n\t};\n});\n\ngameRoot.addEventListener('pointerup', (event) => {\n\tif (!touchStart) return;\n\tconst dx = event.clientX - touchStart.x;\n\tconst dy = event.clientY - touchStart.y;\n\tconst absX = Math.abs(dx);\n\tconst absY = Math.abs(dy);\n\tif (Math.max(absX, absY) > 18) {\n\t\tqueueMove(absX > absY ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'up' : 'down'));\n\t}\n\ttouchStart = null;\n});\n\ntouchButtons.forEach((button) => {\n\tbutton.addEventListener('click', () => queueMove(button.dataset.move));\n});\n\nstartButton.addEventListener('click', startGame);\nrestartButton.addEventListener('click', startGame);\nhowButton.addEventListener('click', () => {\n\tconst tips = document.querySelector('.overlay__tips');\n\ttips.scrollIntoView({\n\t\tbehavior: 'smooth',\n\t\tblock: 'nearest'\n\t});\n});\n\nmuteButton.addEventListener('click', () => {\n\tmuted = !muted;\n\taudio.setMuted(muted);\n\tmuteButton.setAttribute('aria-pressed', String(muted));\n\tmuteButton.innerHTML = `<i data-lucide=\"${muted ? 'volume-x' : 'volume-2'}\"></i>`;\n\tlucide.createIcons();\n});\n\nbuildWorld();\nresetPlayer();\nresize();\nlucide.createIcons();\nloop(performance.now());"},{"name":"style.css","content":" :root {\n \tcolor-scheme: dark;\n \tfont-family: Inter, system-ui, sans-serif;\n \t--bg: #15120f;\n \t--bg-elevated: rgba(34, 28, 22, 0.88);\n \t--panel: rgba(58, 47, 38, 0.82);\n \t--panel-strong: rgba(75, 61, 50, 0.95);\n \t--line: rgba(255, 236, 204, 0.1);\n \t--text: #fff5e6;\n \t--muted: #d1bfa8;\n \t--accent: #f97316;\n \t--accent-2: #ffd166;\n \t--danger: #ff7b72;\n \t--shadow: 0 18px 40px rgba(0, 0, 0, 0.28);\n }\n\n * {\n \tbox-sizing: border-box;\n }\n\n html,\n body {\n \tmargin: 0;\n \tmin-height: 100%;\n \tbackground:\n \t\tradial-gradient(circle at top, rgba(255, 177, 104, 0.12), transparent 28%),\n \t\tlinear-gradient(180deg, #211b16 0%, #15120f 100%);\n \tcolor: var(--text);\n }\n\n body {\n \tmin-height: 100vh;\n }\n\n button {\n \tfont: inherit;\n }\n\n .game-shell {\n \tmin-height: 100vh;\n \tdisplay: grid;\n \tgrid-template-rows: auto 1fr;\n \tgap: 1rem;\n \tpadding: 1rem;\n }\n\n .hud {\n \tdisplay: grid;\n \tgap: 0.9rem;\n \talign-items: start;\n }\n\n .hud__brand,\n .hud__stats {\n \tdisplay: flex;\n \tjustify-content: space-between;\n \talign-items: center;\n \tgap: 0.8rem;\n \tflex-wrap: wrap;\n }\n\n .hud__title-wrap {\n \tdisplay: grid;\n \tgap: 0.15rem;\n }\n\n .hud__eyebrow {\n \tmargin: 0;\n \ttext-transform: uppercase;\n \tletter-spacing: 0.18em;\n \tfont-size: 0.72rem;\n \tcolor: var(--accent-2);\n }\n\n .hud__title {\n \tmargin: 0;\n \tfont-size: clamp(1.7rem, 3vw, 2.8rem);\n \tline-height: 0.95;\n }\n\n .hud__button,\n .overlay__button,\n .touch-controls__button {\n \tborder: 1px solid var(--line);\n \tbackground: var(--panel);\n \tcolor: var(--text);\n \tborder-radius: 14px;\n \tcursor: pointer;\n \ttransition: transform 160ms ease, background 160ms ease, border-color 160ms ease;\n }\n\n .hud__button:hover,\n .overlay__button:hover,\n .touch-controls__button:hover,\n .hud__button:focus-visible,\n .overlay__button:focus-visible,\n .touch-controls__button:focus-visible {\n \ttransform: translateY(-1px);\n \tbackground: var(--panel-strong);\n \tborder-color: rgba(255, 236, 204, 0.25);\n }\n\n .hud__button {\n \twidth: 3rem;\n \theight: 3rem;\n \tdisplay: grid;\n \tplace-items: center;\n }\n\n .hud__button i,\n .touch-controls__button i,\n .overlay__tips i {\n \twidth: 1.1rem;\n \theight: 1.1rem;\n }\n\n .hud__stats {\n \tjustify-content: flex-start;\n }\n\n .stat-card {\n \tmin-width: 6.8rem;\n \tpadding: 0.8rem 1rem;\n \tdisplay: grid;\n \tgap: 0.3rem;\n \tbackground: var(--bg-elevated);\n \tborder: 1px solid var(--line);\n \tborder-radius: 16px;\n \tbox-shadow: var(--shadow);\n \tbackdrop-filter: blur(10px);\n }\n\n .stat-card__label {\n \tfont-size: 0.72rem;\n \ttext-transform: uppercase;\n \tletter-spacing: 0.14em;\n \tcolor: var(--muted);\n }\n\n .stat-card__value {\n \tfont-size: 1.5rem;\n \tline-height: 1;\n }\n\n .stat-card__value--small {\n \tfont-size: 1rem;\n }\n\n .game-stage {\n \tposition: relative;\n \tmin-height: 66vh;\n \tborder-radius: 28px;\n \toverflow: hidden;\n \tborder: 1px solid var(--line);\n \tbackground:\n \t\tlinear-gradient(180deg, rgba(255, 211, 145, 0.08), transparent 22%),\n \t\t#120f0d;\n \tbox-shadow: var(--shadow);\n }\n\n .game-stage__canvas {\n \tposition: absolute;\n \tinset: 0;\n }\n\n .overlay {\n \tposition: absolute;\n \tinset: 0;\n \tdisplay: grid;\n \tplace-items: center;\n \tpadding: 1rem;\n \tbackground: rgba(12, 10, 8, 0.45);\n \tbackdrop-filter: blur(8px);\n \tz-index: 3;\n }\n\n .overlay--hidden {\n \topacity: 0;\n \tpointer-events: none;\n }\n\n .overlay__panel {\n \twidth: min(100%, 34rem);\n \tpadding: 1.4rem;\n \tborder-radius: 24px;\n \tbackground: rgba(34, 28, 22, 0.9);\n \tborder: 1px solid rgba(255, 236, 204, 0.12);\n \tbox-shadow: var(--shadow);\n }\n\n .overlay__panel--compact {\n \twidth: min(100%, 24rem);\n \ttext-align: center;\n }\n\n .overlay__kicker {\n \tmargin: 0 0 0.45rem;\n \ttext-transform: uppercase;\n \tletter-spacing: 0.16em;\n \tcolor: var(--accent-2);\n \tfont-size: 0.74rem;\n }\n\n .overlay__title {\n \tmargin: 0;\n \tfont-size: clamp(2rem, 5vw, 3.4rem);\n \tline-height: 0.95;\n }\n\n .overlay__text {\n \tmargin: 0.9rem 0 0;\n \tcolor: var(--muted);\n \tmax-width: 32ch;\n }\n\n .overlay__actions {\n \tmargin-top: 1.15rem;\n \tdisplay: flex;\n \tgap: 0.75rem;\n \tflex-wrap: wrap;\n }\n\n .overlay__button {\n \tmin-height: 3rem;\n \tpadding: 0.85rem 1.1rem;\n }\n\n .overlay__button--primary {\n \tbackground: var(--accent);\n \tcolor: #1b1209;\n \tborder-color: transparent;\n \tfont-weight: 700;\n }\n\n .overlay__tips {\n \tlist-style: none;\n \tpadding: 0;\n \tmargin: 1rem 0 0;\n \tdisplay: grid;\n \tgap: 0.7rem;\n }\n\n .overlay__tips li {\n \tdisplay: flex;\n \talign-items: center;\n \tgap: 0.7rem;\n \tcolor: var(--muted);\n }\n\n .touch-controls {\n \tposition: absolute;\n \tright: 1rem;\n \tbottom: 1rem;\n \tdisplay: grid;\n \tgrid-template-columns: repeat(3, 3.4rem);\n \tgrid-template-areas:\n \t\t'. up .'\n \t\t'left down right';\n \tgap: 0.45rem;\n \tz-index: 2;\n }\n\n .touch-controls__button {\n \twidth: 3.4rem;\n \theight: 3.4rem;\n \tdisplay: grid;\n \tplace-items: center;\n \tbackground: rgba(34, 28, 22, 0.72);\n \tbackdrop-filter: blur(6px);\n }\n\n .touch-controls__button--up {\n \tgrid-area: up;\n }\n\n .touch-controls__button--left {\n \tgrid-area: left;\n }\n\n .touch-controls__button--down {\n \tgrid-area: down;\n }\n\n .touch-controls__button--right {\n \tgrid-area: right;\n }\n\n @media (min-width: 860px) {\n \t.game-shell {\n \t\tpadding: 1.2rem;\n \t}\n\n \t.hud {\n \t\tgrid-template-columns: 1fr auto;\n \t\talign-items: end;\n \t}\n }\n\n @media (max-width: 640px) {\n \t.game-shell {\n \t\tpadding: 0.75rem;\n \t}\n\n \t.game-stage {\n \t\tmin-height: 72vh;\n \t\tborder-radius: 22px;\n \t}\n\n \t.touch-controls {\n \t\tleft: 50%;\n \t\tright: auto;\n \t\ttransform: translateX(-50%);\n \t\tbottom: 0.85rem;\n \t}\n\n \t.overlay__panel {\n \t\tpadding: 1.15rem;\n \t}\n }"}],"folders":[]},"variants":null,"createdAt":"2026-03-08T18:29:22.355Z","updatedAt":"2026-03-11T17:20:56.722Z","hasThumbnail":true}}