[object Object]

← back to Games Agentabrams

Add 8 new arcade games: Asteroids, Minesweeper, Simon, Connect 4, Stack, Maze, Lights Out, Whack (16→24)

94a2079ea0f0caf510acf979329a8b82729c6386 · 2026-07-24 19:23:47 -0700 · Steve Abrams

Files touched

Diff

commit 94a2079ea0f0caf510acf979329a8b82729c6386
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 24 19:23:47 2026 -0700

    Add 8 new arcade games: Asteroids, Minesweeper, Simon, Connect 4, Stack, Maze, Lights Out, Whack (16→24)
---
 games.json                    |  64 ++++
 games/asteroids/index.html    | 828 ++++++++++++++++++++++++++++++++++++++++++
 games/connect-four/index.html | 720 ++++++++++++++++++++++++++++++++++++
 games/lights-out/index.html   | 471 ++++++++++++++++++++++++
 games/maze/index.html         | 692 +++++++++++++++++++++++++++++++++++
 games/minesweeper/index.html  | 820 +++++++++++++++++++++++++++++++++++++++++
 games/simon/index.html        | 632 ++++++++++++++++++++++++++++++++
 games/tower-stack/index.html  | 513 ++++++++++++++++++++++++++
 games/whack-a-mole/index.html | 728 +++++++++++++++++++++++++++++++++++++
 9 files changed, 5468 insertions(+)

diff --git a/games.json b/games.json
index ed96445..6ac0e7b 100644
--- a/games.json
+++ b/games.json
@@ -1,5 +1,69 @@
 {
   "games": [
+    {
+      "id": "asteroids",
+      "file": "AbramsAsteroids.aa",
+      "title": "Abrams Asteroids",
+      "desc": "A neon vector shooter: thrust, spin, and blast splitting asteroids across wrapping space through escalating waves, with hyperspace and sound. Pure Canvas 2D, zero dependencies.",
+      "path": "games/asteroids/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "minesweeper",
+      "file": "AbramsMines.aa",
+      "title": "Abrams Minesweeper",
+      "desc": "Classic Minesweeper reborn in neon-on-dark — flood-fill sweeps, tap-to-flag, first-click-safe, best-time chasing across three difficulties. Pure vanilla JS, zero dependencies.",
+      "path": "games/minesweeper/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "simon",
+      "file": "AbramsSimon.aa",
+      "title": "Abrams Simon",
+      "desc": "Neon four-pad Simon memory game with Web Audio tones, speeding rounds, strict mode, and a saved best score. Pure vanilla JS, zero dependencies.",
+      "path": "games/simon/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "connect-four",
+      "file": "AbramsConnect4.aa",
+      "title": "Abrams Connect 4",
+      "desc": "Drop glowing neon discs to line up four in a row and outsmart an alpha-beta minimax AI, with silky physics drops and Web Audio. Pure vanilla JS, zero dependencies.",
+      "path": "games/connect-four/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "tower-stack",
+      "file": "AbramsStack.aa",
+      "title": "Abrams Stack",
+      "desc": "Time your taps to stack neon blocks into a sky-high tower — nail a perfect drop for combo bonuses before the overhang slices you down. Pure Canvas 2D, zero dependencies.",
+      "path": "games/tower-stack/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "maze",
+      "file": "AbramsMaze.aa",
+      "title": "Abrams Maze",
+      "desc": "A neon procedurally-generated maze runner: race the timer through ever-larger perfect mazes, drop breadcrumbs, and flash the solution path. Pure Canvas 2D, zero dependencies.",
+      "path": "games/maze/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "lights-out",
+      "file": "AbramsLightsOut.aa",
+      "title": "Abrams Lights Out",
+      "desc": "Toggle glowing neon cells and their neighbors to switch every light off in this slick, solvable-only Lights Out puzzle. Pure vanilla JS, zero dependencies.",
+      "path": "games/lights-out/",
+      "added": "2026-07-24"
+    },
+    {
+      "id": "whack-a-mole",
+      "file": "AbramsWhack.aa",
+      "title": "Abrams Whack",
+      "desc": "A neon whack-a-mole arcade sprint — smash glowing moles for combo multipliers, snag gold jackpots, dodge bombs that cost lives. Pure vanilla JS, zero dependencies.",
+      "path": "games/whack-a-mole/",
+      "added": "2026-07-24"
+    },
     {
       "id": "dvd-bounce",
       "file": "DVDBounce.aa",
diff --git a/games/asteroids/index.html b/games/asteroids/index.html
new file mode 100644
index 0000000..b0f04be
--- /dev/null
+++ b/games/asteroids/index.html
@@ -0,0 +1,828 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<title>Abrams Asteroids</title>
+<style>
+  :root {
+    --neon: #00f0ff;
+    --neon2: #ff2bd6;
+    --accent: #b6ff3a;
+    --bg0: #04060f;
+    --bg1: #0a0f24;
+  }
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  html, body {
+    width: 100%; height: 100%;
+    overflow: hidden;
+    background: var(--bg0);
+    font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+    -webkit-user-select: none; user-select: none;
+    -webkit-tap-highlight-color: transparent;
+    touch-action: none;
+  }
+  #wrap {
+    position: fixed; inset: 0;
+    background:
+      radial-gradient(ellipse at 50% 30%, #0d1636 0%, var(--bg0) 70%),
+      var(--bg0);
+  }
+  canvas { position: absolute; inset: 0; display: block; }
+
+  /* HUD */
+  #hud {
+    position: absolute; top: 0; left: 0; right: 0;
+    display: flex; justify-content: space-between; align-items: flex-start;
+    padding: 14px 18px;
+    pointer-events: none;
+    font-variant-numeric: tabular-nums;
+    text-shadow: 0 0 8px currentColor;
+    z-index: 5;
+  }
+  #hud .col { display: flex; flex-direction: column; gap: 4px; }
+  #hud .col.right { align-items: flex-end; text-align: right; }
+  .label { font-size: 11px; letter-spacing: 3px; color: #6f7bb5; text-shadow: none; }
+  .val { font-size: 22px; font-weight: 700; letter-spacing: 1px; color: var(--neon); }
+  .val.best { color: var(--accent); font-size: 16px; }
+  #lives { display: flex; gap: 6px; margin-top: 2px; }
+  #lives svg { filter: drop-shadow(0 0 4px var(--neon)); }
+  #wave { color: var(--neon2); font-size: 16px; }
+
+  /* Overlay */
+  #overlay {
+    position: absolute; inset: 0;
+    display: flex; flex-direction: column;
+    align-items: center; justify-content: center;
+    text-align: center;
+    background: radial-gradient(ellipse at 50% 45%, rgba(6,10,28,.72) 0%, rgba(2,4,12,.94) 75%);
+    z-index: 10;
+    padding: 24px;
+    backdrop-filter: blur(2px);
+  }
+  #overlay.hidden { display: none; }
+  #title {
+    font-size: clamp(34px, 9vw, 84px);
+    font-weight: 800;
+    letter-spacing: clamp(2px, 1.5vw, 12px);
+    color: #eafcff;
+    text-shadow: 0 0 18px var(--neon), 0 0 40px var(--neon), 0 0 4px #fff;
+    line-height: 1;
+  }
+  #title .sub { display:block; font-size: clamp(13px,3.5vw,22px); letter-spacing: clamp(3px,2vw,16px); color: var(--neon2); text-shadow: 0 0 12px var(--neon2); margin-top: 10px; }
+  #ovMsg { margin-top: 18px; font-size: clamp(14px,4vw,20px); color: #b9c6ff; min-height: 1.2em; text-shadow: 0 0 8px rgba(120,150,255,.4);}
+  #ovScore { margin-top: 6px; font-size: clamp(13px,3.6vw,18px); color: var(--accent); }
+  #playBtn {
+    margin-top: 26px;
+    pointer-events: auto; cursor: pointer;
+    background: linear-gradient(180deg, rgba(0,240,255,.16), rgba(0,240,255,.04));
+    border: 2px solid var(--neon);
+    color: #eafcff;
+    font-size: clamp(16px,4.5vw,22px); font-weight: 700; letter-spacing: 4px;
+    padding: 14px 40px; border-radius: 40px;
+    text-shadow: 0 0 10px var(--neon);
+    box-shadow: 0 0 22px rgba(0,240,255,.35), inset 0 0 18px rgba(0,240,255,.18);
+    transition: transform .08s ease, box-shadow .2s ease;
+  }
+  #playBtn:hover { box-shadow: 0 0 34px rgba(0,240,255,.6), inset 0 0 24px rgba(0,240,255,.28); }
+  #playBtn:active { transform: scale(.96); }
+  #hint {
+    margin-top: 26px; font-size: clamp(11px,3vw,14px); color: #6f7bb5;
+    letter-spacing: 1px; line-height: 1.9; max-width: 560px;
+  }
+  #hint b { color: var(--neon); font-weight: 700; }
+
+  /* Toast / status pills */
+  #statusPills {
+    position: absolute; top: 56px; left: 50%; transform: translateX(-50%);
+    display: flex; gap: 10px; z-index: 6; pointer-events: none;
+  }
+  .pill {
+    font-size: 11px; letter-spacing: 2px; padding: 4px 10px; border-radius: 20px;
+    border: 1px solid rgba(120,150,255,.4); color: #9fb0f0; background: rgba(10,16,40,.6);
+    opacity: 0; transition: opacity .3s;
+  }
+  .pill.on { opacity: 1; }
+  .pill.paused { color: var(--accent); border-color: var(--accent); }
+  .pill.muted { color: var(--neon2); border-color: var(--neon2); }
+
+  #centerToast {
+    position: absolute; top: 42%; left: 50%; transform: translate(-50%,-50%);
+    font-size: clamp(20px,7vw,48px); font-weight: 800; letter-spacing: 6px;
+    color: #eafcff; text-shadow: 0 0 20px var(--neon2), 0 0 40px var(--neon2);
+    z-index: 7; pointer-events: none; opacity: 0; transition: opacity .4s;
+  }
+  #centerToast.on { opacity: 1; }
+
+  /* Touch controls */
+  #touch { position: absolute; inset: 0; z-index: 8; display: none; pointer-events: none; }
+  #touch.show { display: block; }
+  .tbtn {
+    position: absolute; pointer-events: auto;
+    display: flex; align-items: center; justify-content: center;
+    border-radius: 50%;
+    border: 2px solid rgba(0,240,255,.55);
+    background: rgba(8,14,36,.42);
+    color: var(--neon); font-weight: 700; font-size: 13px; letter-spacing: 1px;
+    box-shadow: 0 0 14px rgba(0,240,255,.22), inset 0 0 12px rgba(0,240,255,.12);
+    backdrop-filter: blur(2px);
+    -webkit-user-select: none; user-select: none;
+    transition: background .08s, transform .08s;
+  }
+  .tbtn:active, .tbtn.active { background: rgba(0,240,255,.28); transform: scale(.94); }
+  .tbtn svg { width: 44%; height: 44%; fill: var(--neon); filter: drop-shadow(0 0 4px var(--neon)); }
+  #tLeft  { left: 20px;  bottom: 96px; width: 74px; height: 74px; }
+  #tRight { left: 110px; bottom: 30px; width: 74px; height: 74px; }
+  #tThrust{ right: 110px; bottom: 96px; width: 82px; height: 82px; }
+  #tFire  { right: 20px;  bottom: 30px; width: 82px; height: 82px; border-color: var(--neon2); color: var(--neon2); box-shadow: 0 0 14px rgba(255,43,214,.3), inset 0 0 12px rgba(255,43,214,.14); }
+  #tFire svg { fill: var(--neon2); filter: drop-shadow(0 0 4px var(--neon2)); }
+  #tHyper { right: 24px; top: 84px; width: 56px; height: 56px; font-size: 11px; border-color: var(--accent); color: var(--accent); }
+</style>
+</head>
+<body>
+<div id="wrap">
+  <canvas id="game"></canvas>
+
+  <div id="hud">
+    <div class="col">
+      <span class="label">SCORE</span>
+      <span class="val" id="score">0</span>
+      <span class="val best" id="best">BEST 0</span>
+    </div>
+    <div class="col right">
+      <span class="label">WAVE</span>
+      <span class="val" id="wave">1</span>
+      <span class="label" style="margin-top:6px;">SHIPS</span>
+      <div id="lives"></div>
+    </div>
+  </div>
+
+  <div id="statusPills">
+    <span class="pill paused" id="pillPaused">PAUSED</span>
+    <span class="pill muted" id="pillMuted">MUTED</span>
+  </div>
+
+  <div id="centerToast"></div>
+
+  <!-- Touch controls -->
+  <div id="touch">
+    <div class="tbtn" id="tLeft"><svg viewBox="0 0 100 100"><path d="M65 15 L30 50 L65 85 Z"/></svg></div>
+    <div class="tbtn" id="tRight"><svg viewBox="0 0 100 100"><path d="M35 15 L70 50 L35 85 Z"/></svg></div>
+    <div class="tbtn" id="tThrust"><svg viewBox="0 0 100 100"><path d="M50 12 L80 78 L50 62 L20 78 Z"/></svg></div>
+    <div class="tbtn" id="tFire"><svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="26"/></svg></div>
+    <div class="tbtn" id="tHyper">HYP</div>
+  </div>
+
+  <div id="overlay">
+    <div id="title">ABRAMS<span class="sub">ASTEROIDS</span></div>
+    <div id="ovMsg">Clear the field. Survive the swarm.</div>
+    <div id="ovScore"></div>
+    <button id="playBtn">LAUNCH</button>
+    <div id="hint">
+      <b>&#9650; / W</b> Thrust &nbsp; <b>&#9664; &#9654; / A D</b> Rotate &nbsp; <b>SPACE</b> Fire<br>
+      <b>SHIFT</b> Hyperspace &nbsp; <b>P</b> Pause &nbsp; <b>M</b> Mute &nbsp; <b>R</b> Restart<br>
+      <span style="opacity:.75">Touch devices: use the on-screen pads.</span>
+    </div>
+  </div>
+</div>
+
+<script>
+(function () {
+  "use strict";
+
+  // ---------- Setup ----------
+  var canvas = document.getElementById("game");
+  var ctx = canvas.getContext("2d");
+  var wrap = document.getElementById("wrap");
+
+  var W = 0, H = 0, DPR = 1;
+
+  function resize() {
+    DPR = Math.min(window.devicePixelRatio || 1, 2);
+    var r = wrap.getBoundingClientRect();
+    W = Math.max(320, r.width);
+    H = Math.max(240, r.height);
+    canvas.width = Math.floor(W * DPR);
+    canvas.height = Math.floor(H * DPR);
+    canvas.style.width = W + "px";
+    canvas.style.height = H + "px";
+    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
+  }
+  window.addEventListener("resize", resize);
+  resize();
+
+  // ---------- Audio (Web Audio, procedural) ----------
+  var Audio = (function () {
+    var actx = null, master = null;
+    var muted = false;
+    function ensure() {
+      if (actx) return;
+      try {
+        var AC = window.AudioContext || window.webkitAudioContext;
+        actx = new AC();
+        master = actx.createGain();
+        master.gain.value = 0.35;
+        master.connect(actx.destination);
+      } catch (e) { actx = null; }
+    }
+    function resume() { if (actx && actx.state === "suspended") actx.resume(); }
+    function tone(freq, dur, type, vol, slideTo) {
+      if (muted || !actx) return;
+      var t = actx.currentTime;
+      var o = actx.createOscillator();
+      var g = actx.createGain();
+      o.type = type || "square";
+      o.frequency.setValueAtTime(freq, t);
+      if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t + dur);
+      g.gain.setValueAtTime(0.0001, t);
+      g.gain.exponentialRampToValueAtTime(vol || 0.3, t + 0.008);
+      g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
+      o.connect(g); g.connect(master);
+      o.start(t); o.stop(t + dur + 0.02);
+    }
+    function noise(dur, vol, freq) {
+      if (muted || !actx) return;
+      var t = actx.currentTime;
+      var n = Math.floor(actx.sampleRate * dur);
+      var buf = actx.createBuffer(1, n, actx.sampleRate);
+      var d = buf.getChannelData(0);
+      for (var i = 0; i < n; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / n);
+      var src = actx.createBufferSource(); src.buffer = buf;
+      var flt = actx.createBiquadFilter();
+      flt.type = "lowpass"; flt.frequency.value = freq || 1200;
+      var g = actx.createGain();
+      g.gain.setValueAtTime(vol || 0.4, t);
+      g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
+      src.connect(flt); flt.connect(g); g.connect(master);
+      src.start(t);
+    }
+    return {
+      init: function () { ensure(); resume(); },
+      resume: resume,
+      isMuted: function () { return muted; },
+      toggleMute: function () { muted = !muted; return muted; },
+      shoot: function () { tone(880, 0.09, "square", 0.14, 300); },
+      thrust: function () { noise(0.05, 0.06, 500); },
+      bangBig: function () { noise(0.32, 0.5, 700); tone(70, 0.3, "sawtooth", 0.2, 40); },
+      bangMed: function () { noise(0.22, 0.4, 1000); tone(120, 0.2, "sawtooth", 0.15, 60); },
+      bangSmall: function () { noise(0.14, 0.3, 1600); tone(200, 0.12, "square", 0.12, 90); },
+      hyper: function () { tone(200, 0.35, "sine", 0.25, 1400); },
+      death: function () { noise(0.6, 0.55, 500); tone(160, 0.6, "sawtooth", 0.3, 30); },
+      wave: function () { tone(523, 0.12, "triangle", 0.2); setTimeout(function(){tone(784,0.16,"triangle",0.2);}, 120); },
+      extraLife: function () { tone(660, 0.1, "triangle", 0.2); setTimeout(function(){tone(990,0.14,"triangle",0.22);}, 100); }
+    };
+  })();
+
+  // ---------- Input ----------
+  var keys = {};
+  var touch = { left: false, right: false, thrust: false, fire: false };
+
+  function down(code) {
+    if (code === "KeyM") { toggleMute(); return; }
+    if (code === "KeyP") { togglePause(); return; }
+    if (code === "KeyR") { if (state === "play" || state === "over") restart(); return; }
+    keys[code] = true;
+  }
+  function up(code) { keys[code] = false; }
+
+  window.addEventListener("keydown", function (e) {
+    var c = e.code;
+    if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Space"].indexOf(c) >= 0) e.preventDefault();
+    if (state !== "play") {
+      if (c === "Space" || c === "Enter") {
+        if (state === "start" || state === "over") { startGame(); return; }
+      }
+    }
+    Audio.init();
+    down(c);
+  });
+  window.addEventListener("keyup", function (e) { up(e.code); });
+
+  // Touch buttons
+  function bindTouch(id, prop) {
+    var el = document.getElementById(id);
+    function on(e) { e.preventDefault(); Audio.init(); touch[prop] = true; el.classList.add("active"); }
+    function off(e) { e.preventDefault(); touch[prop] = false; el.classList.remove("active"); }
+    el.addEventListener("touchstart", on, { passive: false });
+    el.addEventListener("touchend", off, { passive: false });
+    el.addEventListener("touchcancel", off, { passive: false });
+    el.addEventListener("mousedown", on);
+    el.addEventListener("mouseup", off);
+    el.addEventListener("mouseleave", off);
+  }
+  bindTouch("tLeft", "left");
+  bindTouch("tRight", "right");
+  bindTouch("tThrust", "thrust");
+  bindTouch("tFire", "fire");
+  (function () {
+    var el = document.getElementById("tHyper");
+    function on(e) { e.preventDefault(); Audio.init(); doHyperspace(); el.classList.add("active"); }
+    function off(e) { e.preventDefault(); el.classList.remove("active"); }
+    el.addEventListener("touchstart", on, { passive: false });
+    el.addEventListener("touchend", off, { passive: false });
+    el.addEventListener("mousedown", on);
+    el.addEventListener("mouseup", off);
+  })();
+
+  var isTouch = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
+  if (isTouch) document.getElementById("touch").classList.add("show");
+
+  // ---------- Game constants ----------
+  var SHIP_R = 14;
+  var TURN = 4.2;           // rad/sec
+  var THRUST = 340;         // px/sec^2
+  var FRICTION = 0.72;      // per sec (velocity retained ~)
+  var MAX_SPEED = 460;
+  var BULLET_SPEED = 560;
+  var BULLET_LIFE = 0.85;
+  var FIRE_COOLDOWN = 0.16;
+  var INVULN_TIME = 2.4;
+  var START_LIVES = 3;
+  var EXTRA_LIFE_EVERY = 10000;
+
+  var AST_SIZES = { big: 46, med: 26, small: 15 };
+  var AST_SCORE = { big: 20, med: 50, small: 100 };
+  var AST_SPEED = { big: 42, med: 66, small: 92 };
+
+  // ---------- State ----------
+  var state = "start"; // start | play | paused | over
+  var ship, bullets, asteroids, particles;
+  var score, best, lives, wave, fireTimer, hyperCooldown, nextExtra;
+  var thrustSoundTimer = 0;
+
+  best = parseInt(localStorage.getItem("abrams-asteroids-best") || "0", 10) || 0;
+
+  function rand(a, b) { return a + Math.random() * (b - a); }
+  function wrapPos(o) {
+    if (o.x < 0) o.x += W; else if (o.x >= W) o.x -= W;
+    if (o.y < 0) o.y += H; else if (o.y >= H) o.y -= H;
+  }
+
+  function makeShip() {
+    return { x: W / 2, y: H / 2, a: -Math.PI / 2, vx: 0, vy: 0, r: SHIP_R, invuln: INVULN_TIME, thrusting: false, alive: true };
+  }
+
+  function makeAsteroid(x, y, size) {
+    var speed = AST_SPEED[size] * rand(0.7, 1.35);
+    var ang = rand(0, Math.PI * 2);
+    var r = AST_SIZES[size];
+    var verts = [], n = Math.floor(rand(9, 14));
+    for (var i = 0; i < n; i++) {
+      verts.push(rand(0.72, 1.15));
+    }
+    return {
+      x: x, y: y, size: size, r: r,
+      vx: Math.cos(ang) * speed, vy: Math.sin(ang) * speed,
+      rot: rand(-1, 1), spin: rand(-0.9, 0.9),
+      verts: verts, hue: rand(180, 300)
+    };
+  }
+
+  function spawnWave(n) {
+    asteroids = [];
+    for (var i = 0; i < n; i++) {
+      // spawn away from ship center
+      var x, y, tries = 0;
+      do {
+        x = rand(0, W); y = rand(0, H);
+        tries++;
+      } while (ship && Math.hypot(x - ship.x, y - ship.y) < 180 && tries < 30);
+      asteroids.push(makeAsteroid(x, y, "big"));
+    }
+  }
+
+  function resetRound() {
+    ship = makeShip();
+    bullets = [];
+    particles = [];
+    fireTimer = 0;
+    hyperCooldown = 0;
+  }
+
+  function startGame() {
+    Audio.init();
+    score = 0; lives = START_LIVES; wave = 1; nextExtra = EXTRA_LIFE_EVERY;
+    resetRound();
+    spawnWave(4);
+    state = "play";
+    hideOverlay();
+    Audio.wave();
+    lastT = performance.now();
+    showToast("WAVE 1", 900);
+  }
+
+  function restart() {
+    startGame();
+  }
+
+  function nextWave() {
+    wave++;
+    spawnWave(Math.min(4 + wave, 12));
+    ship.invuln = Math.max(ship.invuln, 1.2);
+    Audio.wave();
+    showToast("WAVE " + wave, 900);
+  }
+
+  function gameOver() {
+    state = "over";
+    if (score > best) {
+      best = score;
+      localStorage.setItem("abrams-asteroids-best", String(best));
+    }
+    updateHud();
+    showOverlay(
+      "GAME OVER",
+      "The field claimed your fleet.",
+      "SCORE " + score + "   •   BEST " + best,
+      "PLAY AGAIN"
+    );
+  }
+
+  // ---------- Actions ----------
+  function fire() {
+    if (fireTimer > 0 || !ship || !ship.alive) return;
+    if (bullets.length > 8) return;
+    var nx = Math.cos(ship.a), ny = Math.sin(ship.a);
+    bullets.push({
+      x: ship.x + nx * ship.r, y: ship.y + ny * ship.r,
+      vx: ship.vx + nx * BULLET_SPEED, vy: ship.vy + ny * BULLET_SPEED,
+      life: BULLET_LIFE
+    });
+    fireTimer = FIRE_COOLDOWN;
+    Audio.shoot();
+  }
+
+  function doHyperspace() {
+    if (state !== "play" || !ship || !ship.alive || hyperCooldown > 0) return;
+    hyperCooldown = 1.2;
+    // small risk: 12% chance of misjump explosion
+    ship.x = rand(40, W - 40);
+    ship.y = rand(40, H - 40);
+    ship.vx = 0; ship.vy = 0;
+    ship.invuln = Math.max(ship.invuln, 0.8);
+    Audio.hyper();
+    for (var i = 0; i < 14; i++) spawnParticle(ship.x, ship.y, 200, "#00f0ff");
+    if (Math.random() < 0.12) {
+      killShip();
+    }
+  }
+
+  function killShip() {
+    if (!ship.alive) return;
+    ship.alive = false;
+    Audio.death();
+    for (var i = 0; i < 26; i++) spawnParticle(ship.x, ship.y, 240, "#ff2bd6");
+    lives--;
+    updateHud();
+    if (lives <= 0) {
+      setTimeout(gameOver, 900);
+    } else {
+      setTimeout(function () {
+        if (state !== "play") return;
+        ship = makeShip();
+      }, 900);
+    }
+  }
+
+  function splitAsteroid(a, idx) {
+    // score
+    addScore(AST_SCORE[a.size]);
+    // particles
+    var col = "hsl(" + a.hue + ",90%,65%)";
+    var pc = a.size === "big" ? 16 : a.size === "med" ? 11 : 7;
+    for (var i = 0; i < pc; i++) spawnParticle(a.x, a.y, a.size === "big" ? 220 : 160, col);
+    if (a.size === "big") Audio.bangBig();
+    else if (a.size === "med") Audio.bangMed();
+    else Audio.bangSmall();
+
+    asteroids.splice(idx, 1);
+    if (a.size === "big") {
+      asteroids.push(makeAsteroid(a.x, a.y, "med"));
+      asteroids.push(makeAsteroid(a.x, a.y, "med"));
+    } else if (a.size === "med") {
+      asteroids.push(makeAsteroid(a.x, a.y, "small"));
+      asteroids.push(makeAsteroid(a.x, a.y, "small"));
+    }
+  }
+
+  function addScore(n) {
+    score += n;
+    if (score >= nextExtra) {
+      lives++;
+      nextExtra += EXTRA_LIFE_EVERY;
+      Audio.extraLife();
+      showToast("EXTRA SHIP", 800);
+    }
+    updateHud();
+  }
+
+  function spawnParticle(x, y, spd, color) {
+    var a = rand(0, Math.PI * 2);
+    var s = rand(spd * 0.25, spd);
+    particles.push({
+      x: x, y: y, vx: Math.cos(a) * s, vy: Math.sin(a) * s,
+      life: rand(0.4, 0.9), max: 0.9, color: color
+    });
+  }
+
+  // ---------- Update ----------
+  function update(dt) {
+    if (state !== "play") return;
+
+    // ship controls
+    if (ship && ship.alive) {
+      var left = keys["ArrowLeft"] || keys["KeyA"] || touch.left;
+      var right = keys["ArrowRight"] || keys["KeyD"] || touch.right;
+      var thr = keys["ArrowUp"] || keys["KeyW"] || touch.thrust;
+      var shift = keys["ShiftLeft"] || keys["ShiftRight"];
+
+      if (left) ship.a -= TURN * dt;
+      if (right) ship.a += TURN * dt;
+
+      ship.thrusting = !!thr;
+      if (thr) {
+        ship.vx += Math.cos(ship.a) * THRUST * dt;
+        ship.vy += Math.sin(ship.a) * THRUST * dt;
+        thrustSoundTimer -= dt;
+        if (thrustSoundTimer <= 0) { Audio.thrust(); thrustSoundTimer = 0.06; }
+      }
+      // clamp speed
+      var sp = Math.hypot(ship.vx, ship.vy);
+      if (sp > MAX_SPEED) { ship.vx *= MAX_SPEED / sp; ship.vy *= MAX_SPEED / sp; }
+      // friction
+      var f = Math.pow(FRICTION, dt);
+      ship.vx *= f; ship.vy *= f;
+
+      ship.x += ship.vx * dt; ship.y += ship.vy * dt;
+      wrapPos(ship);
+
+      if (shift && hyperCooldown <= 0) doHyperspace();
+
+      if (keys["Space"] || touch.fire) fire();
+
+      if (ship.invuln > 0) ship.invuln -= dt;
+    }
+
+    if (fireTimer > 0) fireTimer -= dt;
+    if (hyperCooldown > 0) hyperCooldown -= dt;
+
+    // bullets
+    for (var i = bullets.length - 1; i >= 0; i--) {
+      var b = bullets[i];
+      b.x += b.vx * dt; b.y += b.vy * dt;
+      wrapPos(b);
+      b.life -= dt;
+      if (b.life <= 0) { bullets.splice(i, 1); continue; }
+    }
+
+    // asteroids
+    for (var j = 0; j < asteroids.length; j++) {
+      var a = asteroids[j];
+      a.x += a.vx * dt; a.y += a.vy * dt;
+      a.rot += a.spin * dt;
+      wrapPos(a);
+    }
+
+    // bullet vs asteroid
+    for (var bi = bullets.length - 1; bi >= 0; bi--) {
+      var bu = bullets[bi];
+      for (var ai = asteroids.length - 1; ai >= 0; ai--) {
+        var as = asteroids[ai];
+        if ((bu.x - as.x) * (bu.x - as.x) + (bu.y - as.y) * (bu.y - as.y) < as.r * as.r) {
+          bullets.splice(bi, 1);
+          splitAsteroid(as, ai);
+          break; // this bullet is gone; move to next bullet
+        }
+      }
+    }
+
+    // ship vs asteroid
+    if (ship && ship.alive && ship.invuln <= 0) {
+      for (var k = 0; k < asteroids.length; k++) {
+        var ak = asteroids[k];
+        var rr = ak.r + ship.r * 0.7;
+        if ((ship.x - ak.x) * (ship.x - ak.x) + (ship.y - ak.y) * (ship.y - ak.y) < rr * rr) {
+          killShip();
+          break;
+        }
+      }
+    }
+
+    // particles
+    for (var p = particles.length - 1; p >= 0; p--) {
+      var pt = particles[p];
+      pt.x += pt.vx * dt; pt.y += pt.vy * dt;
+      pt.vx *= Math.pow(0.4, dt); pt.vy *= Math.pow(0.4, dt);
+      pt.life -= dt;
+      if (pt.life <= 0) particles.splice(p, 1);
+    }
+
+    // wave clear
+    if (asteroids.length === 0) nextWave();
+  }
+
+  // ---------- Render ----------
+  var starField = null;
+  function buildStars() {
+    starField = [];
+    var n = Math.floor((W * H) / 6500);
+    for (var i = 0; i < n; i++) {
+      starField.push({ x: Math.random() * W, y: Math.random() * H, r: Math.random() * 1.4 + 0.2, a: Math.random() * 0.6 + 0.2 });
+    }
+  }
+  buildStars();
+  var lastStarKey = W + "x" + H;
+
+  function drawStars() {
+    var key = W + "x" + H;
+    if (key !== lastStarKey || !starField) { buildStars(); lastStarKey = key; }
+    ctx.save();
+    for (var i = 0; i < starField.length; i++) {
+      var s = starField[i];
+      ctx.globalAlpha = s.a;
+      ctx.fillStyle = "#9fc7ff";
+      ctx.beginPath();
+      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
+      ctx.fill();
+    }
+    ctx.restore();
+  }
+
+  function drawShip() {
+    if (!ship || !ship.alive) return;
+    if (ship.invuln > 0 && Math.floor(ship.invuln * 12) % 2 === 0) return; // blink
+    ctx.save();
+    ctx.translate(ship.x, ship.y);
+    ctx.rotate(ship.a);
+    // thrust flame
+    if (ship.thrusting && Math.random() > 0.35) {
+      ctx.beginPath();
+      ctx.moveTo(-ship.r * 0.6, -5);
+      ctx.lineTo(-ship.r * 1.5 - Math.random() * 8, 0);
+      ctx.lineTo(-ship.r * 0.6, 5);
+      ctx.strokeStyle = "#ff9b2b";
+      ctx.lineWidth = 2;
+      ctx.shadowBlur = 14; ctx.shadowColor = "#ff6a00";
+      ctx.stroke();
+    }
+    // hull
+    ctx.beginPath();
+    ctx.moveTo(ship.r, 0);
+    ctx.lineTo(-ship.r * 0.7, -ship.r * 0.75);
+    ctx.lineTo(-ship.r * 0.4, 0);
+    ctx.lineTo(-ship.r * 0.7, ship.r * 0.75);
+    ctx.closePath();
+    ctx.strokeStyle = "#eafcff";
+    ctx.lineWidth = 2;
+    ctx.shadowBlur = 12; ctx.shadowColor = "#00f0ff";
+    ctx.stroke();
+    ctx.restore();
+  }
+
+  function drawAsteroid(a) {
+    ctx.save();
+    ctx.translate(a.x, a.y);
+    ctx.rotate(a.rot);
+    ctx.beginPath();
+    var n = a.verts.length;
+    for (var i = 0; i < n; i++) {
+      var ang = (i / n) * Math.PI * 2;
+      var rr = a.r * a.verts[i];
+      var px = Math.cos(ang) * rr, py = Math.sin(ang) * rr;
+      if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
+    }
+    ctx.closePath();
+    ctx.strokeStyle = "hsl(" + a.hue + ",85%,68%)";
+    ctx.lineWidth = 2;
+    ctx.shadowBlur = 12; ctx.shadowColor = "hsl(" + a.hue + ",90%,55%)";
+    ctx.stroke();
+    ctx.restore();
+  }
+
+  function drawBullets() {
+    ctx.save();
+    ctx.fillStyle = "#eafcff";
+    ctx.shadowBlur = 10; ctx.shadowColor = "#00f0ff";
+    for (var i = 0; i < bullets.length; i++) {
+      var b = bullets[i];
+      ctx.beginPath();
+      ctx.arc(b.x, b.y, 2.4, 0, Math.PI * 2);
+      ctx.fill();
+    }
+    ctx.restore();
+  }
+
+  function drawParticles() {
+    ctx.save();
+    for (var i = 0; i < particles.length; i++) {
+      var p = particles[i];
+      ctx.globalAlpha = Math.max(0, p.life / p.max);
+      ctx.fillStyle = p.color;
+      ctx.beginPath();
+      ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
+      ctx.fill();
+    }
+    ctx.restore();
+  }
+
+  function render() {
+    ctx.clearRect(0, 0, W, H);
+    drawStars();
+    if (state === "play" || state === "paused" || state === "over") {
+      drawParticles();
+      for (var i = 0; i < (asteroids ? asteroids.length : 0); i++) drawAsteroid(asteroids[i]);
+      drawBullets();
+      drawShip();
+    }
+  }
+
+  // ---------- HUD / overlay ----------
+  var elScore = document.getElementById("score");
+  var elBest = document.getElementById("best");
+  var elWave = document.getElementById("wave");
+  var elLives = document.getElementById("lives");
+  var overlay = document.getElementById("overlay");
+  var ovTitle = document.getElementById("title");
+  var ovMsg = document.getElementById("ovMsg");
+  var ovScore = document.getElementById("ovScore");
+  var playBtn = document.getElementById("playBtn");
+  var pillPaused = document.getElementById("pillPaused");
+  var pillMuted = document.getElementById("pillMuted");
+  var centerToast = document.getElementById("centerToast");
+
+  function shipIcon() {
+    return '<svg width="16" height="16" viewBox="0 0 100 100"><path d="M85 50 L20 20 L38 50 L20 80 Z" fill="none" stroke="#00f0ff" stroke-width="8"/></svg>';
+  }
+
+  function updateHud() {
+    elScore.textContent = score != null ? score : 0;
+    elBest.textContent = "BEST " + best;
+    elWave.textContent = wave != null ? wave : 1;
+    var html = "";
+    var n = Math.max(0, lives != null ? lives : 0);
+    for (var i = 0; i < Math.min(n, 6); i++) html += shipIcon();
+    if (n > 6) html += '<span style="color:#00f0ff;font-size:14px;align-self:center;">x' + n + '</span>';
+    elLives.innerHTML = html;
+  }
+
+  function showOverlay(title, msg, sc, btn) {
+    ovTitle.innerHTML = title === "ABRAMS" ? 'ABRAMS<span class="sub">ASTEROIDS</span>' :
+      (title + '<span class="sub" style="visibility:hidden">.</span>');
+    if (title === "GAME OVER") {
+      ovTitle.innerHTML = 'GAME<span class="sub">OVER</span>';
+    }
+    ovMsg.textContent = msg || "";
+    ovScore.textContent = sc || "";
+    playBtn.textContent = btn || "LAUNCH";
+    overlay.classList.remove("hidden");
+  }
+  function hideOverlay() { overlay.classList.add("hidden"); }
+
+  playBtn.addEventListener("click", function () { Audio.init(); startGame(); });
+
+  var toastTimer = null;
+  function showToast(text, ms) {
+    centerToast.textContent = text;
+    centerToast.classList.add("on");
+    if (toastTimer) clearTimeout(toastTimer);
+    toastTimer = setTimeout(function () { centerToast.classList.remove("on"); }, ms || 800);
+  }
+
+  function togglePause() {
+    if (state === "play") {
+      state = "paused";
+      pillPaused.classList.add("on");
+    } else if (state === "paused") {
+      state = "play";
+      pillPaused.classList.remove("on");
+      lastT = performance.now();
+    }
+  }
+  function toggleMute() {
+    var m = Audio.toggleMute();
+    if (m) pillMuted.classList.add("on"); else pillMuted.classList.remove("on");
+  }
+
+  // ---------- Main loop ----------
+  var lastT = performance.now();
+  function loop(now) {
+    var dt = (now - lastT) / 1000;
+    lastT = now;
+    if (dt > 0.05) dt = 0.05; // clamp to avoid huge steps
+    if (dt < 0) dt = 0;
+
+    update(dt);
+    render();
+    requestAnimationFrame(loop);
+  }
+
+  // init HUD + overlay
+  score = 0; lives = START_LIVES; wave = 1;
+  updateHud();
+  showOverlay("ABRAMS", "Clear the field. Survive the swarm.", "", "LAUNCH");
+
+  requestAnimationFrame(loop);
+})();
+</script>
+</body>
+</html>
diff --git a/games/connect-four/index.html b/games/connect-four/index.html
new file mode 100644
index 0000000..091370a
--- /dev/null
+++ b/games/connect-four/index.html
@@ -0,0 +1,720 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<title>Abrams Connect 4</title>
+<style>
+  :root{
+    --bg0:#070912;
+    --bg1:#0d1226;
+    --board:#0b1030;
+    --board-edge:#1b2a6b;
+    --cell:#050814;
+    --p1:#ff2d78;      /* neon pink */
+    --p1b:#ff8fc0;
+    --p2:#22e0ff;      /* neon cyan */
+    --p2b:#a9f4ff;
+    --ink:#eaf0ff;
+    --muted:#8b96c4;
+    --line:#2a3670;
+    --win:#ffe14d;
+    --accent:#8a6bff;
+  }
+  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
+  html,body{margin:0;height:100%;}
+  body{
+    background:
+      radial-gradient(1200px 700px at 15% -10%, #16205a55, transparent 60%),
+      radial-gradient(1000px 700px at 110% 120%, #4a1e6b55, transparent 55%),
+      linear-gradient(160deg,var(--bg1),var(--bg0));
+    color:var(--ink);
+    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+    display:flex;flex-direction:column;align-items:center;
+    min-height:100%;overflow:hidden;
+    user-select:none;
+  }
+  .wrap{
+    width:100%;max-width:640px;height:100dvh;
+    display:flex;flex-direction:column;
+    padding:10px clamp(8px,3vw,18px);
+    gap:10px;
+  }
+  header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;}
+  .title{
+    font-weight:800;letter-spacing:.5px;font-size:clamp(18px,5vw,26px);
+    background:linear-gradient(90deg,var(--p1),var(--accent),var(--p2));
+    -webkit-background-clip:text;background-clip:text;color:transparent;
+    text-shadow:0 0 24px #8a6bff33;
+    display:flex;align-items:center;gap:8px;
+  }
+  .title .dot{width:12px;height:12px;border-radius:50%;background:var(--p1);box-shadow:0 0 12px var(--p1);}
+  .controls{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
+  select,button{
+    font:inherit;color:var(--ink);
+    background:linear-gradient(180deg,#141c40,#0c1330);
+    border:1px solid var(--line);border-radius:10px;
+    padding:8px 10px;cursor:pointer;transition:.15s;
+    font-size:13px;
+  }
+  button:hover,select:hover{border-color:var(--accent);box-shadow:0 0 0 1px #8a6bff44;}
+  button:active{transform:translateY(1px);}
+  .btn-primary{background:linear-gradient(180deg,#6d54ff,#4b32d6);border-color:#7a63ff;font-weight:700;}
+  .iconbtn{padding:8px 11px;}
+
+  .scorebar{
+    display:flex;gap:8px;align-items:stretch;justify-content:center;
+  }
+  .score{
+    flex:1;max-width:190px;
+    display:flex;align-items:center;gap:10px;
+    background:linear-gradient(180deg,#0f1638,#0a1029);
+    border:1px solid var(--line);border-radius:12px;
+    padding:8px 12px;position:relative;overflow:hidden;
+  }
+  .score.active{border-color:currentColor;box-shadow:0 0 18px -4px currentColor, inset 0 0 22px -12px currentColor;}
+  .score.p1{color:var(--p1);}
+  .score.p2{color:var(--p2);}
+  .score .chip{
+    width:26px;height:26px;border-radius:50%;flex:0 0 auto;
+    background:radial-gradient(circle at 35% 30%,#fff8,currentColor 60%);
+    box-shadow:0 0 12px currentColor;
+  }
+  .score .meta{display:flex;flex-direction:column;line-height:1.15;}
+  .score .name{font-size:12px;color:var(--muted);}
+  .score .val{font-size:20px;font-weight:800;color:var(--ink);}
+
+  .status{
+    text-align:center;font-size:14px;min-height:20px;color:var(--muted);
+    font-weight:600;letter-spacing:.3px;
+  }
+  .status b.p1{color:var(--p1);} .status b.p2{color:var(--p2);}
+
+  .boardwrap{flex:1;display:flex;align-items:center;justify-content:center;min-height:0;}
+  canvas{
+    display:block;width:100%;height:100%;
+    touch-action:none;border-radius:16px;
+    cursor:pointer;
+  }
+  footer{
+    text-align:center;font-size:11px;color:#5b6699;padding-bottom:4px;
+  }
+  .thinking{color:var(--accent);}
+  @media (max-width:420px){
+    .score .val{font-size:18px;}
+  }
+</style>
+</head>
+<body>
+<div class="wrap">
+  <header>
+    <div class="title"><span class="dot"></span>Abrams Connect 4</div>
+    <div class="controls">
+      <select id="mode" title="Game mode">
+        <option value="ai">vs Computer</option>
+        <option value="2p">2 Players</option>
+      </select>
+      <select id="diff" title="Difficulty">
+        <option value="1">Easy</option>
+        <option value="3">Medium</option>
+        <option value="5" selected>Hard</option>
+        <option value="7">Expert</option>
+      </select>
+      <button id="mute" class="iconbtn" title="Toggle sound">🔊</button>
+      <button id="newgame" class="btn-primary">New Game</button>
+    </div>
+  </header>
+
+  <div class="scorebar">
+    <div class="score p1 active" id="score1">
+      <span class="chip"></span>
+      <span class="meta"><span class="name" id="name1">You</span><span class="val" id="val1">0</span></span>
+    </div>
+    <div class="score p2" id="score2">
+      <span class="chip"></span>
+      <span class="meta"><span class="name" id="name2">Computer</span><span class="val" id="val2">0</span></span>
+    </div>
+  </div>
+
+  <div class="status" id="status">Your turn — drop a disc</div>
+
+  <div class="boardwrap">
+    <canvas id="board"></canvas>
+  </div>
+
+  <footer>Connect four in a row — horizontal, vertical, or diagonal · Pure vanilla JS, zero dependencies.</footer>
+</div>
+
+<script>
+(() => {
+  "use strict";
+
+  // ---------- Constants ----------
+  const COLS = 7, ROWS = 6, NEED = 4;
+  const EMPTY = 0, P1 = 1, P2 = 2;
+
+  // ---------- DOM ----------
+  const canvas = document.getElementById('board');
+  const ctx = canvas.getContext('2d');
+  const $ = id => document.getElementById(id);
+  const modeSel = $('mode'), diffSel = $('diff'), muteBtn = $('mute');
+  const statusEl = $('status');
+  const val1 = $('val1'), val2 = $('val2'), name1 = $('name1'), name2 = $('name2');
+  const score1El = $('score1'), score2El = $('score2');
+
+  // ---------- State ----------
+  let board = [];            // board[r][c], r=0 is TOP
+  let current = P1;
+  let scores = { 1:0, 2:0 };
+  let gameOver = false;
+  let winLine = null;        // array of [r,c]
+  let winPlayer = 0;
+  let aiThinking = false;
+  let hoverCol = -1;
+  let muted = false;
+  let animating = false;
+
+  // Animated / falling discs
+  const discs = [];  // {r,c,player,y (px), targetY, vy, settled}
+  let dropAnim = null;
+
+  // Layout (recomputed on resize)
+  let L = { size: 0, pad: 0, cell: 0, radius: 0, boardX: 0, boardY: 0, boardW: 0, boardH: 0, top: 0 };
+
+  function newBoard() {
+    board = Array.from({length: ROWS}, () => new Array(COLS).fill(EMPTY));
+  }
+
+  // ---------- Audio (procedural, optional) ----------
+  let audioCtx = null;
+  function ac() {
+    if (muted) return null;
+    if (!audioCtx) {
+      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
+      catch (e) { return null; }
+    }
+    if (audioCtx.state === 'suspended') audioCtx.resume();
+    return audioCtx;
+  }
+  function tone(freq, dur, type = 'sine', gain = 0.12, slideTo = null) {
+    const a = ac(); if (!a) return;
+    const o = a.createOscillator(), g = a.createGain();
+    o.type = type; o.frequency.value = freq;
+    if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, a.currentTime + dur);
+    g.gain.setValueAtTime(0, a.currentTime);
+    g.gain.linearRampToValueAtTime(gain, a.currentTime + 0.008);
+    g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + dur);
+    o.connect(g); g.connect(a.destination);
+    o.start(); o.stop(a.currentTime + dur + 0.02);
+  }
+  const sfx = {
+    drop(){ tone(220, 0.14, 'triangle', 0.13, 140); },
+    land(){ tone(120, 0.10, 'sine', 0.16); tone(180, 0.06, 'square', 0.05); },
+    hover(){ tone(660, 0.03, 'sine', 0.04); },
+    win(){ [523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.22,'triangle',0.14),i*110)); },
+    draw(){ tone(300,0.2,'sine',0.1,180); },
+    click(){ tone(440,0.04,'square',0.05); }
+  };
+
+  // ---------- Layout ----------
+  function resize() {
+    const wrap = canvas.parentElement;
+    const availW = wrap.clientWidth;
+    const availH = wrap.clientHeight;
+    // board aspect: COLS wide, ROWS+~1 (drop lane) tall
+    const laneRows = ROWS + 1;
+    const targetRatio = COLS / laneRows;
+    let w = availW, h = w / targetRatio;
+    if (h > availH) { h = availH; w = h * targetRatio; }
+    const dpr = Math.min(window.devicePixelRatio || 1, 2.5);
+    canvas.style.width = w + 'px';
+    canvas.style.height = h + 'px';
+    canvas.width = Math.round(w * dpr);
+    canvas.height = Math.round(h * dpr);
+    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+
+    L.cell = w / COLS;
+    L.top = L.cell;                 // drop lane height (one cell)
+    L.boardX = 0;
+    L.boardY = L.top;
+    L.boardW = w;
+    L.boardH = L.cell * ROWS;
+    L.radius = L.cell * 0.40;
+    L.w = w; L.h = h;
+    // sync any settled disc pixel positions
+    for (const d of discs) if (d.settled) d.y = cellCenterY(d.r);
+    draw();
+  }
+
+  function cellCenterX(c){ return L.cell * c + L.cell/2; }
+  function cellCenterY(r){ return L.top + L.cell * r + L.cell/2; }
+
+  // ---------- Drawing ----------
+  function roundRect(x,y,w,h,r){
+    ctx.beginPath();
+    ctx.moveTo(x+r,y);
+    ctx.arcTo(x+w,y,x+w,y+h,r);
+    ctx.arcTo(x+w,y+h,x,y+h,r);
+    ctx.arcTo(x,y+h,x,y,r);
+    ctx.arcTo(x,y,x+w,y,r);
+    ctx.closePath();
+  }
+
+  function discColors(p){
+    return p === P1
+      ? { a:'#ff2d78', b:'#ff8fc0', glow:'#ff2d78' }
+      : { a:'#22e0ff', b:'#a9f4ff', glow:'#22e0ff' };
+  }
+
+  function drawDisc(cx, cy, p, r, highlight){
+    const col = discColors(p);
+    ctx.save();
+    ctx.shadowColor = col.glow;
+    ctx.shadowBlur = highlight ? r*1.1 : r*0.55;
+    const g = ctx.createRadialGradient(cx - r*0.35, cy - r*0.4, r*0.1, cx, cy, r);
+    g.addColorStop(0, col.b);
+    g.addColorStop(0.55, col.a);
+    g.addColorStop(1, p===P1 ? '#a3134b' : '#0f7f96');
+    ctx.fillStyle = g;
+    ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.fill();
+    ctx.shadowBlur = 0;
+    // rim
+    ctx.lineWidth = Math.max(1, r*0.06);
+    ctx.strokeStyle = 'rgba(255,255,255,.22)';
+    ctx.beginPath(); ctx.arc(cx, cy, r*0.96, 0, Math.PI*2); ctx.stroke();
+    // gloss
+    ctx.fillStyle = 'rgba(255,255,255,.28)';
+    ctx.beginPath(); ctx.ellipse(cx - r*0.3, cy - r*0.35, r*0.34, r*0.2, -0.5, 0, Math.PI*2); ctx.fill();
+    ctx.restore();
+  }
+
+  function draw(){
+    if (!L.w) return;
+    ctx.clearRect(0,0,L.w,L.h);
+
+    // hover preview in the lane
+    if (!gameOver && !aiThinking && hoverCol >= 0 && dropRow(hoverCol) >= 0) {
+      const cx = cellCenterX(hoverCol);
+      ctx.save();
+      ctx.globalAlpha = 0.5;
+      drawDisc(cx, L.top/2, current, L.radius, false);
+      ctx.restore();
+      // column guide
+      ctx.save();
+      ctx.globalAlpha = 0.10;
+      ctx.fillStyle = discColors(current).a;
+      ctx.fillRect(L.cell*hoverCol, L.boardY, L.cell, L.boardH);
+      ctx.restore();
+    }
+
+    // board panel
+    ctx.save();
+    ctx.shadowColor = '#1b2a6b';
+    ctx.shadowBlur = 30;
+    const bg = ctx.createLinearGradient(0, L.boardY, 0, L.boardY+L.boardH);
+    bg.addColorStop(0, '#131a44');
+    bg.addColorStop(1, '#0a1030');
+    ctx.fillStyle = bg;
+    roundRect(L.boardX+2, L.boardY, L.boardW-4, L.boardH, L.cell*0.28);
+    ctx.fill();
+    ctx.restore();
+    // board edge stroke
+    ctx.lineWidth = 2;
+    ctx.strokeStyle = 'rgba(120,150,255,.25)';
+    roundRect(L.boardX+2, L.boardY, L.boardW-4, L.boardH, L.cell*0.28);
+    ctx.stroke();
+
+    // holes + settled discs
+    for (let r=0;r<ROWS;r++){
+      for (let c=0;c<COLS;c++){
+        const cx = cellCenterX(c), cy = cellCenterY(r);
+        // hole
+        ctx.save();
+        ctx.fillStyle = '#050814';
+        ctx.beginPath(); ctx.arc(cx, cy, L.radius, 0, Math.PI*2); ctx.fill();
+        ctx.lineWidth = Math.max(1, L.radius*0.05);
+        ctx.strokeStyle = 'rgba(0,0,0,.6)';
+        ctx.beginPath(); ctx.arc(cx, cy, L.radius, 0, Math.PI*2); ctx.stroke();
+        ctx.restore();
+      }
+    }
+
+    // discs (settled + falling)
+    for (const d of discs) {
+      const cx = cellCenterX(d.c);
+      const hi = winLine && winLine.some(p => p[0]===d.r && p[1]===d.c);
+      drawDisc(cx, d.y, d.player, L.radius, hi);
+    }
+
+    // win line
+    if (winLine && winLine.length) {
+      const a = winLine[0], b = winLine[winLine.length-1];
+      ctx.save();
+      ctx.strokeStyle = 'rgba(255,225,77,.9)';
+      ctx.lineWidth = L.radius*0.22;
+      ctx.lineCap = 'round';
+      ctx.shadowColor = '#ffe14d';
+      ctx.shadowBlur = 24;
+      ctx.beginPath();
+      ctx.moveTo(cellCenterX(a[1]), cellCenterY(a[0]));
+      ctx.lineTo(cellCenterX(b[1]), cellCenterY(b[0]));
+      ctx.stroke();
+      ctx.restore();
+    }
+  }
+
+  // ---------- Game logic ----------
+  function dropRow(c){
+    for (let r=ROWS-1;r>=0;r--) if (board[r][c]===EMPTY) return r;
+    return -1;
+  }
+
+  function validCols(bd){
+    const out=[];
+    for (let c=0;c<COLS;c++) if (bd[0][c]===EMPTY) out.push(c);
+    return out;
+  }
+
+  function boardFull(bd){
+    for (let c=0;c<COLS;c++) if (bd[0][c]===EMPTY) return false;
+    return true;
+  }
+
+  // returns winning line (array of [r,c]) for player p, or null
+  function findWin(bd, p){
+    const dirs = [[0,1],[1,0],[1,1],[1,-1]];
+    for (let r=0;r<ROWS;r++){
+      for (let c=0;c<COLS;c++){
+        if (bd[r][c]!==p) continue;
+        for (const [dr,dc] of dirs){
+          const line=[[r,c]];
+          let rr=r+dr, cc=c+dc;
+          while (rr>=0 && rr<ROWS && cc>=0 && cc<COLS && bd[rr][cc]===p){
+            line.push([rr,cc]);
+            if (line.length===NEED) return line;
+            rr+=dr; cc+=dc;
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  // ---------- Human move ----------
+  function attemptDrop(c){
+    if (gameOver || animating || aiThinking) return;
+    if (modeSel.value==='ai' && current===P2) return; // not human's turn
+    const r = dropRow(c);
+    if (r<0) return;
+    doDrop(c, current);
+  }
+
+  function doDrop(c, player){
+    const r = dropRow(c);
+    if (r<0) return;
+    board[r][c] = player;
+    animating = true;
+    sfx.drop();
+    const targetY = cellCenterY(r);
+    const disc = { r, c, player, y: L.top/2, vy: 0, settled:false };
+    discs.push(disc);
+    startFall(disc, targetY);
+  }
+
+  function startFall(disc, targetY){
+    const grav = L.cell * 0.06;   // px per frame^2 (scaled by cell)
+    let last = performance.now();
+    let bounced = false;
+    function step(now){
+      const dt = Math.min(2, (now - last)/16.6667); last = now;
+      disc.vy += grav * dt;
+      disc.y += disc.vy * dt;
+      if (disc.y >= targetY){
+        disc.y = targetY;
+        if (!bounced && disc.vy > L.cell*0.25){
+          disc.vy = -disc.vy * 0.28;  // small bounce
+          bounced = true;
+          sfx.land();
+          draw();
+          requestAnimationFrame(step);
+          return;
+        }
+        disc.settled = true;
+        animating = false;
+        if (!bounced) sfx.land();
+        draw();
+        afterMove(disc.r, disc.c, disc.player);
+        return;
+      }
+      draw();
+      requestAnimationFrame(step);
+    }
+    requestAnimationFrame(step);
+  }
+
+  function afterMove(r, c, player){
+    const win = findWin(board, player);
+    if (win){
+      winLine = win; winPlayer = player; gameOver = true;
+      scores[player]++;
+      updateScores();
+      sfx.win();
+      setStatus(playerName(player, true) + ' wins the round! 🎉');
+      flashWin();
+      draw();
+      return;
+    }
+    if (boardFull(board)){
+      gameOver = true;
+      sfx.draw();
+      setStatus("It's a draw — board full.");
+      draw();
+      return;
+    }
+    current = player===P1 ? P2 : P1;
+    updateTurnUI();
+    if (modeSel.value==='ai' && current===P2 && !gameOver){
+      queueAI();
+    }
+  }
+
+  let winFlashTimer = null;
+  function flashWin(){
+    let n=0;
+    clearInterval(winFlashTimer);
+    winFlashTimer = setInterval(()=>{
+      n++;
+      // toggle handled implicitly by redraw jitter of glow via time — simple pulse:
+      draw();
+      if (n>8) clearInterval(winFlashTimer);
+    }, 140);
+  }
+
+  // ---------- AI: minimax + alpha-beta ----------
+  function queueAI(){
+    aiThinking = true;
+    setStatus('<span class="thinking">Computer is thinking…</span>');
+    draw();
+    // yield to let UI paint, keep responsive
+    setTimeout(runAI, 60);
+  }
+
+  function runAI(){
+    const depth = parseInt(diffSel.value, 10) || 5;
+    const move = bestMove(board, depth);
+    aiThinking = false;
+    if (move == null){ // no move (shouldn't happen)
+      updateTurnUI();
+      return;
+    }
+    doDrop(move, P2);
+  }
+
+  // Order columns center-first for better pruning
+  const COL_ORDER = (() => {
+    const mid=(COLS-1)/2;
+    return [...Array(COLS).keys()].sort((a,b)=>Math.abs(a-mid)-Math.abs(b-mid));
+  })();
+
+  function bestMove(bd, depth){
+    // immediate win / block shortcuts for snappiness at low depth
+    const valid = validColsOrdered(bd);
+    if (!valid.length) return null;
+    // win now?
+    for (const c of valid){ const b2=cloneDrop(bd,c,P2); if (findWin(b2,P2)) return c; }
+    // block opponent win?
+    for (const c of valid){ const b2=cloneDrop(bd,c,P1); if (findWin(b2,P1)) return c; }
+
+    let bestScore = -Infinity, bestCols = [valid[0]];
+    for (const c of valid){
+      const b2 = cloneDrop(bd, c, P2);
+      const s = minimax(b2, depth-1, -Infinity, Infinity, false);
+      if (s > bestScore){ bestScore = s; bestCols = [c]; }
+      else if (s === bestScore){ bestCols.push(c); }
+    }
+    // prefer central among ties
+    bestCols.sort((a,b)=>Math.abs(a-(COLS-1)/2)-Math.abs(b-(COLS-1)/2));
+    return bestCols[0];
+  }
+
+  function validColsOrdered(bd){
+    return COL_ORDER.filter(c => bd[0][c]===EMPTY);
+  }
+
+  function cloneDrop(bd, c, p){
+    const b2 = bd.map(row => row.slice());
+    for (let r=ROWS-1;r>=0;r--){ if (b2[r][c]===EMPTY){ b2[r][c]=p; break; } }
+    return b2;
+  }
+
+  function minimax(bd, depth, alpha, beta, maximizing){
+    if (findWin(bd, P2)) return 100000 + depth;      // AI win, sooner better
+    if (findWin(bd, P1)) return -100000 - depth;     // human win
+    if (depth===0 || boardFull(bd)) return evaluate(bd);
+
+    const cols = validColsOrdered(bd);
+    if (maximizing){
+      let value = -Infinity;
+      for (const c of cols){
+        const b2 = cloneDrop(bd, c, P2);
+        value = Math.max(value, minimax(b2, depth-1, alpha, beta, false));
+        alpha = Math.max(alpha, value);
+        if (alpha >= beta) break;
+      }
+      return value;
+    } else {
+      let value = Infinity;
+      for (const c of cols){
+        const b2 = cloneDrop(bd, c, P1);
+        value = Math.min(value, minimax(b2, depth-1, alpha, beta, true));
+        beta = Math.min(beta, value);
+        if (beta <= alpha) break;
+      }
+      return value;
+    }
+  }
+
+  // Heuristic evaluation from AI (P2) perspective
+  function evaluate(bd){
+    let score = 0;
+    // center column preference
+    const centerC = Math.floor(COLS/2);
+    let centerCount = 0;
+    for (let r=0;r<ROWS;r++) if (bd[r][centerC]===P2) centerCount++;
+    score += centerCount * 6;
+
+    const windows = [];
+    // horizontal
+    for (let r=0;r<ROWS;r++)
+      for (let c=0;c<=COLS-4;c++)
+        windows.push([bd[r][c],bd[r][c+1],bd[r][c+2],bd[r][c+3]]);
+    // vertical
+    for (let c=0;c<COLS;c++)
+      for (let r=0;r<=ROWS-4;r++)
+        windows.push([bd[r][c],bd[r+1][c],bd[r+2][c],bd[r+3][c]]);
+    // diag down-right
+    for (let r=0;r<=ROWS-4;r++)
+      for (let c=0;c<=COLS-4;c++)
+        windows.push([bd[r][c],bd[r+1][c+1],bd[r+2][c+2],bd[r+3][c+3]]);
+    // diag up-right
+    for (let r=3;r<ROWS;r++)
+      for (let c=0;c<=COLS-4;c++)
+        windows.push([bd[r][c],bd[r-1][c+1],bd[r-2][c+2],bd[r-3][c+3]]);
+
+    for (const w of windows) score += scoreWindow(w);
+    return score;
+  }
+
+  function scoreWindow(w){
+    let ai=0, hu=0, e=0;
+    for (const v of w){ if (v===P2) ai++; else if (v===P1) hu++; else e++; }
+    if (ai>0 && hu>0) return 0; // mixed, dead
+    let s=0;
+    if (ai===4) s+=1000;
+    else if (ai===3 && e===1) s+=12;
+    else if (ai===2 && e===2) s+=4;
+    if (hu===4) s-=1000;
+    else if (hu===3 && e===1) s-=16;   // block threats a bit harder
+    else if (hu===2 && e===2) s-=4;
+    return s;
+  }
+
+  // ---------- UI helpers ----------
+  function playerName(p, cap){
+    if (modeSel.value==='ai') return p===P1 ? 'You' : 'Computer';
+    return p===P1 ? 'Player 1' : 'Player 2';
+  }
+  function setStatus(html){ statusEl.innerHTML = html; }
+  function updateScores(){ val1.textContent = scores[P1]; val2.textContent = scores[P2]; }
+  function updateNames(){
+    if (modeSel.value==='ai'){ name1.textContent='You'; name2.textContent='Computer'; }
+    else { name1.textContent='Player 1'; name2.textContent='Player 2'; }
+  }
+  function updateTurnUI(){
+    score1El.classList.toggle('active', current===P1);
+    score2El.classList.toggle('active', current===P2);
+    if (gameOver) return;
+    const cls = current===P1 ? 'p1' : 'p2';
+    const who = playerName(current);
+    if (modeSel.value==='ai' && current===P2){
+      setStatus('<span class="thinking">Computer is thinking…</span>');
+    } else {
+      setStatus(`<b class="${cls}">${who}</b> — drop a disc`);
+    }
+  }
+
+  // ---------- New game ----------
+  function newGame(){
+    clearInterval(winFlashTimer);
+    newBoard();
+    discs.length = 0;
+    current = P1;
+    gameOver = false; winLine = null; winPlayer = 0; aiThinking = false; animating = false;
+    hoverCol = -1;
+    updateNames();
+    updateScores();
+    updateTurnUI();
+    resize();
+    // if AI is P1? No — human/P1 always starts here.
+  }
+
+  // ---------- Input ----------
+  function colFromEvent(e){
+    const rect = canvas.getBoundingClientRect();
+    const x = (e.clientX ?? (e.touches && e.touches[0].clientX)) - rect.left;
+    const c = Math.floor(x / (rect.width / COLS));
+    return Math.max(0, Math.min(COLS-1, c));
+  }
+
+  canvas.addEventListener('mousemove', e => {
+    if (gameOver || aiThinking) { if (hoverCol!==-1){hoverCol=-1;draw();} return; }
+    const c = colFromEvent(e);
+    if (c !== hoverCol){ hoverCol = c; if(!muted && dropRow(c)>=0) sfx.hover(); draw(); }
+  });
+  canvas.addEventListener('mouseleave', () => { if (hoverCol!==-1){ hoverCol=-1; draw(); } });
+  canvas.addEventListener('click', e => { ac(); attemptDrop(colFromEvent(e)); });
+
+  // Touch
+  let touchCol = -1;
+  canvas.addEventListener('touchstart', e => {
+    ac();
+    e.preventDefault();
+    touchCol = colFromEvent(e);
+    hoverCol = touchCol;
+    draw();
+  }, {passive:false});
+  canvas.addEventListener('touchmove', e => {
+    e.preventDefault();
+    const c = colFromEvent(e);
+    if (c!==hoverCol){ hoverCol=c; draw(); }
+  }, {passive:false});
+  canvas.addEventListener('touchend', e => {
+    e.preventDefault();
+    if (hoverCol>=0){ const c=hoverCol; hoverCol=-1; attemptDrop(c); draw(); }
+  }, {passive:false});
+
+  // Keyboard: 1-7 to drop, N new game
+  window.addEventListener('keydown', e => {
+    if (e.key>='1' && e.key<='7'){ ac(); attemptDrop(parseInt(e.key,10)-1); }
+    else if (e.key==='n' || e.key==='N'){ newGame(); }
+  });
+
+  // Controls
+  $('newgame').addEventListener('click', () => { ac(); sfx.click(); newGame(); });
+  modeSel.addEventListener('change', () => { sfx.click(); scores={1:0,2:0}; newGame(); });
+  diffSel.addEventListener('change', () => { sfx.click(); });
+  muteBtn.addEventListener('click', () => {
+    muted = !muted;
+    muteBtn.textContent = muted ? '🔇' : '🔊';
+    if (!muted) { ac(); sfx.click(); }
+  });
+
+  window.addEventListener('resize', resize);
+  window.addEventListener('orientationchange', () => setTimeout(resize, 120));
+
+  // ---------- Boot ----------
+  newGame();
+})();
+</script>
+</body>
+</html>
diff --git a/games/lights-out/index.html b/games/lights-out/index.html
new file mode 100644
index 0000000..badcefd
--- /dev/null
+++ b/games/lights-out/index.html
@@ -0,0 +1,471 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+<title>Abrams Lights Out</title>
+<style>
+  :root{
+    --bg:#070a12;
+    --bg2:#0d1220;
+    --panel:rgba(255,255,255,.045);
+    --stroke:rgba(140,180,255,.14);
+    --txt:#e8eefc;
+    --dim:#8a96b4;
+    --neon:#38f0d6;
+    --neon2:#12b8d8;
+    --lit-a:#3bffe0;
+    --lit-b:#1aa9ff;
+    --gap:clamp(6px,1.4vmin,14px);
+  }
+  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
+  html,body{height:100%;margin:0}
+  body{
+    font-family:'Trebuchet MS',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
+    color:var(--txt);
+    background:
+      radial-gradient(1100px 700px at 15% -10%,rgba(30,200,220,.14),transparent 60%),
+      radial-gradient(900px 600px at 110% 120%,rgba(60,120,255,.13),transparent 55%),
+      linear-gradient(160deg,var(--bg),var(--bg2));
+    min-height:100%;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+    padding:clamp(8px,2vmin,20px);
+    overflow:hidden;
+    user-select:none;
+  }
+  .wrap{
+    width:100%;
+    max-width:min(560px,94vmin);
+    display:flex;
+    flex-direction:column;
+    gap:clamp(10px,2vmin,16px);
+  }
+  header{text-align:center;line-height:1.05}
+  h1{
+    margin:0;
+    font-size:clamp(22px,5.4vmin,40px);
+    font-weight:800;
+    letter-spacing:.5px;
+    background:linear-gradient(90deg,var(--lit-a),var(--lit-b));
+    -webkit-background-clip:text;background-clip:text;
+    -webkit-text-fill-color:transparent;
+    filter:drop-shadow(0 0 14px rgba(56,240,214,.35));
+  }
+  .sub{margin:3px 0 0;color:var(--dim);font-size:clamp(10px,2.4vmin,13px);letter-spacing:2px;text-transform:uppercase}
+
+  .stats{
+    display:flex;justify-content:center;gap:clamp(8px,2vmin,14px);flex-wrap:wrap;
+  }
+  .stat{
+    background:var(--panel);border:1px solid var(--stroke);border-radius:12px;
+    padding:clamp(6px,1.4vmin,10px) clamp(10px,2.4vmin,18px);
+    min-width:74px;text-align:center;backdrop-filter:blur(6px);
+  }
+  .stat .k{font-size:clamp(9px,2vmin,10px);color:var(--dim);letter-spacing:1.5px;text-transform:uppercase}
+  .stat .v{font-size:clamp(16px,4vmin,24px);font-weight:800;font-variant-numeric:tabular-nums;margin-top:2px}
+  .stat.best .v{color:var(--neon)}
+
+  .boardwrap{position:relative;width:100%;aspect-ratio:1/1;margin:0 auto}
+  #board{
+    position:absolute;inset:0;
+    display:grid;gap:var(--gap);
+    padding:var(--gap);
+    background:rgba(4,8,16,.5);
+    border:1px solid var(--stroke);
+    border-radius:clamp(14px,3vmin,22px);
+    box-shadow:inset 0 0 40px rgba(0,0,0,.5), 0 20px 60px rgba(0,0,0,.5);
+  }
+  .cell{
+    border:none;padding:0;margin:0;cursor:pointer;outline:none;
+    border-radius:clamp(8px,2.2vmin,16px);
+    position:relative;
+    background:linear-gradient(160deg,#12182a,#0a0f1c);
+    box-shadow:inset 0 1px 0 rgba(255,255,255,.05), inset 0 0 0 1px rgba(120,150,220,.08),
+               0 6px 14px rgba(0,0,0,.35);
+    transition:transform .16s cubic-bezier(.3,1.4,.5,1),
+               box-shadow .3s ease, background .3s ease, filter .3s ease;
+    color:transparent;
+    -webkit-appearance:none;appearance:none;
+  }
+  .cell::after{
+    content:"";position:absolute;inset:22%;border-radius:50%;
+    background:radial-gradient(circle at 35% 30%,rgba(255,255,255,.15),transparent 60%);
+    opacity:.25;transition:opacity .3s ease;
+  }
+  .cell.on{
+    background:linear-gradient(160deg,var(--lit-a),var(--lit-b));
+    box-shadow:inset 0 0 18px rgba(255,255,255,.5),
+               0 0 22px rgba(56,240,214,.65),
+               0 0 46px rgba(26,169,255,.45),
+               0 6px 16px rgba(0,0,0,.3);
+    filter:saturate(1.15);
+  }
+  .cell.on::after{opacity:.85}
+  .cell:active{transform:scale(.9)}
+  .cell.pulse{animation:pulse .4s ease}
+  @keyframes pulse{0%{transform:scale(1)}45%{transform:scale(1.12)}100%{transform:scale(1)}}
+  @media (hover:hover){
+    .cell:hover{transform:translateY(-2px)}
+    .cell.on:hover{filter:saturate(1.3) brightness(1.06)}
+  }
+
+  .controls{display:flex;flex-wrap:wrap;gap:clamp(6px,1.6vmin,10px);justify-content:center;align-items:center}
+  button.btn,select.btn{
+    font-family:inherit;font-size:clamp(11px,2.6vmin,14px);font-weight:700;
+    color:var(--txt);background:var(--panel);border:1px solid var(--stroke);
+    border-radius:11px;padding:clamp(8px,1.8vmin,11px) clamp(12px,3vmin,18px);
+    cursor:pointer;letter-spacing:.5px;transition:.2s;backdrop-filter:blur(6px);
+    -webkit-appearance:none;appearance:none;
+  }
+  button.btn:hover,select.btn:hover{border-color:var(--neon);color:#fff;box-shadow:0 0 14px rgba(56,240,214,.25)}
+  button.btn:active{transform:translateY(1px)}
+  button.primary{
+    background:linear-gradient(160deg,rgba(56,240,214,.22),rgba(26,169,255,.18));
+    border-color:rgba(56,240,214,.4);color:#eafffb;
+  }
+  select.btn{padding-right:30px;background-image:linear-gradient(45deg,transparent 50%,var(--neon) 50%),linear-gradient(135deg,var(--neon) 50%,transparent 50%);background-position:calc(100% - 15px) center,calc(100% - 10px) center;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
+  select.btn option{background:#0d1220;color:var(--txt)}
+
+  .win-overlay{
+    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
+    border-radius:clamp(14px,3vmin,22px);
+    background:radial-gradient(circle at 50% 40%,rgba(56,240,214,.14),rgba(4,8,16,.86));
+    backdrop-filter:blur(3px);opacity:0;pointer-events:none;transition:opacity .45s ease;
+    z-index:5;text-align:center;
+  }
+  .win-overlay.show{opacity:1;pointer-events:auto}
+  .win-card h2{
+    margin:0;font-size:clamp(28px,8vmin,54px);font-weight:800;
+    background:linear-gradient(90deg,var(--lit-a),#fff,var(--lit-b));
+    -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
+    filter:drop-shadow(0 0 20px rgba(56,240,214,.6));
+    animation:pop .5s cubic-bezier(.2,1.6,.4,1);
+  }
+  @keyframes pop{0%{transform:scale(.4);opacity:0}100%{transform:scale(1);opacity:1}}
+  .win-card p{margin:10px 0 16px;color:var(--txt);font-size:clamp(13px,3.2vmin,16px)}
+  .win-card .big{color:var(--neon);font-weight:800}
+  canvas.confetti{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:4}
+  footer{text-align:center;color:var(--dim);font-size:clamp(9px,2.2vmin,11px);letter-spacing:1px}
+</style>
+</head>
+<body>
+<div class="wrap">
+  <header>
+    <h1>ABRAMS LIGHTS OUT</h1>
+    <p class="sub">Turn every light off</p>
+  </header>
+
+  <div class="stats">
+    <div class="stat"><div class="k">Moves</div><div class="v" id="moves">0</div></div>
+    <div class="stat"><div class="k">Time</div><div class="v" id="time">0:00</div></div>
+    <div class="stat best"><div class="k">Best</div><div class="v" id="best">—</div></div>
+  </div>
+
+  <div class="boardwrap">
+    <div id="board"></div>
+    <canvas class="confetti" id="confetti"></canvas>
+    <div class="win-overlay" id="overlay">
+      <div class="win-card">
+        <h2>SOLVED!</h2>
+        <p>Cleared in <span class="big" id="winMoves">0</span> moves &middot; <span class="big" id="winTime">0:00</span></p>
+        <button class="btn primary" id="againBtn">New Puzzle</button>
+      </div>
+    </div>
+  </div>
+
+  <div class="controls">
+    <button class="btn primary" id="newBtn">New Puzzle</button>
+    <select class="btn" id="sizeSel" title="Grid size">
+      <option value="4">4 × 4</option>
+      <option value="5" selected>5 × 5</option>
+      <option value="6">6 × 6</option>
+    </select>
+    <button class="btn" id="muteBtn" title="Sound">🔊 Sound</button>
+  </div>
+
+  <footer>Click a light to toggle it and its neighbors · Pure vanilla JS, zero dependencies</footer>
+</div>
+
+<script>
+(function(){
+  "use strict";
+  var boardEl = document.getElementById('board');
+  var movesEl = document.getElementById('moves');
+  var timeEl  = document.getElementById('time');
+  var bestEl  = document.getElementById('best');
+  var overlay = document.getElementById('overlay');
+  var winMovesEl = document.getElementById('winMoves');
+  var winTimeEl  = document.getElementById('winTime');
+  var sizeSel = document.getElementById('sizeSel');
+  var muteBtn = document.getElementById('muteBtn');
+  var confettiCanvas = document.getElementById('confetti');
+
+  var N = parseInt(localStorage.getItem('lo_size') || '5', 10);
+  if ([4,5,6].indexOf(N) < 0) N = 5;
+  sizeSel.value = String(N);
+
+  var grid = [];          // boolean[N*N]
+  var cells = [];         // button elements
+  var moves = 0;
+  var solved = false;
+  var startTime = 0;
+  var elapsed = 0;
+  var timerId = null;
+  var muted = localStorage.getItem('lo_muted') === '1';
+
+  updateMuteBtn();
+
+  /* ---------- Audio (procedural, zero assets) ---------- */
+  var actx = null;
+  function audio(){
+    if (muted) return null;
+    if (!actx){
+      try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
+      catch(e){ return null; }
+    }
+    if (actx.state === 'suspended') actx.resume();
+    return actx;
+  }
+  function beep(freq, dur, type, gainVal){
+    var ac = audio(); if (!ac) return;
+    var o = ac.createOscillator();
+    var g = ac.createGain();
+    o.type = type || 'sine';
+    o.frequency.value = freq;
+    g.gain.value = 0;
+    o.connect(g); g.connect(ac.destination);
+    var t = ac.currentTime;
+    var peak = (gainVal == null ? 0.12 : gainVal);
+    g.gain.setValueAtTime(0, t);
+    g.gain.linearRampToValueAtTime(peak, t + 0.008);
+    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
+    o.start(t);
+    o.stop(t + dur + 0.02);
+  }
+  function toggleSound(litCount){
+    // pitch nudges up as fewer lights remain
+    var base = 300 + (N*N - litCount) * 12;
+    beep(base, 0.12, 'triangle', 0.09);
+  }
+  function winSound(){
+    var notes = [523.25, 659.25, 783.99, 1046.5];
+    for (var i=0;i<notes.length;i++){
+      (function(f,i){ setTimeout(function(){ beep(f, 0.35, 'sine', 0.13); }, i*110); })(notes[i], i);
+    }
+  }
+
+  /* ---------- Board build ---------- */
+  function build(){
+    boardEl.style.gridTemplateColumns = 'repeat(' + N + ',1fr)';
+    boardEl.style.gridTemplateRows = 'repeat(' + N + ',1fr)';
+    boardEl.innerHTML = '';
+    cells = [];
+    for (var i=0;i<N*N;i++){
+      var b = document.createElement('button');
+      b.className = 'cell';
+      b.setAttribute('data-i', i);
+      b.setAttribute('aria-label', 'light ' + (i+1));
+      (function(idx){
+        b.addEventListener('click', function(){ press(idx, true); });
+      })(i);
+      boardEl.appendChild(b);
+      cells.push(b);
+    }
+  }
+
+  function render(){
+    for (var i=0;i<N*N;i++){
+      if (grid[i]) cells[i].classList.add('on');
+      else cells[i].classList.remove('on');
+    }
+  }
+
+  function neighbors(idx){
+    var r = Math.floor(idx / N), c = idx % N;
+    var out = [idx];
+    if (r > 0)     out.push(idx - N);
+    if (r < N-1)   out.push(idx + N);
+    if (c > 0)     out.push(idx - 1);
+    if (c < N-1)   out.push(idx + 1);
+    return out;
+  }
+
+  function applyPress(idx){
+    var ns = neighbors(idx);
+    for (var k=0;k<ns.length;k++) grid[ns[k]] = !grid[ns[k]];
+  }
+
+  function litCount(){
+    var n = 0;
+    for (var i=0;i<N*N;i++) if (grid[i]) n++;
+    return n;
+  }
+
+  function press(idx, human){
+    if (solved) return;
+    if (human && moves === 0 && !timerId) startTimer();
+    applyPress(idx);
+    // visual pulse on affected cells
+    var ns = neighbors(idx);
+    for (var k=0;k<ns.length;k++){
+      (function(el){
+        el.classList.remove('pulse');
+        void el.offsetWidth;
+        el.classList.add('pulse');
+      })(cells[ns[k]]);
+    }
+    render();
+    if (human){
+      moves++;
+      movesEl.textContent = moves;
+      var lc = litCount();
+      toggleSound(lc);
+      if (lc === 0) onWin();
+    }
+  }
+
+  /* ---------- Solvable board generation ---------- */
+  function newPuzzle(){
+    solved = false;
+    overlay.classList.remove('show');
+    stopConfetti();
+    moves = 0;
+    movesEl.textContent = '0';
+    resetTimer();
+
+    // start all-off, apply random valid presses -> guaranteed solvable
+    grid = new Array(N*N).fill(false);
+    var total = N*N;
+    var pressCount = Math.max(3, Math.floor(total * (0.35 + Math.random()*0.35)));
+    var used = {};
+    var applied = 0, guard = 0;
+    while (applied < pressCount && guard < 1000){
+      guard++;
+      var idx = Math.floor(Math.random()*total);
+      applyPress(idx);
+      applied++;
+    }
+    // ensure not already solved
+    if (litCount() === 0){ applyPress(Math.floor(Math.random()*total)); }
+    render();
+    loadBest();
+  }
+
+  /* ---------- Win ---------- */
+  function onWin(){
+    solved = true;
+    stopTimer();
+    winSound();
+    winMovesEl.textContent = moves;
+    winTimeEl.textContent = fmt(elapsed);
+    // best
+    var key = bestKey();
+    var prev = parseInt(localStorage.getItem(key) || '0', 10);
+    if (!prev || moves < prev){
+      localStorage.setItem(key, String(moves));
+    }
+    loadBest();
+    setTimeout(function(){ overlay.classList.add('show'); startConfetti(); }, 250);
+  }
+
+  function bestKey(){ return 'lo_best_' + N; }
+  function loadBest(){
+    var v = localStorage.getItem(bestKey());
+    bestEl.textContent = v ? v : '—';
+  }
+
+  /* ---------- Timer ---------- */
+  function fmt(sec){
+    var m = Math.floor(sec/60), s = sec%60;
+    return m + ':' + (s<10?'0':'') + s;
+  }
+  function startTimer(){
+    startTime = Date.now() - elapsed*1000;
+    if (timerId) clearInterval(timerId);
+    timerId = setInterval(function(){
+      elapsed = Math.floor((Date.now()-startTime)/1000);
+      timeEl.textContent = fmt(elapsed);
+    }, 250);
+  }
+  function stopTimer(){ if (timerId){ clearInterval(timerId); timerId = null; } }
+  function resetTimer(){
+    stopTimer(); elapsed = 0; timeEl.textContent = '0:00';
+  }
+
+  /* ---------- Confetti ---------- */
+  var cctx, particles = [], cfRAF = null;
+  function startConfetti(){
+    var wrap = confettiCanvas.parentElement;
+    confettiCanvas.width = wrap.clientWidth;
+    confettiCanvas.height = wrap.clientHeight;
+    cctx = confettiCanvas.getContext('2d');
+    particles = [];
+    var colors = ['#3bffe0','#1aa9ff','#ffffff','#12b8d8','#7cf9ff'];
+    for (var i=0;i<90;i++){
+      particles.push({
+        x: Math.random()*confettiCanvas.width,
+        y: Math.random()*-confettiCanvas.height,
+        r: 3 + Math.random()*5,
+        c: colors[Math.floor(Math.random()*colors.length)],
+        vx: (Math.random()-0.5)*2,
+        vy: 2 + Math.random()*3.5,
+        rot: Math.random()*Math.PI,
+        vr: (Math.random()-0.5)*0.3
+      });
+    }
+    if (cfRAF) cancelAnimationFrame(cfRAF);
+    var frames = 0;
+    (function loop(){
+      frames++;
+      cctx.clearRect(0,0,confettiCanvas.width,confettiCanvas.height);
+      for (var i=0;i<particles.length;i++){
+        var p = particles[i];
+        p.x += p.vx; p.y += p.vy; p.rot += p.vr;
+        cctx.save();
+        cctx.translate(p.x,p.y); cctx.rotate(p.rot);
+        cctx.fillStyle = p.c;
+        cctx.globalAlpha = 0.9;
+        cctx.fillRect(-p.r/2,-p.r/2,p.r,p.r*1.6);
+        cctx.restore();
+        if (p.y > confettiCanvas.height + 20){ p.y = -20; p.x = Math.random()*confettiCanvas.width; }
+      }
+      if (frames < 240 && solved) cfRAF = requestAnimationFrame(loop);
+      else if (solved) { /* fade */ cctx.clearRect(0,0,confettiCanvas.width,confettiCanvas.height); }
+    })();
+  }
+  function stopConfetti(){
+    if (cfRAF) cancelAnimationFrame(cfRAF);
+    if (cctx) cctx.clearRect(0,0,confettiCanvas.width,confettiCanvas.height);
+  }
+
+  /* ---------- Controls ---------- */
+  function updateMuteBtn(){
+    muteBtn.textContent = muted ? '🔇 Muted' : '🔊 Sound';
+  }
+  muteBtn.addEventListener('click', function(){
+    muted = !muted;
+    localStorage.setItem('lo_muted', muted ? '1':'0');
+    updateMuteBtn();
+    if (!muted) beep(500,0.1,'sine',0.08);
+  });
+  document.getElementById('newBtn').addEventListener('click', newPuzzle);
+  document.getElementById('againBtn').addEventListener('click', newPuzzle);
+  sizeSel.addEventListener('change', function(){
+    N = parseInt(sizeSel.value,10);
+    localStorage.setItem('lo_size', String(N));
+    build(); newPuzzle();
+  });
+
+  window.addEventListener('resize', function(){
+    if (solved && overlay.classList.contains('show')) startConfetti();
+  });
+
+  /* ---------- Init ---------- */
+  build();
+  newPuzzle();
+})();
+</script>
+</body>
+</html>
diff --git a/games/maze/index.html b/games/maze/index.html
new file mode 100644
index 0000000..e1009f3
--- /dev/null
+++ b/games/maze/index.html
@@ -0,0 +1,692 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+<title>Abrams Maze</title>
+<style>
+  :root {
+    --bg: #05060c;
+    --panel: rgba(10, 14, 28, 0.72);
+    --wall: #1b2350;
+    --wall-glow: #4b63ff;
+    --floor: #090c1a;
+    --player: #38ffd4;
+    --player-glow: #16f0c8;
+    --goal: #ff5cc8;
+    --goal-glow: #ff2fb0;
+    --trail: rgba(56, 255, 212, 0.16);
+    --hint: #ffe45c;
+    --text: #cfe0ff;
+    --muted: #7488c2;
+    --accent: #6c8bff;
+  }
+  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
+  html, body {
+    width: 100%; height: 100%;
+    background: var(--bg);
+    color: var(--text);
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+    overflow: hidden;
+    overscroll-behavior: none;
+    user-select: none;
+    -webkit-user-select: none;
+    touch-action: none;
+  }
+  #app {
+    position: fixed; inset: 0;
+    display: flex; flex-direction: column;
+    background:
+      radial-gradient(1200px 700px at 50% -10%, rgba(76, 99, 255, 0.16), transparent 60%),
+      radial-gradient(900px 600px at 100% 110%, rgba(255, 92, 200, 0.10), transparent 60%),
+      var(--bg);
+  }
+  header {
+    display: flex; align-items: center; justify-content: space-between;
+    gap: 8px; padding: 10px 14px;
+    flex-wrap: wrap;
+  }
+  .title {
+    font-weight: 800; letter-spacing: 2px; font-size: 17px;
+    text-transform: uppercase;
+    background: linear-gradient(90deg, var(--player), var(--accent), var(--goal));
+    -webkit-background-clip: text; background-clip: text; color: transparent;
+    text-shadow: 0 0 24px rgba(108, 139, 255, 0.35);
+    white-space: nowrap;
+  }
+  .stats { display: flex; gap: 8px; flex-wrap: wrap; }
+  .stat {
+    background: var(--panel);
+    border: 1px solid rgba(108, 139, 255, 0.22);
+    border-radius: 10px;
+    padding: 5px 10px;
+    font-size: 12px;
+    line-height: 1.15;
+    min-width: 62px;
+    text-align: center;
+    backdrop-filter: blur(6px);
+  }
+  .stat b { display: block; font-size: 15px; color: #fff; font-variant-numeric: tabular-nums; margin-top: 1px; }
+  .stat span { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 1px; }
+  #stage { flex: 1; position: relative; min-height: 0; }
+  canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
+  .toolbar {
+    display: flex; gap: 8px; justify-content: center; align-items: center;
+    padding: 8px 12px 4px; flex-wrap: wrap;
+  }
+  button.tool {
+    background: var(--panel);
+    border: 1px solid rgba(108, 139, 255, 0.28);
+    color: var(--text);
+    border-radius: 10px;
+    padding: 8px 12px;
+    font-size: 12px; font-weight: 600;
+    letter-spacing: 0.5px;
+    cursor: pointer;
+    transition: transform .08s ease, border-color .15s ease, background .15s ease;
+  }
+  button.tool:hover { border-color: var(--accent); }
+  button.tool:active { transform: scale(0.94); }
+  button.tool.on { border-color: var(--player); color: var(--player); box-shadow: 0 0 14px rgba(56, 255, 212, 0.25); }
+  /* D-pad */
+  #dpad {
+    display: grid;
+    grid-template-columns: repeat(3, 60px);
+    grid-template-rows: repeat(3, 60px);
+    gap: 8px;
+    justify-content: center;
+    padding: 6px 0 16px;
+    touch-action: none;
+  }
+  .dbtn {
+    background: var(--panel);
+    border: 1px solid rgba(108, 139, 255, 0.3);
+    border-radius: 14px;
+    color: var(--accent);
+    font-size: 26px; font-weight: 700;
+    display: flex; align-items: center; justify-content: center;
+    cursor: pointer;
+    transition: transform .06s ease, background .1s ease;
+    user-select: none;
+  }
+  .dbtn:active, .dbtn.press { background: rgba(108, 139, 255, 0.28); transform: scale(0.9); color: #fff; }
+  .dbtn.up { grid-column: 2; grid-row: 1; }
+  .dbtn.left { grid-column: 1; grid-row: 2; }
+  .dbtn.right { grid-column: 3; grid-row: 2; }
+  .dbtn.down { grid-column: 2; grid-row: 3; }
+  .dbtn.spacer { visibility: hidden; }
+  #banner {
+    position: absolute; left: 50%; top: 42%;
+    transform: translate(-50%, -50%) scale(0.8);
+    text-align: center; pointer-events: none;
+    opacity: 0; transition: opacity .35s ease, transform .35s ease;
+    z-index: 5;
+  }
+  #banner.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
+  #banner .big {
+    font-size: clamp(28px, 8vw, 60px); font-weight: 900; letter-spacing: 2px;
+    background: linear-gradient(90deg, var(--player), var(--goal));
+    -webkit-background-clip: text; background-clip: text; color: transparent;
+    text-shadow: 0 0 40px rgba(255, 92, 200, 0.4);
+  }
+  #banner .sub { color: var(--text); margin-top: 6px; font-size: clamp(13px, 3.5vw, 18px); }
+  .hint-desktop { color: var(--muted); font-size: 11px; text-align: center; padding: 0 12px 4px; }
+  @media (min-width: 760px) and (hover: hover) {
+    #dpad { display: none; }
+  }
+  @media (max-width: 380px) {
+    #dpad { grid-template-columns: repeat(3, 52px); grid-template-rows: repeat(3, 52px); }
+  }
+</style>
+</head>
+<body>
+<div id="app">
+  <header>
+    <div class="title">Abrams Maze</div>
+    <div class="stats">
+      <div class="stat"><span>Level</span><b id="s-level">1</b></div>
+      <div class="stat"><span>Time</span><b id="s-time">0.0</b></div>
+      <div class="stat"><span>Best Time</span><b id="s-best">—</b></div>
+      <div class="stat"><span>Top Level</span><b id="s-top">1</b></div>
+    </div>
+  </header>
+
+  <div id="stage">
+    <canvas id="c"></canvas>
+    <div id="banner"><div class="big" id="banner-big">LEVEL UP</div><div class="sub" id="banner-sub"></div></div>
+  </div>
+
+  <div class="toolbar">
+    <button class="tool" id="btn-hint">💡 Hint Path</button>
+    <button class="tool on" id="btn-trail">🍞 Trail</button>
+    <button class="tool" id="btn-sound">🔇 Sound</button>
+    <button class="tool" id="btn-new">🎲 New Maze</button>
+  </div>
+  <div class="hint-desktop">Arrow keys / WASD to move · hold to glide · reach the pink goal</div>
+
+  <div id="dpad">
+    <div class="dbtn spacer"></div>
+    <div class="dbtn up" data-dir="up">▲</div>
+    <div class="dbtn spacer"></div>
+    <div class="dbtn left" data-dir="left">◀</div>
+    <div class="dbtn spacer"></div>
+    <div class="dbtn right" data-dir="right">▶</div>
+    <div class="dbtn spacer"></div>
+    <div class="dbtn down" data-dir="down">▼</div>
+    <div class="dbtn spacer"></div>
+  </div>
+</div>
+
+<script>
+(function () {
+  "use strict";
+
+  // ---------- Persistent state ----------
+  var LS_KEY = "abrams-maze-v1";
+  var store = { bestTime: null, topLevel: 1, sound: false, trail: true };
+  try {
+    var raw = localStorage.getItem(LS_KEY);
+    if (raw) { var p = JSON.parse(raw); for (var k in p) if (p[k] !== undefined) store[k] = p[k]; }
+  } catch (e) {}
+  function save() { try { localStorage.setItem(LS_KEY, JSON.stringify(store)); } catch (e) {} }
+
+  // ---------- DOM ----------
+  var canvas = document.getElementById("c");
+  var ctx = canvas.getContext("2d");
+  var stage = document.getElementById("stage");
+  var elLevel = document.getElementById("s-level");
+  var elTime = document.getElementById("s-time");
+  var elBest = document.getElementById("s-best");
+  var elTop = document.getElementById("s-top");
+  var banner = document.getElementById("banner");
+  var bannerBig = document.getElementById("banner-big");
+  var bannerSub = document.getElementById("banner-sub");
+
+  // ---------- Audio (procedural, optional) ----------
+  var actx = null;
+  function ensureAudio() {
+    if (!store.sound) return;
+    if (!actx) {
+      try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { actx = null; }
+    }
+    if (actx && actx.state === "suspended") actx.resume();
+  }
+  function beep(freq, dur, type, vol) {
+    if (!store.sound || !actx) return;
+    try {
+      var o = actx.createOscillator(), g = actx.createGain();
+      o.type = type || "sine";
+      o.frequency.value = freq;
+      g.gain.value = 0;
+      o.connect(g); g.connect(actx.destination);
+      var t = actx.currentTime;
+      g.gain.setValueAtTime(0, t);
+      g.gain.linearRampToValueAtTime(vol || 0.08, t + 0.008);
+      g.gain.exponentialRampToValueAtTime(0.0001, t + (dur || 0.09));
+      o.start(t); o.stop(t + (dur || 0.09) + 0.02);
+    } catch (e) {}
+  }
+  function sfxStep() { beep(180 + Math.random() * 40, 0.05, "triangle", 0.04); }
+  function sfxBump() { beep(90, 0.08, "square", 0.05); }
+  function sfxWin() {
+    if (!store.sound || !actx) return;
+    var notes = [523, 659, 784, 1046];
+    notes.forEach(function (f, i) { setTimeout(function () { beep(f, 0.16, "sine", 0.09); }, i * 90); });
+  }
+
+  // ---------- Maze generation (recursive backtracker) ----------
+  // Each cell stores wall bits: 1=N,2=E,4=S,8=W (bit set means wall present)
+  var maze = null; // {cols, rows, cells}
+  var N = 1, E = 2, S = 4, W = 8;
+  var DX = { 1: 0, 2: 1, 4: 0, 8: -1 };
+  var DY = { 1: -1, 2: 0, 4: 1, 8: 0 };
+  var OPP = { 1: 4, 2: 8, 4: 1, 8: 2 };
+
+  function genMaze(cols, rows) {
+    var cells = new Uint8Array(cols * rows);
+    for (var i = 0; i < cells.length; i++) cells[i] = 15; // all walls
+    var visited = new Uint8Array(cols * rows);
+    var stack = [];
+    var startIdx = 0;
+    visited[startIdx] = 1;
+    stack.push(startIdx);
+    while (stack.length) {
+      var cur = stack[stack.length - 1];
+      var cx = cur % cols, cy = (cur / cols) | 0;
+      var dirs = [N, E, S, W];
+      // shuffle
+      for (var s = dirs.length - 1; s > 0; s--) {
+        var r = (Math.random() * (s + 1)) | 0;
+        var tmp = dirs[s]; dirs[s] = dirs[r]; dirs[r] = tmp;
+      }
+      var moved = false;
+      for (var d = 0; d < 4; d++) {
+        var dir = dirs[d];
+        var nx = cx + DX[dir], ny = cy + DY[dir];
+        if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
+        var nIdx = ny * cols + nx;
+        if (visited[nIdx]) continue;
+        cells[cur] &= ~dir;
+        cells[nIdx] &= ~OPP[dir];
+        visited[nIdx] = 1;
+        stack.push(nIdx);
+        moved = true;
+        break;
+      }
+      if (!moved) stack.pop();
+    }
+    return { cols: cols, rows: rows, cells: cells };
+  }
+
+  function canMove(m, cx, cy, dir) {
+    var idx = cy * m.cols + cx;
+    return (m.cells[idx] & dir) === 0;
+  }
+
+  // BFS solution path from start to goal
+  function solvePath(m, sx, sy, gx, gy) {
+    var cols = m.cols, rows = m.rows;
+    var prev = new Int32Array(cols * rows).fill(-1);
+    var seen = new Uint8Array(cols * rows);
+    var q = [sy * cols + sx];
+    seen[q[0]] = 1;
+    var head = 0;
+    while (head < q.length) {
+      var cur = q[head++];
+      if (cur === gy * cols + gx) break;
+      var cx = cur % cols, cy = (cur / cols) | 0;
+      var dirs = [N, E, S, W];
+      for (var d = 0; d < 4; d++) {
+        var dir = dirs[d];
+        if (m.cells[cur] & dir) continue;
+        var nx = cx + DX[dir], ny = cy + DY[dir];
+        if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
+        var nIdx = ny * cols + nx;
+        if (seen[nIdx]) continue;
+        seen[nIdx] = 1; prev[nIdx] = cur; q.push(nIdx);
+      }
+    }
+    var path = [];
+    var t = gy * cols + gx;
+    if (!seen[t]) return path;
+    while (t !== -1) { path.push(t); t = prev[t]; }
+    path.reverse();
+    return path;
+  }
+
+  // ---------- Game state ----------
+  var game = {
+    level: 1,
+    cols: 8, rows: 8,
+    player: { cx: 0, cy: 0, x: 0, y: 0 }, // cx/cy = cell, x/y = pixel (animated)
+    goal: { cx: 0, cy: 0 },
+    startTime: 0,
+    elapsed: 0,
+    running: false,
+    won: false,
+    trail: {},          // "cx,cy" -> visit count
+    hintPath: null,     // array of idx
+    hintUntil: 0,
+    cell: 24, ox: 0, oy: 0
+  };
+
+  function sizeForLevel(lvl) {
+    var base = 7 + lvl; // grows each level
+    return Math.min(base, 45);
+  }
+
+  function newLevel(lvl, isNewGame) {
+    game.level = lvl;
+    var sz = sizeForLevel(lvl);
+    game.cols = sz; game.rows = sz;
+    maze = genMaze(sz, sz);
+    game.player.cx = 0; game.player.cy = 0;
+    game.goal.cx = sz - 1; game.goal.cy = sz - 1;
+    game.trail = {};
+    game.trail["0,0"] = 1;
+    game.hintPath = null;
+    game.hintUntil = 0;
+    game.won = false;
+    game.running = true;
+    game.elapsed = 0;
+    game.startTime = performance.now();
+    layout();
+    // snap pixel pos to start cell
+    game.player.x = game.ox + game.player.cx * game.cell + game.cell / 2;
+    game.player.y = game.oy + game.player.cy * game.cell + game.cell / 2;
+    elLevel.textContent = lvl;
+    updateBanner(isNewGame);
+  }
+
+  function updateBanner(isNewGame) {
+    if (isNewGame) return;
+    bannerBig.textContent = "LEVEL " + game.level;
+    bannerSub.textContent = game.cols + " × " + game.rows + " maze";
+    banner.classList.add("show");
+    setTimeout(function () { banner.classList.remove("show"); }, 1100);
+  }
+
+  // ---------- Layout / sizing ----------
+  var dpr = 1;
+  function layout() {
+    dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 2));
+    var w = stage.clientWidth, h = stage.clientHeight;
+    canvas.width = Math.floor(w * dpr);
+    canvas.height = Math.floor(h * dpr);
+    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+    var pad = 14;
+    var avail = Math.min(w, h) - pad * 2;
+    game.cell = Math.max(6, Math.floor(avail / game.cols));
+    var gridW = game.cell * game.cols;
+    var gridH = game.cell * game.rows;
+    game.ox = Math.floor((w - gridW) / 2);
+    game.oy = Math.floor((h - gridH) / 2);
+  }
+
+  window.addEventListener("resize", function () {
+    var pcx = game.player.cx, pcy = game.player.cy;
+    layout();
+    // keep player pixel aligned to its cell after resize
+    game.player.x = game.ox + pcx * game.cell + game.cell / 2;
+    game.player.y = game.oy + pcy * game.cell + game.cell / 2;
+  });
+
+  // ---------- Movement ----------
+  var moveCooldown = 0;
+  function tryMove(dir) {
+    if (!game.running || game.won) return;
+    var p = game.player;
+    if (!canMove(maze, p.cx, p.cy, dir)) { sfxBump(); return; }
+    p.cx += DX[dir]; p.cy += DY[dir];
+    var key = p.cx + "," + p.cy;
+    game.trail[key] = (game.trail[key] || 0) + 1;
+    sfxStep();
+    if (p.cx === game.goal.cx && p.cy === game.goal.cy) win();
+  }
+
+  var dirMap = {
+    ArrowUp: N, KeyW: N, ArrowRight: E, KeyD: E,
+    ArrowDown: S, KeyS: S, ArrowLeft: W, KeyA: W
+  };
+  var held = {}; // dir -> true
+  var repeatTimer = 0;
+
+  window.addEventListener("keydown", function (e) {
+    var dir = dirMap[e.code];
+    if (dir) {
+      e.preventDefault();
+      ensureAudio();
+      if (!held[dir]) { held[dir] = true; tryMove(dir); repeatTimer = 0; }
+    } else if (e.code === "Space") {
+      e.preventDefault(); doHint();
+    }
+  });
+  window.addEventListener("keyup", function (e) {
+    var dir = dirMap[e.code];
+    if (dir) held[dir] = false;
+  });
+
+  // D-pad + held-to-glide
+  var dpad = document.getElementById("dpad");
+  var dpadHeld = {};
+  var dirWord = { up: N, down: S, left: W, right: E };
+  function dpadPress(word, el) {
+    ensureAudio();
+    var dir = dirWord[word];
+    if (!dpadHeld[dir]) { dpadHeld[dir] = true; tryMove(dir); }
+    if (el) el.classList.add("press");
+  }
+  function dpadRelease(word, el) {
+    dpadHeld[dirWord[word]] = false;
+    if (el) el.classList.remove("press");
+  }
+  Array.prototype.forEach.call(dpad.querySelectorAll(".dbtn[data-dir]"), function (btn) {
+    var word = btn.getAttribute("data-dir");
+    btn.addEventListener("touchstart", function (e) { e.preventDefault(); dpadPress(word, btn); }, { passive: false });
+    btn.addEventListener("touchend", function (e) { e.preventDefault(); dpadRelease(word, btn); }, { passive: false });
+    btn.addEventListener("touchcancel", function (e) { dpadRelease(word, btn); });
+    btn.addEventListener("mousedown", function (e) { e.preventDefault(); dpadPress(word, btn); });
+    btn.addEventListener("mouseup", function () { dpadRelease(word, btn); });
+    btn.addEventListener("mouseleave", function () { dpadRelease(word, btn); });
+  });
+
+  // Swipe on canvas as an alternate touch control
+  var swipe = null;
+  stage.addEventListener("touchstart", function (e) {
+    if (e.target !== canvas) return;
+    ensureAudio();
+    var t = e.touches[0];
+    swipe = { x: t.clientX, y: t.clientY };
+  }, { passive: true });
+  stage.addEventListener("touchmove", function (e) {
+    if (!swipe) return;
+    var t = e.touches[0];
+    var dx = t.clientX - swipe.x, dy = t.clientY - swipe.y;
+    var thresh = 26;
+    if (Math.abs(dx) > thresh || Math.abs(dy) > thresh) {
+      if (Math.abs(dx) > Math.abs(dy)) tryMove(dx > 0 ? E : W);
+      else tryMove(dy > 0 ? S : N);
+      swipe = { x: t.clientX, y: t.clientY };
+    }
+  }, { passive: true });
+  stage.addEventListener("touchend", function () { swipe = null; });
+
+  // ---------- Win / level up ----------
+  function win() {
+    game.won = true;
+    game.running = false;
+    game.elapsed = (performance.now() - game.startTime) / 1000;
+    sfxWin();
+    var t = game.elapsed;
+    var newBest = (store.bestTime == null || t < store.bestTime);
+    if (newBest) store.bestTime = t;
+    if (game.level + 1 > store.topLevel) store.topLevel = game.level + 1;
+    save();
+    refreshHUD();
+    bannerBig.textContent = "SOLVED!";
+    bannerSub.textContent = "Level " + game.level + " · " + t.toFixed(2) + "s" + (newBest ? " · NEW BEST!" : "");
+    banner.classList.add("show");
+    setTimeout(function () {
+      banner.classList.remove("show");
+      setTimeout(function () { newLevel(game.level + 1, false); }, 250);
+    }, 1400);
+  }
+
+  // ---------- Hint path ----------
+  function doHint() {
+    if (!game.running || game.won) return;
+    ensureAudio();
+    game.hintPath = solvePath(maze, game.player.cx, game.player.cy, game.goal.cx, game.goal.cy);
+    game.hintUntil = performance.now() + 1600;
+    beep(660, 0.12, "sine", 0.06);
+  }
+
+  // ---------- HUD ----------
+  function refreshHUD() {
+    elBest.textContent = store.bestTime == null ? "—" : store.bestTime.toFixed(2) + "s";
+    elTop.textContent = store.topLevel;
+  }
+
+  // ---------- Buttons ----------
+  var btnHint = document.getElementById("btn-hint");
+  var btnTrail = document.getElementById("btn-trail");
+  var btnSound = document.getElementById("btn-sound");
+  var btnNew = document.getElementById("btn-new");
+
+  btnHint.addEventListener("click", doHint);
+  btnTrail.addEventListener("click", function () {
+    store.trail = !store.trail; save();
+    btnTrail.classList.toggle("on", store.trail);
+  });
+  btnSound.addEventListener("click", function () {
+    store.sound = !store.sound; save();
+    btnSound.classList.toggle("on", store.sound);
+    btnSound.textContent = store.sound ? "🔊 Sound" : "🔇 Sound";
+    if (store.sound) { ensureAudio(); beep(520, 0.1, "sine", 0.07); }
+  });
+  btnNew.addEventListener("click", function () { ensureAudio(); newLevel(game.level, false); });
+
+  // init button visual states
+  btnTrail.classList.toggle("on", store.trail);
+  btnSound.classList.toggle("on", store.sound);
+  btnSound.textContent = store.sound ? "🔊 Sound" : "🔇 Sound";
+
+  // ---------- Render ----------
+  function draw(now) {
+    var w = stage.clientWidth, h = stage.clientHeight;
+    ctx.clearRect(0, 0, w, h);
+
+    var cell = game.cell, ox = game.ox, oy = game.oy, cols = game.cols, rows = game.rows;
+    var gridW = cell * cols, gridH = cell * rows;
+
+    // floor
+    ctx.fillStyle = getVar("--floor");
+    roundRect(ctx, ox - 4, oy - 4, gridW + 8, gridH + 8, 8);
+    ctx.fill();
+
+    // trail
+    if (store.trail) {
+      ctx.fillStyle = getVar("--trail");
+      for (var key in game.trail) {
+        var parts = key.split(","); var tx = +parts[0], ty = +parts[1];
+        ctx.fillRect(ox + tx * cell + 1, oy + ty * cell + 1, cell - 2, cell - 2);
+      }
+    }
+
+    // hint path
+    if (game.hintPath && now < game.hintUntil) {
+      var pulse = 0.35 + 0.35 * (0.5 + 0.5 * Math.sin(now / 120));
+      ctx.save();
+      ctx.strokeStyle = getVar("--hint");
+      ctx.globalAlpha = pulse;
+      ctx.lineWidth = Math.max(2, cell * 0.28);
+      ctx.lineJoin = "round"; ctx.lineCap = "round";
+      ctx.shadowColor = getVar("--hint");
+      ctx.shadowBlur = 12;
+      ctx.beginPath();
+      for (var hi = 0; hi < game.hintPath.length; hi++) {
+        var id = game.hintPath[hi];
+        var hx = id % cols, hy = (id / cols) | 0;
+        var px = ox + hx * cell + cell / 2, py = oy + hy * cell + cell / 2;
+        if (hi === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
+      }
+      ctx.stroke();
+      ctx.restore();
+    } else if (game.hintPath && now >= game.hintUntil) {
+      game.hintPath = null;
+    }
+
+    // walls
+    ctx.save();
+    ctx.strokeStyle = getVar("--wall-glow");
+    ctx.shadowColor = getVar("--wall-glow");
+    ctx.shadowBlur = Math.min(10, cell * 0.4);
+    ctx.lineWidth = Math.max(1.4, cell * 0.09);
+    ctx.lineCap = "round";
+    ctx.beginPath();
+    for (var y = 0; y < rows; y++) {
+      for (var x = 0; x < cols; x++) {
+        var c = maze.cells[y * cols + x];
+        var x0 = ox + x * cell, y0 = oy + y * cell, x1 = x0 + cell, y1 = y0 + cell;
+        if (c & N) { ctx.moveTo(x0, y0); ctx.lineTo(x1, y0); }
+        if (c & W) { ctx.moveTo(x0, y0); ctx.lineTo(x0, y1); }
+        if (c & E) { ctx.moveTo(x1, y0); ctx.lineTo(x1, y1); }
+        if (c & S) { ctx.moveTo(x0, y1); ctx.lineTo(x1, y1); }
+      }
+    }
+    ctx.stroke();
+    ctx.restore();
+
+    // goal
+    var gx = ox + game.goal.cx * cell + cell / 2;
+    var gy = oy + game.goal.cy * cell + cell / 2;
+    var gr = cell * 0.3;
+    var gpulse = 1 + 0.14 * Math.sin(now / 220);
+    ctx.save();
+    ctx.shadowColor = getVar("--goal-glow");
+    ctx.shadowBlur = 22;
+    ctx.fillStyle = getVar("--goal");
+    star(ctx, gx, gy, gr * gpulse, gr * 0.45 * gpulse, 5, now / 900);
+    ctx.fill();
+    ctx.restore();
+
+    // player (animate pixel toward its cell)
+    var targetX = ox + game.player.cx * cell + cell / 2;
+    var targetY = oy + game.player.cy * cell + cell / 2;
+    game.player.x += (targetX - game.player.x) * 0.35;
+    game.player.y += (targetY - game.player.y) * 0.35;
+    var pr = cell * 0.30;
+    ctx.save();
+    ctx.shadowColor = getVar("--player-glow");
+    ctx.shadowBlur = 20;
+    var grad = ctx.createRadialGradient(game.player.x, game.player.y, 1, game.player.x, game.player.y, pr);
+    grad.addColorStop(0, "#eafffb");
+    grad.addColorStop(0.5, getVar("--player"));
+    grad.addColorStop(1, getVar("--player-glow"));
+    ctx.fillStyle = grad;
+    ctx.beginPath();
+    ctx.arc(game.player.x, game.player.y, pr, 0, Math.PI * 2);
+    ctx.fill();
+    ctx.restore();
+
+    // live timer
+    if (game.running && !game.won) {
+      game.elapsed = (now - game.startTime) / 1000;
+    }
+    elTime.textContent = game.elapsed.toFixed(1);
+  }
+
+  function star(c, cx, cy, outer, inner, points, rot) {
+    c.beginPath();
+    for (var i = 0; i < points * 2; i++) {
+      var rr = (i % 2 === 0) ? outer : inner;
+      var a = rot + (i * Math.PI) / points;
+      var px = cx + Math.cos(a) * rr, py = cy + Math.sin(a) * rr;
+      if (i === 0) c.moveTo(px, py); else c.lineTo(px, py);
+    }
+    c.closePath();
+  }
+  function roundRect(c, x, y, w, h, r) {
+    c.beginPath();
+    c.moveTo(x + r, y);
+    c.arcTo(x + w, y, x + w, y + h, r);
+    c.arcTo(x + w, y + h, x, y + h, r);
+    c.arcTo(x, y + h, x, y, r);
+    c.arcTo(x, y, x + w, y, r);
+    c.closePath();
+  }
+  var varCache = {};
+  function getVar(name) {
+    if (varCache[name]) return varCache[name];
+    var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
+    varCache[name] = v || "#fff";
+    return varCache[name];
+  }
+
+  // ---------- Main loop (held-to-glide repeat) ----------
+  var lastRepeat = 0;
+  function loop(now) {
+    // handle held movement (keyboard + dpad) with a steady cadence
+    var anyHeld = false, heldDir = 0;
+    for (var d in held) if (held[d]) { anyHeld = true; heldDir = +d; }
+    if (!anyHeld) for (var d2 in dpadHeld) if (dpadHeld[d2]) { anyHeld = true; heldDir = +d2; }
+    if (anyHeld && heldDir) {
+      if (now - lastRepeat > 110) { tryMove(heldDir); lastRepeat = now; }
+    }
+    draw(now);
+    requestAnimationFrame(loop);
+  }
+
+  // ---------- Boot ----------
+  function boot() {
+    refreshHUD();
+    // Resume best progress: start at level 1 fresh each session, but keep records.
+    newLevel(1, false);
+    requestAnimationFrame(loop);
+  }
+  // ensure stage has size before first layout
+  requestAnimationFrame(boot);
+})();
+</script>
+</body>
+</html>
diff --git a/games/minesweeper/index.html b/games/minesweeper/index.html
new file mode 100644
index 0000000..0165f34
--- /dev/null
+++ b/games/minesweeper/index.html
@@ -0,0 +1,820 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+<title>Abrams Minesweeper</title>
+<style>
+  :root{
+    --bg:#080b14;
+    --bg2:#0d1220;
+    --panel:#111a2e;
+    --panel2:#0b1120;
+    --grid:#0a0f1c;
+    --cell:#1a2540;
+    --cell-hi:#243258;
+    --cell-lo:#0f1830;
+    --revealed:#0c1424;
+    --line:#22304f;
+    --neon:#00f0ff;
+    --neon2:#ff2bd6;
+    --neon3:#7cff5c;
+    --amber:#ffb020;
+    --danger:#ff3b5c;
+    --text:#cfe3ff;
+    --muted:#6b83ad;
+    --n1:#4ea1ff;
+    --n2:#5cffb0;
+    --n3:#ff6b8a;
+    --n4:#b48bff;
+    --n5:#ffb020;
+    --n6:#38e8ff;
+    --n7:#ff8ad6;
+    --n8:#c8d4ee;
+  }
+  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
+  html,body{margin:0;padding:0;height:100%;}
+  body{
+    font-family:"Segoe UI",Roboto,-apple-system,system-ui,sans-serif;
+    background:
+      radial-gradient(1200px 600px at 20% -10%, rgba(0,240,255,.10), transparent 60%),
+      radial-gradient(900px 500px at 110% 10%, rgba(255,43,214,.10), transparent 55%),
+      linear-gradient(160deg,var(--bg),var(--bg2));
+    color:var(--text);
+    min-height:100%;
+    display:flex;
+    flex-direction:column;
+    align-items:center;
+    justify-content:flex-start;
+    overflow:hidden;
+    user-select:none;
+    -webkit-user-select:none;
+  }
+  #app{
+    width:100%;
+    max-width:920px;
+    min-height:100%;
+    display:flex;
+    flex-direction:column;
+    padding:10px clamp(8px,2vw,18px) 14px;
+    gap:10px;
+  }
+  header{
+    display:flex;
+    align-items:center;
+    justify-content:space-between;
+    gap:10px;
+    flex-wrap:wrap;
+  }
+  .title{
+    display:flex;
+    align-items:baseline;
+    gap:8px;
+    letter-spacing:.5px;
+  }
+  .title h1{
+    margin:0;
+    font-size:clamp(18px,3.4vw,26px);
+    font-weight:800;
+    background:linear-gradient(90deg,var(--neon),var(--neon2));
+    -webkit-background-clip:text;background-clip:text;
+    -webkit-text-fill-color:transparent;
+    text-shadow:0 0 18px rgba(0,240,255,.25);
+  }
+  .title .sub{
+    font-size:11px;
+    color:var(--muted);
+    text-transform:uppercase;
+    letter-spacing:2px;
+  }
+  .controls{
+    display:flex;
+    align-items:center;
+    gap:8px;
+    flex-wrap:wrap;
+  }
+  select,button{
+    font-family:inherit;
+    font-size:13px;
+    color:var(--text);
+    background:linear-gradient(180deg,var(--cell-hi),var(--cell));
+    border:1px solid var(--line);
+    border-radius:9px;
+    padding:8px 12px;
+    cursor:pointer;
+    transition:transform .06s ease,box-shadow .15s ease,border-color .15s ease,background .15s ease;
+    outline:none;
+  }
+  select{padding-right:26px;}
+  button:hover,select:hover{border-color:var(--neon);box-shadow:0 0 12px rgba(0,240,255,.25);}
+  button:active{transform:translateY(1px) scale(.98);}
+  button.primary{
+    background:linear-gradient(180deg,rgba(0,240,255,.22),rgba(0,240,255,.06));
+    border-color:rgba(0,240,255,.5);
+    color:#eafcff;
+    font-weight:700;
+  }
+  button.icon{padding:8px 11px;}
+  button.on{border-color:var(--neon3);box-shadow:0 0 12px rgba(124,255,92,.35);color:#eaffee;}
+  .status{
+    display:flex;
+    align-items:stretch;
+    justify-content:space-between;
+    gap:10px;
+    background:linear-gradient(180deg,var(--panel),var(--panel2));
+    border:1px solid var(--line);
+    border-radius:14px;
+    padding:10px 14px;
+    box-shadow:inset 0 0 24px rgba(0,240,255,.05), 0 6px 20px rgba(0,0,0,.35);
+  }
+  .stat{
+    display:flex;
+    flex-direction:column;
+    align-items:center;
+    gap:2px;
+    min-width:74px;
+  }
+  .stat .label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px;}
+  .stat .val{
+    font-family:"Courier New",monospace;
+    font-weight:800;
+    font-size:clamp(20px,4.4vw,28px);
+    letter-spacing:1px;
+    line-height:1;
+  }
+  .stat.mines .val{color:var(--danger);text-shadow:0 0 10px rgba(255,59,92,.45);}
+  .stat.time .val{color:var(--neon);text-shadow:0 0 10px rgba(0,240,255,.4);}
+  .face{
+    display:flex;align-items:center;justify-content:center;
+    font-size:clamp(24px,6vw,34px);
+    width:clamp(52px,11vw,64px);height:clamp(52px,11vw,64px);
+    border-radius:14px;
+    background:linear-gradient(180deg,var(--cell-hi),var(--cell-lo));
+    border:1px solid var(--line);
+    cursor:pointer;
+    box-shadow:0 4px 14px rgba(0,0,0,.4);
+    transition:transform .06s ease,box-shadow .15s ease;
+  }
+  .face:hover{box-shadow:0 0 16px rgba(0,240,255,.3);}
+  .face:active{transform:translateY(1px) scale(.96);}
+  .boardwrap{
+    flex:1;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+    min-height:0;
+    position:relative;
+  }
+  .board{
+    display:grid;
+    gap:3px;
+    padding:10px;
+    background:linear-gradient(180deg,var(--grid),#060a14);
+    border:1px solid var(--line);
+    border-radius:16px;
+    box-shadow:inset 0 0 40px rgba(0,240,255,.06), 0 10px 34px rgba(0,0,0,.5);
+    touch-action:manipulation;
+  }
+  .cell{
+    display:flex;align-items:center;justify-content:center;
+    font-weight:800;
+    line-height:1;
+    border-radius:6px;
+    background:linear-gradient(180deg,var(--cell-hi),var(--cell));
+    border:1px solid rgba(255,255,255,.04);
+    box-shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 2px 4px rgba(0,0,0,.35);
+    cursor:pointer;
+    transition:background .08s ease,transform .05s ease,box-shadow .12s ease;
+    color:var(--text);
+    overflow:hidden;
+  }
+  .cell:hover:not(.open):not(.flag){background:linear-gradient(180deg,#2c3d68,#1e2c50);}
+  .cell.open{
+    background:var(--revealed);
+    box-shadow:inset 0 0 0 1px rgba(0,240,255,.05);
+    cursor:default;
+    border-color:rgba(0,240,255,.06);
+  }
+  .cell.open.empty{background:#0a1120;}
+  .cell.flag{background:linear-gradient(180deg,#2a1636,#1c0f26);}
+  .cell.mine{background:radial-gradient(circle at 50% 45%,rgba(255,59,92,.5),#200712);}
+  .cell.exploded{background:radial-gradient(circle at 50% 45%,#ff3b5c,#7a0018);box-shadow:0 0 18px rgba(255,59,92,.7);}
+  .cell.wrong{background:linear-gradient(180deg,#2a2030,#221018);}
+  .cell.hint{animation:pulse 1s ease infinite;}
+  @keyframes pulse{0%,100%{box-shadow:inset 0 0 0 2px var(--neon);}50%{box-shadow:inset 0 0 12px 2px var(--neon);}}
+  .n1{color:var(--n1);}.n2{color:var(--n2);}.n3{color:var(--n3);}.n4{color:var(--n4);}
+  .n5{color:var(--n5);}.n6{color:var(--n6);}.n7{color:var(--n7);}.n8{color:var(--n8);}
+  .flagbar{
+    display:flex;align-items:center;justify-content:center;gap:10px;
+    flex-wrap:wrap;
+  }
+  .flagtoggle{
+    display:flex;align-items:center;gap:10px;
+    padding:10px 18px;
+    font-size:15px;font-weight:700;
+    border-radius:12px;
+    background:linear-gradient(180deg,var(--cell-hi),var(--cell));
+    border:1px solid var(--line);
+  }
+  .flagtoggle.armed{
+    border-color:var(--neon2);
+    box-shadow:0 0 18px rgba(255,43,214,.4);
+    background:linear-gradient(180deg,rgba(255,43,214,.25),rgba(255,43,214,.08));
+    color:#ffeafb;
+  }
+  .flagtoggle .mode{font-size:11px;letter-spacing:1px;text-transform:uppercase;opacity:.85;}
+  .hintbtn{font-size:13px;}
+  .overlay{
+    position:absolute;inset:0;
+    display:none;
+    align-items:center;justify-content:center;
+    background:rgba(4,8,16,.72);
+    backdrop-filter:blur(3px);
+    border-radius:16px;
+    z-index:5;
+    padding:16px;
+  }
+  .overlay.show{display:flex;animation:fade .25s ease;}
+  @keyframes fade{from{opacity:0}to{opacity:1}}
+  .card{
+    text-align:center;
+    background:linear-gradient(180deg,var(--panel),var(--panel2));
+    border:1px solid var(--line);
+    border-radius:18px;
+    padding:26px 30px;
+    box-shadow:0 16px 50px rgba(0,0,0,.6);
+    max-width:340px;
+  }
+  .card h2{
+    margin:0 0 6px;
+    font-size:30px;
+    letter-spacing:1px;
+  }
+  .card.win h2{color:var(--neon3);text-shadow:0 0 20px rgba(124,255,92,.5);}
+  .card.lose h2{color:var(--danger);text-shadow:0 0 20px rgba(255,59,92,.5);}
+  .card p{margin:6px 0;color:var(--text);font-size:14px;}
+  .card .best{color:var(--amber);font-weight:700;}
+  .card .newbest{color:var(--neon3);font-weight:800;animation:pulse 1s infinite;}
+  .card button{margin-top:16px;font-size:15px;padding:12px 26px;}
+  footer{
+    text-align:center;font-size:11px;color:var(--muted);letter-spacing:.5px;
+  }
+  footer .best-line{color:var(--amber);}
+  @media (max-width:520px){
+    .stat{min-width:58px;}
+    .controls{width:100%;justify-content:center;}
+  }
+</style>
+</head>
+<body>
+<div id="app">
+  <header>
+    <div class="title">
+      <h1>ABRAMS MINESWEEPER</h1>
+      <span class="sub">neon sweep</span>
+    </div>
+    <div class="controls">
+      <select id="difficulty" aria-label="Difficulty">
+        <option value="beginner">Beginner · 9×9 · 10</option>
+        <option value="intermediate">Intermediate · 16×16 · 40</option>
+        <option value="expert">Expert · 16×30 · 99</option>
+      </select>
+      <button id="newgame" class="primary">↻ New Game</button>
+      <button id="mute" class="icon on" title="Toggle sound">🔊</button>
+    </div>
+  </header>
+
+  <div class="status">
+    <div class="stat mines">
+      <span class="label">Mines</span>
+      <span class="val" id="mineCount">010</span>
+    </div>
+    <div class="face" id="face" title="New game">🙂</div>
+    <div class="stat time">
+      <span class="label">Time</span>
+      <span class="val" id="timer">000</span>
+    </div>
+  </div>
+
+  <div class="flagbar">
+    <div class="flagtoggle" id="flagToggle" role="button" tabindex="0" aria-pressed="false">
+      <span id="flagIcon">⛏️</span>
+      <span class="mode" id="flagMode">Dig mode</span>
+    </div>
+    <button class="hintbtn" id="hint">💡 Hint</button>
+  </div>
+
+  <div class="boardwrap">
+    <div class="board" id="board"></div>
+    <div class="overlay" id="overlay">
+      <div class="card" id="card">
+        <h2 id="ovTitle">You Win!</h2>
+        <p id="ovMsg"></p>
+        <p id="ovBest"></p>
+        <button class="primary" id="ovBtn">Play Again</button>
+      </div>
+    </div>
+  </div>
+
+  <footer>
+    <div class="best-line" id="footerBest"></div>
+    <div>Left-click / tap to dig · Right-click or Flag toggle to mark · First click is always safe</div>
+  </footer>
+</div>
+
+<script>
+(function(){
+  "use strict";
+
+  const DIFFS = {
+    beginner:     { rows:9,  cols:9,  mines:10, label:"Beginner" },
+    intermediate: { rows:16, cols:16, mines:40, label:"Intermediate" },
+    expert:       { rows:16, cols:30, mines:99, label:"Expert" }
+  };
+
+  const LS_KEY = "abrams_minesweeper_best_v1";
+  const LS_MUTE = "abrams_minesweeper_mute_v1";
+
+  // --- DOM refs
+  const boardEl   = document.getElementById("board");
+  const mineCountEl = document.getElementById("mineCount");
+  const timerEl   = document.getElementById("timer");
+  const faceEl    = document.getElementById("face");
+  const diffSel   = document.getElementById("difficulty");
+  const newBtn    = document.getElementById("newgame");
+  const muteBtn   = document.getElementById("mute");
+  const flagToggle= document.getElementById("flagToggle");
+  const flagIcon  = document.getElementById("flagIcon");
+  const flagMode  = document.getElementById("flagMode");
+  const hintBtn   = document.getElementById("hint");
+  const overlay   = document.getElementById("overlay");
+  const card      = document.getElementById("card");
+  const ovTitle   = document.getElementById("ovTitle");
+  const ovMsg     = document.getElementById("ovMsg");
+  const ovBest    = document.getElementById("ovBest");
+  const ovBtn     = document.getElementById("ovBtn");
+  const footerBest= document.getElementById("footerBest");
+
+  // --- state
+  let cfg, grid, cellEls, flagMode_on=false, firstClickDone, gameOver, won;
+  let flagsPlaced=0, revealedCount=0, totalSafe=0;
+  let timer=0, timerId=null, muted=false;
+
+  // ---------- Web Audio (procedural, no assets) ----------
+  let audioCtx=null;
+  function ac(){
+    if(muted) return null;
+    if(!audioCtx){
+      try{ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); }
+      catch(e){ return null; }
+    }
+    if(audioCtx.state==="suspended") audioCtx.resume();
+    return audioCtx;
+  }
+  function blip(freq, dur, type, vol){
+    const ctx=ac(); if(!ctx) return;
+    const o=ctx.createOscillator(), g=ctx.createGain();
+    o.type=type||"square"; o.frequency.value=freq;
+    g.gain.value=0.0001;
+    o.connect(g); g.connect(ctx.destination);
+    const t=ctx.currentTime;
+    g.gain.setValueAtTime(0.0001,t);
+    g.gain.exponentialRampToValueAtTime(vol||0.12, t+0.008);
+    g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
+    o.start(t); o.stop(t+dur+0.02);
+  }
+  const S = {
+    reveal(){ blip(520,0.05,"triangle",0.08); },
+    flood(){ blip(680,0.07,"sine",0.06); },
+    flag(){ blip(880,0.06,"square",0.10); },
+    unflag(){ blip(300,0.06,"square",0.08); },
+    win(){ [523,659,784,1047].forEach((f,i)=>setTimeout(()=>blip(f,0.18,"triangle",0.14),i*110)); },
+    lose(){ blip(160,0.4,"sawtooth",0.16); setTimeout(()=>blip(90,0.5,"sawtooth",0.14),140); },
+    click(){ blip(440,0.03,"square",0.05); }
+  };
+
+  // ---------- persistence ----------
+  function loadBest(){
+    try{ return JSON.parse(localStorage.getItem(LS_KEY)) || {}; }
+    catch(e){ return {}; }
+  }
+  function saveBest(o){
+    try{ localStorage.setItem(LS_KEY, JSON.stringify(o)); }catch(e){}
+  }
+  function getBest(key){ const b=loadBest(); return (typeof b[key]==="number")?b[key]:null; }
+  function setBest(key,val){ const b=loadBest(); b[key]=val; saveBest(b); }
+
+  function loadMute(){
+    try{ return localStorage.getItem(LS_MUTE)==="1"; }catch(e){ return false; }
+  }
+  function saveMute(v){ try{ localStorage.setItem(LS_MUTE, v?"1":"0"); }catch(e){} }
+
+  // ---------- helpers ----------
+  function pad3(n){ n=Math.max(0,Math.min(999,n|0)); return String(n).padStart(3,"0"); }
+  function idx(r,c){ return r*cfg.cols+c; }
+  function inBounds(r,c){ return r>=0 && r<cfg.rows && c>=0 && c<cfg.cols; }
+  function neighbors(r,c){
+    const out=[];
+    for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){
+      if(dr===0&&dc===0) continue;
+      if(inBounds(r+dr,c+dc)) out.push([r+dr,c+dc]);
+    }
+    return out;
+  }
+
+  // ---------- board sizing ----------
+  function sizeBoard(){
+    // Compute cell size that fits the available viewport.
+    const app = document.getElementById("app");
+    const appW = app.clientWidth - (parseFloat(getComputedStyle(app).paddingLeft)+parseFloat(getComputedStyle(app).paddingRight));
+    // available height: viewport minus other chrome (approx). Use boardwrap.
+    const wrap = boardEl.parentElement;
+    const wrapH = wrap.clientHeight || (window.innerHeight*0.55);
+    const gap=3, pad=10;
+    const availW = Math.max(120, appW) - pad*2 - gap*(cfg.cols-1);
+    const availH = Math.max(120, wrapH) - pad*2 - gap*(cfg.rows-1) - 4;
+    let size = Math.floor(Math.min(availW/cfg.cols, availH/cfg.rows));
+    size = Math.max(16, Math.min(46, size));
+    boardEl.style.gridTemplateColumns = `repeat(${cfg.cols}, ${size}px)`;
+    boardEl.style.gridTemplateRows    = `repeat(${cfg.rows}, ${size}px)`;
+    const fs = Math.max(10, Math.round(size*0.56));
+    for(const el of cellEls){ el.style.fontSize = fs+"px"; }
+  }
+
+  // ---------- new game ----------
+  function newGame(){
+    const key = diffSel.value;
+    cfg = DIFFS[key];
+    grid = [];
+    cellEls = [];
+    firstClickDone=false; gameOver=false; won=false;
+    flagsPlaced=0; revealedCount=0;
+    totalSafe = cfg.rows*cfg.cols - cfg.mines;
+    stopTimer(); timer=0; timerEl.textContent="000";
+    faceEl.textContent="🙂";
+    overlay.classList.remove("show");
+    setFlagMode(false);
+
+    for(let i=0;i<cfg.rows*cfg.cols;i++){
+      grid.push({ mine:false, count:0, open:false, flag:false, r:Math.floor(i/cfg.cols), c:i%cfg.cols });
+    }
+
+    boardEl.innerHTML="";
+    const frag=document.createDocumentFragment();
+    for(let r=0;r<cfg.rows;r++){
+      for(let c=0;c<cfg.cols;c++){
+        const el=document.createElement("div");
+        el.className="cell";
+        el.dataset.r=r; el.dataset.c=c;
+        frag.appendChild(el);
+        cellEls.push(el);
+      }
+    }
+    boardEl.appendChild(frag);
+    mineCountEl.textContent = pad3(cfg.mines);
+    sizeBoard();
+    updateFooterBest();
+  }
+
+  // ---------- mine placement (first click safe) ----------
+  function placeMines(safeR, safeC){
+    // build forbidden set: the clicked cell + its neighbors (guarantees an opening)
+    const forbidden = new Set();
+    forbidden.add(idx(safeR,safeC));
+    for(const [nr,nc] of neighbors(safeR,safeC)) forbidden.add(idx(nr,nc));
+
+    // if board too small to keep all neighbors safe, at least keep the clicked cell safe
+    let openable = cfg.rows*cfg.cols - forbidden.size;
+    let localForbidden = forbidden;
+    if(openable < cfg.mines){
+      localForbidden = new Set([idx(safeR,safeC)]);
+    }
+
+    const candidates=[];
+    for(let i=0;i<cfg.rows*cfg.cols;i++){ if(!localForbidden.has(i)) candidates.push(i); }
+    // Fisher-Yates partial shuffle
+    for(let i=candidates.length-1;i>0;i--){
+      const j=Math.floor(Math.random()*(i+1));
+      [candidates[i],candidates[j]]=[candidates[j],candidates[i]];
+    }
+    for(let i=0;i<cfg.mines;i++){ grid[candidates[i]].mine=true; }
+
+    // counts
+    for(let r=0;r<cfg.rows;r++)for(let c=0;c<cfg.cols;c++){
+      const cell=grid[idx(r,c)];
+      if(cell.mine){ cell.count=-1; continue; }
+      let n=0;
+      for(const [nr,nc] of neighbors(r,c)){ if(grid[idx(nr,nc)].mine) n++; }
+      cell.count=n;
+    }
+  }
+
+  // ---------- rendering a cell ----------
+  function renderCell(i){
+    const cell=grid[i], el=cellEls[i];
+    el.className="cell";
+    el.textContent="";
+    if(cell.flag && !cell.open){
+      el.classList.add("flag");
+      el.textContent="🚩";
+      return;
+    }
+    if(cell.open){
+      el.classList.add("open");
+      if(cell.mine){
+        el.classList.add("mine");
+        el.textContent="💣";
+      } else if(cell.count>0){
+        el.classList.add("n"+cell.count);
+        el.textContent=String(cell.count);
+      } else {
+        el.classList.add("empty");
+      }
+    }
+  }
+
+  // ---------- reveal / flood ----------
+  function reveal(r,c){
+    if(gameOver) return;
+    const start=grid[idx(r,c)];
+    if(start.open || start.flag) return;
+
+    if(!firstClickDone){
+      placeMines(r,c);
+      firstClickDone=true;
+      startTimer();
+    }
+
+    if(start.mine){
+      explode(idx(r,c));
+      return;
+    }
+
+    // iterative flood fill
+    const stack=[[r,c]];
+    let flooded=0;
+    while(stack.length){
+      const [cr,cc]=stack.pop();
+      const ci=idx(cr,cc);
+      const cell=grid[ci];
+      if(cell.open || cell.flag || cell.mine) continue;
+      cell.open=true; revealedCount++; flooded++;
+      renderCell(ci);
+      if(cell.count===0){
+        for(const [nr,nc] of neighbors(cr,cc)){
+          const ni=idx(nr,nc);
+          if(!grid[ni].open && !grid[ni].flag && !grid[ni].mine){
+            stack.push([nr,nc]);
+          }
+        }
+      }
+    }
+    if(flooded>1) S.flood(); else S.reveal();
+    checkWin();
+  }
+
+  // chord: if an open numbered cell has exactly `count` flags around it, reveal the rest
+  function chord(r,c){
+    if(gameOver) return;
+    const cell=grid[idx(r,c)];
+    if(!cell.open || cell.count<=0) return;
+    let flags=0; const toOpen=[];
+    for(const [nr,nc] of neighbors(r,c)){
+      const nb=grid[idx(nr,nc)];
+      if(nb.flag) flags++;
+      else if(!nb.open) toOpen.push([nr,nc]);
+    }
+    if(flags===cell.count){
+      for(const [nr,nc] of toOpen){ reveal(nr,nc); if(gameOver) return; }
+    }
+  }
+
+  function explode(i){
+    grid[i].open=true;
+    gameOver=true; won=false;
+    stopTimer();
+    // reveal all mines + mark wrong flags
+    for(let k=0;k<grid.length;k++){
+      const cell=grid[k];
+      if(cell.mine){
+        cell.open=true; renderCell(k);
+        if(k===i) cellEls[k].classList.add("exploded");
+      } else if(cell.flag){
+        renderCell(k);
+        cellEls[k].classList.add("wrong");
+        cellEls[k].textContent="❌";
+      }
+    }
+    cellEls[i].classList.add("exploded");
+    faceEl.textContent="😵";
+    S.lose();
+    setTimeout(()=>showOverlay(false), 380);
+  }
+
+  function checkWin(){
+    if(gameOver) return;
+    if(revealedCount>=totalSafe){
+      gameOver=true; won=true;
+      stopTimer();
+      // auto-flag remaining mines
+      for(let k=0;k<grid.length;k++){
+        if(grid[k].mine && !grid[k].flag){ grid[k].flag=true; renderCell(k); }
+      }
+      flagsPlaced=cfg.mines;
+      mineCountEl.textContent="000";
+      faceEl.textContent="😎";
+      S.win();
+      handleWinRecord();
+      setTimeout(()=>showOverlay(true), 260);
+    }
+  }
+
+  // ---------- flag ----------
+  function toggleFlag(r,c){
+    if(gameOver) return;
+    const cell=grid[idx(r,c)];
+    if(cell.open) return;
+    if(!firstClickDone){ startTimer(); firstClickDone=false; } // don't place mines on flag
+    cell.flag=!cell.flag;
+    flagsPlaced += cell.flag?1:-1;
+    renderCell(idx(r,c));
+    mineCountEl.textContent = pad3(cfg.mines - flagsPlaced);
+    if(cell.flag) S.flag(); else S.unflag();
+  }
+
+  // ---------- timer ----------
+  function startTimer(){
+    if(timerId) return;
+    timerId=setInterval(()=>{
+      timer++; if(timer>999) timer=999;
+      timerEl.textContent=pad3(timer);
+    },1000);
+  }
+  function stopTimer(){ if(timerId){ clearInterval(timerId); timerId=null; } }
+
+  // ---------- win record ----------
+  function handleWinRecord(){
+    const key=diffSel.value;
+    const prev=getBest(key);
+    if(prev===null || timer<prev){
+      setBest(key, timer);
+      won_newBest=true;
+    } else {
+      won_newBest=false;
+    }
+    updateFooterBest();
+  }
+  let won_newBest=false;
+
+  // ---------- overlay ----------
+  function showOverlay(isWin){
+    card.classList.remove("win","lose");
+    if(isWin){
+      card.classList.add("win");
+      ovTitle.textContent="⚡ CLEARED ⚡";
+      ovMsg.textContent = `${DIFFS[diffSel.value].label} swept in ${timer}s`;
+      const best=getBest(diffSel.value);
+      if(won_newBest){
+        ovBest.innerHTML = `<span class="newbest">★ NEW BEST TIME ★</span>`;
+      } else {
+        ovBest.innerHTML = best!==null ? `<span class="best">Best: ${best}s</span>` : "";
+      }
+    } else {
+      card.classList.add("lose");
+      ovTitle.textContent="💥 BOOM 💥";
+      ovMsg.textContent="You hit a mine.";
+      const best=getBest(diffSel.value);
+      ovBest.innerHTML = best!==null ? `<span class="best">Best: ${best}s</span>` : "";
+    }
+    overlay.classList.add("show");
+  }
+
+  function updateFooterBest(){
+    const best=getBest(diffSel.value);
+    footerBest.textContent = best!==null
+      ? `Best ${DIFFS[diffSel.value].label}: ${best}s`
+      : `No best time yet for ${DIFFS[diffSel.value].label}`;
+  }
+
+  // ---------- flag mode toggle ----------
+  function setFlagMode(on){
+    flagMode_on=on;
+    flagToggle.classList.toggle("armed",on);
+    flagToggle.setAttribute("aria-pressed", on?"true":"false");
+    flagIcon.textContent = on?"🚩":"⛏️";
+    flagMode.textContent = on?"Flag mode":"Dig mode";
+  }
+
+  // ---------- hint ----------
+  function giveHint(){
+    if(gameOver) return;
+    if(!firstClickDone){
+      // hint the center-ish safe first click
+      const r=Math.floor(cfg.rows/2), c=Math.floor(cfg.cols/2);
+      pulseHint(idx(r,c));
+      return;
+    }
+    // find a safe unopened, unflagged cell
+    const safe=[];
+    for(let k=0;k<grid.length;k++){
+      if(!grid[k].open && !grid[k].flag && !grid[k].mine) safe.push(k);
+    }
+    if(safe.length){
+      pulseHint(safe[Math.floor(Math.random()*safe.length)]);
+      S.click();
+    }
+  }
+  function pulseHint(i){
+    const el=cellEls[i];
+    el.classList.add("hint");
+    setTimeout(()=>el.classList.remove("hint"),1600);
+  }
+
+  // ---------- event handling ----------
+  let touchTimer=null, touchMoved=false, longPressed=false;
+
+  function cellFromEvent(e){
+    const t = e.target.closest ? e.target.closest(".cell") : null;
+    if(!t) return null;
+    return { r:+t.dataset.r, c:+t.dataset.c };
+  }
+
+  boardEl.addEventListener("click", (e)=>{
+    if(longPressed){ longPressed=false; return; }
+    const pos=cellFromEvent(e); if(!pos) return;
+    if(gameOver) return;
+    ac(); // unlock audio on gesture
+    const cell=grid[idx(pos.r,pos.c)];
+    if(flagMode_on){
+      toggleFlag(pos.r,pos.c);
+    } else if(cell.open && cell.count>0){
+      chord(pos.r,pos.c);
+    } else {
+      reveal(pos.r,pos.c);
+    }
+  });
+
+  boardEl.addEventListener("contextmenu",(e)=>{
+    e.preventDefault();
+    const pos=cellFromEvent(e); if(!pos) return;
+    if(gameOver) return;
+    ac();
+    const cell=grid[idx(pos.r,pos.c)];
+    if(cell.open && cell.count>0) chord(pos.r,pos.c);
+    else toggleFlag(pos.r,pos.c);
+  });
+
+  // touch: long-press = flag
+  boardEl.addEventListener("touchstart",(e)=>{
+    if(gameOver) return;
+    const pos=cellFromEvent(e); if(!pos) return;
+    touchMoved=false; longPressed=false;
+    touchTimer=setTimeout(()=>{
+      longPressed=true;
+      ac();
+      const cell=grid[idx(pos.r,pos.c)];
+      if(cell.open && cell.count>0) chord(pos.r,pos.c);
+      else toggleFlag(pos.r,pos.c);
+    },420);
+  },{passive:true});
+  boardEl.addEventListener("touchmove",()=>{ touchMoved=true; if(touchTimer){clearTimeout(touchTimer);touchTimer=null;} },{passive:true});
+  boardEl.addEventListener("touchend",()=>{ if(touchTimer){clearTimeout(touchTimer);touchTimer=null;} });
+
+  // ---------- control wiring ----------
+  newBtn.addEventListener("click", ()=>{ S.click(); newGame(); });
+  faceEl.addEventListener("click", ()=>{ S.click(); newGame(); });
+  ovBtn.addEventListener("click", ()=>{ newGame(); });
+  diffSel.addEventListener("change", ()=>{ newGame(); });
+
+  flagToggle.addEventListener("click", ()=>{ setFlagMode(!flagMode_on); S.click(); });
+  flagToggle.addEventListener("keydown",(e)=>{ if(e.key===" "||e.key==="Enter"){ e.preventDefault(); setFlagMode(!flagMode_on); }});
+  hintBtn.addEventListener("click", ()=>{ giveHint(); });
+
+  muteBtn.addEventListener("click", ()=>{
+    muted=!muted; saveMute(muted);
+    muteBtn.textContent = muted?"🔇":"🔊";
+    muteBtn.classList.toggle("on",!muted);
+    if(!muted) S.click();
+  });
+
+  // keyboard: F toggles flag mode, N new game, H hint
+  document.addEventListener("keydown",(e)=>{
+    if(e.target && (e.target.tagName==="SELECT")) return;
+    const k=e.key.toLowerCase();
+    if(k==="f"){ setFlagMode(!flagMode_on); }
+    else if(k==="n"){ newGame(); }
+    else if(k==="h"){ giveHint(); }
+  });
+
+  window.addEventListener("resize", ()=>{ if(cfg) sizeBoard(); });
+  window.addEventListener("orientationchange", ()=>{ setTimeout(()=>{ if(cfg) sizeBoard(); },200); });
+
+  // ---------- init ----------
+  muted=loadMute();
+  muteBtn.textContent = muted?"🔇":"🔊";
+  muteBtn.classList.toggle("on",!muted);
+  newGame();
+})();
+</script>
+</body>
+</html>
diff --git a/games/simon/index.html b/games/simon/index.html
new file mode 100644
index 0000000..469dd39
--- /dev/null
+++ b/games/simon/index.html
@@ -0,0 +1,632 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+<title>Abrams Simon</title>
+<style>
+  :root {
+    --bg0: #05060a;
+    --bg1: #0b0f1c;
+    --ink: #e8f0ff;
+    --muted: #7b88a8;
+    --accent: #45e6ff;
+    --green: #1fd67a;
+    --red: #ff3b5c;
+    --yellow: #ffd23b;
+    --blue: #3b7bff;
+  }
+
+  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
+
+  html, body {
+    margin: 0;
+    padding: 0;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    background:
+      radial-gradient(circle at 50% 8%, #12203f 0%, var(--bg1) 45%, var(--bg0) 100%);
+    color: var(--ink);
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+    user-select: none;
+    -webkit-user-select: none;
+    touch-action: manipulation;
+  }
+
+  #app {
+    position: absolute;
+    inset: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: clamp(8px, 2.4vmin, 24px);
+    gap: clamp(8px, 2vmin, 20px);
+  }
+
+  header {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 4px;
+    text-align: center;
+  }
+
+  h1 {
+    margin: 0;
+    font-size: clamp(20px, 5vmin, 40px);
+    font-weight: 800;
+    letter-spacing: 0.18em;
+    text-transform: uppercase;
+    background: linear-gradient(90deg, var(--green), var(--yellow), var(--red), var(--blue), var(--accent));
+    -webkit-background-clip: text;
+    background-clip: text;
+    color: transparent;
+    filter: drop-shadow(0 0 12px rgba(69, 230, 255, 0.35));
+  }
+
+  .subtitle {
+    font-size: clamp(10px, 1.8vmin, 13px);
+    color: var(--muted);
+    letter-spacing: 0.24em;
+    text-transform: uppercase;
+  }
+
+  .stats {
+    display: flex;
+    gap: clamp(10px, 3vmin, 28px);
+    align-items: stretch;
+  }
+
+  .stat {
+    min-width: clamp(70px, 18vmin, 110px);
+    padding: clamp(6px, 1.4vmin, 12px) clamp(10px, 2.4vmin, 18px);
+    border-radius: 14px;
+    background: rgba(20, 30, 56, 0.55);
+    border: 1px solid rgba(90, 120, 190, 0.18);
+    box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.35);
+    text-align: center;
+  }
+  .stat .label {
+    font-size: clamp(9px, 1.5vmin, 11px);
+    letter-spacing: 0.16em;
+    text-transform: uppercase;
+    color: var(--muted);
+  }
+  .stat .value {
+    font-size: clamp(20px, 5vmin, 34px);
+    font-weight: 800;
+    line-height: 1.05;
+    color: var(--accent);
+    text-shadow: 0 0 14px rgba(69, 230, 255, 0.4);
+  }
+  .stat.best .value { color: var(--yellow); text-shadow: 0 0 14px rgba(255, 210, 59, 0.4); }
+
+  /* Board */
+  .board-wrap {
+    position: relative;
+    width: min(78vmin, 560px);
+    height: min(78vmin, 560px);
+    max-width: 90vw;
+    max-height: 60vh;
+  }
+
+  .board {
+    position: absolute;
+    inset: 0;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: 1fr 1fr;
+    gap: clamp(8px, 2.2vmin, 20px);
+    border-radius: 50%;
+    padding: clamp(8px, 2.2vmin, 20px);
+    background:
+      radial-gradient(circle at center, rgba(15, 22, 44, 0.9), rgba(5, 7, 14, 0.9));
+    box-shadow:
+      0 0 0 2px rgba(90, 120, 190, 0.15),
+      0 0 60px rgba(0, 0, 0, 0.6),
+      inset 0 0 40px rgba(0, 0, 0, 0.7);
+  }
+
+  .pad {
+    position: relative;
+    border: none;
+    cursor: pointer;
+    background: var(--c);
+    filter: brightness(0.42) saturate(0.9);
+    transition: filter 0.09s ease, box-shadow 0.09s ease, transform 0.06s ease;
+    outline: none;
+    box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.55);
+  }
+  .pad::after {
+    content: "";
+    position: absolute;
+    inset: 0;
+    border-radius: inherit;
+    box-shadow: inset 0 0 22px rgba(255, 255, 255, 0.08);
+    pointer-events: none;
+  }
+
+  .pad.tl { border-top-left-radius: 100%; --c: var(--green); }
+  .pad.tr { border-top-right-radius: 100%; --c: var(--red); }
+  .pad.bl { border-bottom-left-radius: 100%; --c: var(--yellow); }
+  .pad.br { border-bottom-right-radius: 100%; --c: var(--blue); }
+
+  .pad.lit {
+    filter: brightness(1.5) saturate(1.25);
+    box-shadow:
+      0 0 46px 8px var(--c),
+      inset 0 0 40px rgba(255, 255, 255, 0.5);
+    transform: scale(1.015);
+  }
+
+  .board.playable .pad:not(:disabled):active {
+    filter: brightness(1.35) saturate(1.2);
+  }
+
+  /* Center hub */
+  .hub {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 34%;
+    height: 34%;
+    border-radius: 50%;
+    background: radial-gradient(circle at 35% 30%, #16223f, #080b16 72%);
+    border: 2px solid rgba(90, 120, 190, 0.25);
+    box-shadow: 0 0 30px rgba(0, 0, 0, 0.7), inset 0 0 26px rgba(0, 0, 0, 0.7);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 2px;
+    z-index: 3;
+    text-align: center;
+    padding: 4px;
+  }
+  .hub .hub-title {
+    font-size: clamp(10px, 2.1vmin, 15px);
+    font-weight: 800;
+    letter-spacing: 0.16em;
+    color: var(--accent);
+    text-shadow: 0 0 12px rgba(69, 230, 255, 0.5);
+  }
+  .hub .hub-msg {
+    font-size: clamp(9px, 1.7vmin, 12px);
+    color: var(--muted);
+    letter-spacing: 0.06em;
+    min-height: 1.1em;
+  }
+
+  /* Controls */
+  .controls {
+    display: flex;
+    flex-wrap: wrap;
+    gap: clamp(8px, 2vmin, 16px);
+    align-items: center;
+    justify-content: center;
+  }
+
+  button.ctrl {
+    font-family: inherit;
+    font-weight: 700;
+    letter-spacing: 0.1em;
+    text-transform: uppercase;
+    font-size: clamp(11px, 2vmin, 14px);
+    color: var(--ink);
+    background: linear-gradient(180deg, rgba(40, 58, 100, 0.75), rgba(20, 30, 56, 0.75));
+    border: 1px solid rgba(90, 130, 210, 0.35);
+    border-radius: 12px;
+    padding: clamp(8px, 1.8vmin, 12px) clamp(16px, 4vmin, 26px);
+    cursor: pointer;
+    transition: transform 0.08s ease, box-shadow 0.15s ease, border-color 0.15s ease;
+    box-shadow: 0 0 0 rgba(69, 230, 255, 0);
+  }
+  button.ctrl:hover { border-color: var(--accent); box-shadow: 0 0 18px rgba(69, 230, 255, 0.25); }
+  button.ctrl:active { transform: translateY(1px) scale(0.98); }
+
+  button.start {
+    background: linear-gradient(180deg, var(--accent), #1b9fbf);
+    color: #041018;
+    border-color: rgba(69, 230, 255, 0.6);
+    box-shadow: 0 0 22px rgba(69, 230, 255, 0.35);
+  }
+  button.start:hover { box-shadow: 0 0 30px rgba(69, 230, 255, 0.55); }
+
+  .toggle {
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+    font-size: clamp(10px, 1.8vmin, 13px);
+    letter-spacing: 0.08em;
+    text-transform: uppercase;
+    color: var(--muted);
+    cursor: pointer;
+    padding: clamp(7px, 1.6vmin, 11px) clamp(12px, 3vmin, 18px);
+    border-radius: 12px;
+    border: 1px solid rgba(90, 130, 210, 0.22);
+    background: rgba(20, 30, 56, 0.4);
+    transition: border-color 0.15s ease, color 0.15s ease;
+  }
+  .toggle.on { color: var(--ink); border-color: rgba(69, 230, 255, 0.5); }
+  .toggle .dot {
+    width: 12px; height: 12px; border-radius: 50%;
+    background: rgba(120, 140, 180, 0.4);
+    box-shadow: inset 0 0 4px rgba(0,0,0,0.6);
+    transition: background 0.15s ease, box-shadow 0.15s ease;
+  }
+  .toggle.on .dot {
+    background: var(--green);
+    box-shadow: 0 0 10px var(--green);
+  }
+  .toggle.strict.on .dot { background: var(--red); box-shadow: 0 0 10px var(--red); }
+
+  .board.flash-fail { animation: shake 0.4s ease; }
+  @keyframes shake {
+    0%,100% { transform: translateX(0); }
+    20% { transform: translateX(-8px); }
+    40% { transform: translateX(8px); }
+    60% { transform: translateX(-5px); }
+    80% { transform: translateX(5px); }
+  }
+
+  @media (max-height: 560px) {
+    header .subtitle { display: none; }
+    .board-wrap { max-height: 52vh; }
+  }
+</style>
+</head>
+<body>
+<div id="app">
+  <header>
+    <h1>Abrams Simon</h1>
+    <div class="subtitle">Watch &middot; Remember &middot; Repeat</div>
+  </header>
+
+  <div class="stats">
+    <div class="stat"><div class="label">Round</div><div class="value" id="roundVal">0</div></div>
+    <div class="stat best"><div class="label">Best</div><div class="value" id="bestVal">0</div></div>
+  </div>
+
+  <div class="board-wrap">
+    <div class="board" id="board">
+      <button class="pad tl" data-i="0" aria-label="green" disabled></button>
+      <button class="pad tr" data-i="1" aria-label="red" disabled></button>
+      <button class="pad bl" data-i="2" aria-label="yellow" disabled></button>
+      <button class="pad br" data-i="3" aria-label="blue" disabled></button>
+    </div>
+    <div class="hub">
+      <div class="hub-title" id="hubTitle">READY?</div>
+      <div class="hub-msg" id="hubMsg">Press Start</div>
+    </div>
+  </div>
+
+  <div class="controls">
+    <button class="ctrl start" id="startBtn">Start</button>
+    <div class="toggle strict" id="strictToggle" role="switch" aria-checked="false" tabindex="0">
+      <span class="dot"></span><span>Strict</span>
+    </div>
+    <div class="toggle on" id="soundToggle" role="switch" aria-checked="true" tabindex="0">
+      <span class="dot"></span><span>Sound</span>
+    </div>
+  </div>
+</div>
+
+<script>
+(function () {
+  "use strict";
+
+  // ---- Elements ----
+  const pads = Array.from(document.querySelectorAll(".pad"));
+  const board = document.getElementById("board");
+  const roundVal = document.getElementById("roundVal");
+  const bestVal = document.getElementById("bestVal");
+  const hubTitle = document.getElementById("hubTitle");
+  const hubMsg = document.getElementById("hubMsg");
+  const startBtn = document.getElementById("startBtn");
+  const strictToggle = document.getElementById("strictToggle");
+  const soundToggle = document.getElementById("soundToggle");
+
+  // ---- State ----
+  const BEST_KEY = "abrams-simon-best";
+  const SOUND_KEY = "abrams-simon-sound";
+  const STRICT_KEY = "abrams-simon-strict";
+
+  let sequence = [];
+  let userStep = 0;
+  let acceptingInput = false;
+  let playing = false;
+  let busy = false; // during playback / transitions
+  let best = 0;
+
+  // pad tones (green, red, yellow, blue) — pleasant-ish intervals
+  const TONES = [329.63, 261.63, 220.0, 164.81]; // E4, C4, A3, E3
+  const FAIL_TONE = 110.0;
+
+  // ---- Audio ----
+  let audioCtx = null;
+  let soundOn = true;
+
+  function ensureAudio() {
+    if (!audioCtx) {
+      try {
+        const AC = window.AudioContext || window.webkitAudioContext;
+        audioCtx = new AC();
+      } catch (e) {
+        audioCtx = null;
+      }
+    }
+    if (audioCtx && audioCtx.state === "suspended") {
+      audioCtx.resume().catch(function () {});
+    }
+    return audioCtx;
+  }
+
+  function playTone(freq, durationMs, type) {
+    if (!soundOn) return;
+    const ctx = ensureAudio();
+    if (!ctx) return;
+    const now = ctx.currentTime;
+    const dur = durationMs / 1000;
+
+    const osc = ctx.createOscillator();
+    const gain = ctx.createGain();
+    osc.type = type || "sine";
+    osc.frequency.setValueAtTime(freq, now);
+
+    gain.gain.setValueAtTime(0.0001, now);
+    gain.gain.exponentialRampToValueAtTime(0.28, now + 0.012);
+    gain.gain.setValueAtTime(0.28, now + Math.max(0.012, dur - 0.06));
+    gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
+
+    osc.connect(gain).connect(ctx.destination);
+    osc.start(now);
+    osc.stop(now + dur + 0.02);
+  }
+
+  function playFail() {
+    if (!soundOn) return;
+    const ctx = ensureAudio();
+    if (!ctx) return;
+    const now = ctx.currentTime;
+    const osc = ctx.createOscillator();
+    const gain = ctx.createGain();
+    osc.type = "sawtooth";
+    osc.frequency.setValueAtTime(FAIL_TONE, now);
+    osc.frequency.exponentialRampToValueAtTime(55, now + 0.7);
+    gain.gain.setValueAtTime(0.0001, now);
+    gain.gain.exponentialRampToValueAtTime(0.3, now + 0.02);
+    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.75);
+    osc.connect(gain).connect(ctx.destination);
+    osc.start(now);
+    osc.stop(now + 0.8);
+  }
+
+  function winChime() {
+    if (!soundOn) return;
+    const ctx = ensureAudio();
+    if (!ctx) return;
+    [523.25, 659.25, 783.99].forEach(function (f, k) {
+      setTimeout(function () { playTone(f, 160, "triangle"); }, k * 90);
+    });
+  }
+
+  // ---- Timing (speeds up with level) ----
+  function litDuration() {
+    // starts ~560ms, floors ~200ms
+    return Math.max(200, 560 - sequence.length * 22);
+  }
+  function gapDuration() {
+    return Math.max(80, 200 - sequence.length * 8);
+  }
+
+  // ---- Visual pad flash ----
+  function litPad(i, duration) {
+    return new Promise(function (resolve) {
+      const pad = pads[i];
+      pad.classList.add("lit");
+      playTone(TONES[i], duration, "sine");
+      setTimeout(function () {
+        pad.classList.remove("lit");
+        resolve();
+      }, duration);
+    });
+  }
+
+  function sleep(ms) {
+    return new Promise(function (r) { setTimeout(r, ms); });
+  }
+
+  // ---- Sequence playback ----
+  async function playSequence() {
+    busy = true;
+    acceptingInput = false;
+    setBoardPlayable(false);
+    hubTitle.textContent = "WATCH";
+    hubMsg.textContent = "Round " + sequence.length;
+    await sleep(500);
+    const dur = litDuration();
+    const gap = gapDuration();
+    for (let k = 0; k < sequence.length; k++) {
+      await litPad(sequence[k], dur);
+      await sleep(gap);
+    }
+    busy = false;
+    acceptingInput = true;
+    userStep = 0;
+    setBoardPlayable(true);
+    hubTitle.textContent = "YOUR TURN";
+    hubMsg.textContent = "0 / " + sequence.length;
+  }
+
+  function setBoardPlayable(on) {
+    pads.forEach(function (p) { p.disabled = !on; });
+    board.classList.toggle("playable", on);
+  }
+
+  // ---- Round flow ----
+  function nextRound() {
+    sequence.push(Math.floor(Math.random() * 4));
+    roundVal.textContent = sequence.length;
+    playSequence();
+  }
+
+  function startGame() {
+    ensureAudio();
+    sequence = [];
+    userStep = 0;
+    playing = true;
+    acceptingInput = false;
+    roundVal.textContent = "0";
+    startBtn.textContent = "Restart";
+    board.classList.remove("flash-fail");
+    hubTitle.textContent = "GO!";
+    hubMsg.textContent = "";
+    setTimeout(nextRound, 350);
+  }
+
+  async function handlePress(i) {
+    if (!acceptingInput || busy || !playing) return;
+
+    // flash the pressed pad
+    const dur = 220;
+    acceptingInput = false;
+    await litPad(i, dur);
+
+    if (i !== sequence[userStep]) {
+      gameOver();
+      return;
+    }
+
+    userStep++;
+    hubMsg.textContent = userStep + " / " + sequence.length;
+
+    if (userStep >= sequence.length) {
+      // round cleared
+      updateBest(sequence.length);
+      winChime();
+      hubTitle.textContent = "NICE";
+      hubMsg.textContent = "Round " + sequence.length + " cleared";
+      acceptingInput = false;
+      setBoardPlayable(false);
+      await sleep(650);
+      nextRound();
+    } else {
+      acceptingInput = true;
+    }
+  }
+
+  async function gameOver() {
+    playing = false;
+    acceptingInput = false;
+    setBoardPlayable(false);
+    playFail();
+    board.classList.add("flash-fail");
+
+    // flash all pads red-ish
+    pads.forEach(function (p) { p.classList.add("lit"); });
+    await sleep(320);
+    pads.forEach(function (p) { p.classList.remove("lit"); });
+    board.classList.remove("flash-fail");
+
+    const reached = Math.max(0, sequence.length - 1);
+    hubTitle.textContent = "GAME OVER";
+    hubMsg.textContent = "Reached round " + reached;
+    startBtn.textContent = "Start";
+
+    if (strictOn) {
+      // strict: sequence resets fully (already will on Start)
+      sequence = [];
+      roundVal.textContent = "0";
+    }
+  }
+
+  function updateBest(level) {
+    if (level > best) {
+      best = level;
+      bestVal.textContent = best;
+      try { localStorage.setItem(BEST_KEY, String(best)); } catch (e) {}
+    }
+  }
+
+  // ---- Toggles ----
+  let strictOn = false;
+
+  function setSound(on) {
+    soundOn = on;
+    soundToggle.classList.toggle("on", on);
+    soundToggle.setAttribute("aria-checked", on ? "true" : "false");
+    try { localStorage.setItem(SOUND_KEY, on ? "1" : "0"); } catch (e) {}
+    if (on) ensureAudio();
+  }
+
+  function setStrict(on) {
+    strictOn = on;
+    strictToggle.classList.toggle("on", on);
+    strictToggle.setAttribute("aria-checked", on ? "true" : "false");
+    try { localStorage.setItem(STRICT_KEY, on ? "1" : "0"); } catch (e) {}
+  }
+
+  // ---- Wire events ----
+  pads.forEach(function (pad) {
+    const i = parseInt(pad.getAttribute("data-i"), 10);
+    // Pointer events cover mouse + touch; prevent double-fire.
+    pad.addEventListener("pointerdown", function (ev) {
+      ev.preventDefault();
+      handlePress(i);
+    });
+    // keyboard access
+    pad.addEventListener("keydown", function (ev) {
+      if (ev.key === "Enter" || ev.key === " ") {
+        ev.preventDefault();
+        handlePress(i);
+      }
+    });
+  });
+
+  startBtn.addEventListener("click", function () {
+    ensureAudio();
+    startGame();
+  });
+
+  soundToggle.addEventListener("click", function () { setSound(!soundOn); });
+  soundToggle.addEventListener("keydown", function (ev) {
+    if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); setSound(!soundOn); }
+  });
+
+  strictToggle.addEventListener("click", function () { setStrict(!strictOn); });
+  strictToggle.addEventListener("keydown", function (ev) {
+    if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); setStrict(!strictOn); }
+  });
+
+  // number-key shortcuts 1-4
+  window.addEventListener("keydown", function (ev) {
+    if (ev.repeat) return;
+    const map = { "1": 0, "2": 1, "3": 2, "4": 3 };
+    if (ev.key in map) { handlePress(map[ev.key]); }
+    else if (ev.key === "Enter" && !playing) { ensureAudio(); startGame(); }
+  });
+
+  // ---- Init from storage ----
+  (function init() {
+    try {
+      const b = parseInt(localStorage.getItem(BEST_KEY) || "0", 10);
+      if (!isNaN(b) && b > 0) { best = b; bestVal.textContent = b; }
+    } catch (e) {}
+
+    let s = true, st = false;
+    try {
+      const sv = localStorage.getItem(SOUND_KEY);
+      if (sv !== null) s = sv === "1";
+      const stv = localStorage.getItem(STRICT_KEY);
+      if (stv !== null) st = stv === "1";
+    } catch (e) {}
+    setSound(s);
+    setStrict(st);
+  })();
+})();
+</script>
+</body>
+</html>
diff --git a/games/tower-stack/index.html b/games/tower-stack/index.html
new file mode 100644
index 0000000..d25f198
--- /dev/null
+++ b/games/tower-stack/index.html
@@ -0,0 +1,513 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+<title>Abrams Stack</title>
+<style>
+  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
+  html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a18; }
+  #wrap {
+    position: fixed; inset: 0;
+    display: flex; align-items: center; justify-content: center;
+    background: radial-gradient(ellipse at 50% 0%, #1a1240 0%, #0a0a18 60%, #050510 100%);
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+    user-select: none;
+  }
+  canvas { display: block; touch-action: none; }
+
+  /* HUD */
+  #hud {
+    position: absolute; top: 0; left: 0; right: 0;
+    display: flex; justify-content: space-between; align-items: flex-start;
+    padding: 18px 20px; pointer-events: none; z-index: 5;
+  }
+  .stat { color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,.6); }
+  .stat .label { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; opacity: .55; }
+  .stat .val { font-size: 34px; font-weight: 800; line-height: 1; margin-top: 2px;
+    background: linear-gradient(180deg,#fff,#b9c4ff); -webkit-background-clip: text; background-clip: text; color: transparent; }
+  .stat.right { text-align: right; }
+  #combo {
+    position:absolute; top: 78px; left: 50%; transform: translateX(-50%);
+    font-size: 15px; font-weight: 700; letter-spacing: 1px; color: #ffe27a;
+    text-shadow: 0 0 18px rgba(255,210,80,.7); opacity: 0; transition: opacity .2s; pointer-events:none; z-index:5;
+  }
+
+  /* Mute */
+  #mute {
+    position: absolute; top: 16px; right: 50%; transform: translateX(50%);
+    width: 42px; height: 42px; border-radius: 12px;
+    background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.14);
+    color: #fff; font-size: 19px; cursor: pointer; z-index: 6;
+    display:flex; align-items:center; justify-content:center; backdrop-filter: blur(6px);
+    transition: background .15s, transform .1s;
+  }
+  #mute:hover { background: rgba(255,255,255,.14); }
+  #mute:active { transform: translateX(50%) scale(.9); }
+
+  /* Overlay */
+  #overlay {
+    position: absolute; inset: 0; z-index: 10;
+    display: flex; flex-direction: column; align-items: center; justify-content: center;
+    text-align: center; padding: 24px;
+    background: radial-gradient(ellipse at 50% 40%, rgba(30,20,70,.55), rgba(5,5,16,.9));
+    backdrop-filter: blur(3px);
+    transition: opacity .35s;
+  }
+  #overlay.hidden { opacity: 0; pointer-events: none; }
+  #title {
+    font-size: clamp(42px, 12vw, 92px); font-weight: 900; letter-spacing: -1px; line-height: .95;
+    background: linear-gradient(120deg,#7af0ff,#a97bff 45%,#ff7ac6 90%);
+    -webkit-background-clip: text; background-clip: text; color: transparent;
+    filter: drop-shadow(0 4px 26px rgba(160,110,255,.45));
+  }
+  #subtitle { margin-top: 14px; font-size: 15px; letter-spacing: 3px; text-transform: uppercase; color: rgba(255,255,255,.6); }
+  #big {
+    margin: 26px 0 6px; font-size: clamp(34px,9vw,60px); font-weight: 900; color:#fff;
+    text-shadow: 0 0 30px rgba(140,110,255,.6);
+  }
+  #ovBest { font-size: 14px; letter-spacing: 2px; color:#8fe0ff; text-transform: uppercase; }
+  #hint {
+    margin-top: 34px; font-size: 15px; color: rgba(255,255,255,.82);
+    padding: 14px 26px; border: 1px solid rgba(255,255,255,.16); border-radius: 50px;
+    background: rgba(255,255,255,.05); animation: pulse 1.8s ease-in-out infinite;
+  }
+  @keyframes pulse { 0%,100%{ transform: scale(1); opacity:.85 } 50%{ transform: scale(1.05); opacity:1 } }
+  #credit { position:absolute; bottom: 16px; font-size: 11px; letter-spacing: 2px; color: rgba(255,255,255,.28); text-transform: uppercase; }
+</style>
+</head>
+<body>
+<div id="wrap">
+  <canvas id="c"></canvas>
+
+  <div id="hud">
+    <div class="stat">
+      <div class="label">Height</div>
+      <div class="val" id="score">0</div>
+    </div>
+    <div class="stat right">
+      <div class="label">Best</div>
+      <div class="val" id="best">0</div>
+    </div>
+  </div>
+  <div id="combo"></div>
+  <button id="mute" aria-label="Toggle sound">🔊</button>
+
+  <div id="overlay">
+    <div id="title">ABRAMS<br>STACK</div>
+    <div id="subtitle">stack it high</div>
+    <div id="big" style="display:none"></div>
+    <div id="ovBest"></div>
+    <div id="hint">Tap / Click / Space to drop</div>
+  </div>
+  <div id="credit">Pure Canvas 2D · Zero Dependencies</div>
+</div>
+
+<script>
+(function () {
+  "use strict";
+
+  var canvas = document.getElementById("c");
+  var ctx = canvas.getContext("2d");
+  var scoreEl = document.getElementById("score");
+  var bestEl = document.getElementById("best");
+  var comboEl = document.getElementById("combo");
+  var overlay = document.getElementById("overlay");
+  var titleEl = document.getElementById("title");
+  var subtitleEl = document.getElementById("subtitle");
+  var bigEl = document.getElementById("big");
+  var ovBestEl = document.getElementById("ovBest");
+  var hintEl = document.getElementById("hint");
+  var muteBtn = document.getElementById("mute");
+
+  // ---------- Sizing (DPR aware, iframe responsive) ----------
+  var W = 0, H = 0, DPR = 1;
+  function resize() {
+    DPR = Math.min(window.devicePixelRatio || 1, 2);
+    W = window.innerWidth;
+    H = window.innerHeight;
+    canvas.width = Math.floor(W * DPR);
+    canvas.height = Math.floor(H * DPR);
+    canvas.style.width = W + "px";
+    canvas.style.height = H + "px";
+    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
+  }
+  window.addEventListener("resize", resize);
+  resize();
+
+  // ---------- Persistent best ----------
+  var best = 0;
+  try { best = parseInt(localStorage.getItem("abramsStackBest") || "0", 10) || 0; } catch (e) {}
+  bestEl.textContent = best;
+
+  // ---------- Audio (procedural, optional) ----------
+  var muted = false;
+  try { muted = localStorage.getItem("abramsStackMuted") === "1"; } catch (e) {}
+  muteBtn.textContent = muted ? "🔇" : "🔊";
+  var actx = null;
+  function ensureAudio() {
+    if (muted) return;
+    if (!actx) {
+      try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { actx = null; }
+    }
+    if (actx && actx.state === "suspended") actx.resume();
+  }
+  function tone(freq, dur, type, vol, glideTo) {
+    if (muted || !actx) return;
+    var t = actx.currentTime;
+    var o = actx.createOscillator();
+    var g = actx.createGain();
+    o.type = type || "sine";
+    o.frequency.setValueAtTime(freq, t);
+    if (glideTo) o.frequency.exponentialRampToValueAtTime(glideTo, t + dur);
+    g.gain.setValueAtTime(0.0001, t);
+    g.gain.exponentialRampToValueAtTime(vol || 0.2, t + 0.008);
+    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
+    o.connect(g); g.connect(actx.destination);
+    o.start(t); o.stop(t + dur + 0.02);
+  }
+  function sndDrop() { tone(220, 0.14, "sine", 0.22, 150); }
+  function sndSlice() { tone(140, 0.12, "sawtooth", 0.14, 90); }
+  function sndPerfect(n) {
+    if (muted || !actx) return;
+    var base = 520 + Math.min(n, 8) * 40;
+    tone(base, 0.14, "triangle", 0.22);
+    setTimeout(function () { tone(base * 1.5, 0.16, "triangle", 0.18); }, 70);
+  }
+  function sndOver() {
+    tone(300, 0.5, "sawtooth", 0.2, 70);
+    setTimeout(function () { tone(160, 0.5, "sine", 0.15, 60); }, 90);
+  }
+  muteBtn.addEventListener("click", function (e) {
+    e.stopPropagation();
+    muted = !muted;
+    muteBtn.textContent = muted ? "🔇" : "🔊";
+    try { localStorage.setItem("abramsStackMuted", muted ? "1" : "0"); } catch (er) {}
+    if (!muted) ensureAudio();
+  });
+
+  // ---------- Game state ----------
+  var STATE_START = 0, STATE_PLAY = 1, STATE_OVER = 2;
+  var state = STATE_START;
+
+  var BLOCK_H = 46;        // logical pixel height of each block
+  var baseWidth = 0;       // starting block width
+  var blocks = [];         // placed blocks {x, w, hue}
+  var current = null;      // moving block {x, w, dir, hue}
+  var speed = 0;
+  var camY = 0;            // camera vertical offset (target)
+  var camYCur = 0;         // smoothed
+  var score = 0;
+  var combo = 0;
+  var particles = [];
+  var slices = [];         // falling sliced-off pieces
+  var flash = 0;           // perfect flash intensity
+
+  function hueForLevel(n) { return (200 + n * 9) % 360; }
+
+  function groundY() { return H - 90; } // y of top of base row in world space (before camera)
+
+  function reset() {
+    blocks = [];
+    slices = [];
+    particles = [];
+    score = 0;
+    combo = 0;
+    camY = 0; camYCur = 0;
+    flash = 0;
+    baseWidth = Math.min(W * 0.5, 300);
+    speed = Math.max(2.6, W / 300);
+    var bx = (W - baseWidth) / 2;
+    blocks.push({ x: bx, w: baseWidth, hue: hueForLevel(0) });
+    spawnCurrent();
+  }
+
+  function spawnCurrent() {
+    var prev = blocks[blocks.length - 1];
+    var fromLeft = blocks.length % 2 === 0;
+    var startX = fromLeft ? -prev.w : W;
+    current = {
+      x: fromLeft ? -prev.w * 0.6 : W - prev.w * 0.4,
+      w: prev.w,
+      dir: fromLeft ? 1 : -1,
+      hue: hueForLevel(blocks.length)
+    };
+    // speed scales with height
+    speed = Math.max(2.6, W / 300) * (1 + blocks.length * 0.035);
+  }
+
+  // world Y (bottom-up) for the top of a block at index i (i=0 is base)
+  function blockWorldY(i) { return groundY() - i * BLOCK_H; }
+
+  function showCombo(txt) {
+    comboEl.textContent = txt;
+    comboEl.style.opacity = "1";
+    clearTimeout(showCombo._t);
+    showCombo._t = setTimeout(function () { comboEl.style.opacity = "0"; }, 800);
+  }
+
+  function drop() {
+    if (state !== STATE_PLAY) return;
+    var prev = blocks[blocks.length - 1];
+    var cx = current.x, cw = current.w;
+    var overlapL = Math.max(cx, prev.x);
+    var overlapR = Math.min(cx + cw, prev.x + prev.w);
+    var overlap = overlapR - overlapL;
+
+    if (overlap <= 0) {
+      // total miss -> game over. add the whole falling block as a slice
+      slices.push({ x: cx, w: cw, y: blockWorldY(blocks.length - 1) - BLOCK_H, hue: current.hue, vy: 0, vx: current.dir * 1.5, rot: 0, vr: current.dir * 0.05 });
+      gameOver();
+      return;
+    }
+
+    // perfect detection
+    var diff = Math.abs(cx - prev.x);
+    var perfect = diff < 4;
+
+    if (perfect) {
+      combo++;
+      flash = 1;
+      sndPerfect(combo);
+      // snap to perfect alignment (no shrink), small bonus width back on combo
+      cx = prev.x;
+      showCombo(combo > 1 ? "PERFECT ×" + combo + "  +" + (combo) : "PERFECT!");
+      spawnParticles(cx + cw / 2, blockWorldY(blocks.length - 1) - BLOCK_H / 2, current.hue, 22, true);
+    } else {
+      combo = 0;
+      sndDrop();
+      // slice off the overhang
+      if (cx < prev.x) {
+        // overhang on the left
+        var offW = prev.x - cx;
+        slices.push({ x: cx, w: offW, y: blockWorldY(blocks.length - 1) - BLOCK_H, hue: current.hue, vy: 0, vx: -1.4, rot: 0, vr: -0.06 });
+      } else {
+        var offW2 = (cx + cw) - (prev.x + prev.w);
+        if (offW2 > 0) slices.push({ x: prev.x + prev.w, w: offW2, y: blockWorldY(blocks.length - 1) - BLOCK_H, hue: current.hue, vy: 0, vx: 1.4, rot: 0, vr: 0.06 });
+      }
+      sndSlice();
+      cx = overlapL;
+      cw = overlap;
+      spawnParticles(overlapL + overlap / 2, blockWorldY(blocks.length - 1) - BLOCK_H / 2, current.hue, 8, false);
+    }
+
+    blocks.push({ x: cx, w: cw, hue: current.hue });
+    score++;
+    scoreEl.textContent = score;
+
+    // camera pans up as tower grows (keep top few visible)
+    var visibleRows = Math.max(4, Math.floor((H - 200) / BLOCK_H));
+    if (blocks.length > visibleRows) {
+      camY = (blocks.length - visibleRows) * BLOCK_H;
+    }
+
+    spawnCurrent();
+  }
+
+  function spawnParticles(x, worldY, hue, count, sparkle) {
+    for (var i = 0; i < count; i++) {
+      var a = Math.random() * Math.PI * 2;
+      var sp = (sparkle ? 2 + Math.random() * 4 : 1 + Math.random() * 2.5);
+      particles.push({
+        x: x, wy: worldY,
+        vx: Math.cos(a) * sp,
+        vy: Math.sin(a) * sp - (sparkle ? 1.5 : 0.5),
+        life: 1, hue: hue, size: 2 + Math.random() * (sparkle ? 4 : 2)
+      });
+    }
+  }
+
+  function gameOver() {
+    state = STATE_OVER;
+    sndOver();
+    if (score > best) {
+      best = score;
+      try { localStorage.setItem("abramsStackBest", String(best)); } catch (e) {}
+      bestEl.textContent = best;
+    }
+    titleEl.style.display = "none";
+    subtitleEl.style.display = "none";
+    bigEl.style.display = "block";
+    bigEl.textContent = "Height  " + score;
+    ovBestEl.textContent = (score >= best && score > 0 ? "★ NEW BEST ★" : "Best  " + best);
+    hintEl.textContent = "Tap / Click / Space to retry";
+    overlay.classList.remove("hidden");
+  }
+
+  function startGame() {
+    ensureAudio();
+    overlay.classList.add("hidden");
+    titleEl.style.display = "";
+    subtitleEl.style.display = "";
+    bigEl.style.display = "none";
+    reset();
+    state = STATE_PLAY;
+  }
+
+  // ---------- Input ----------
+  function onAction(e) {
+    if (e && e.target === muteBtn) return;
+    if (e) e.preventDefault();
+    ensureAudio();
+    if (state === STATE_PLAY) {
+      drop();
+    } else {
+      startGame();
+    }
+  }
+  window.addEventListener("pointerdown", onAction, { passive: false });
+  window.addEventListener("keydown", function (e) {
+    if (e.code === "Space" || e.key === " " || e.code === "Enter") {
+      e.preventDefault();
+      onAction();
+    }
+  });
+
+  // ---------- Render helpers ----------
+  function roundRect(x, y, w, h, r) {
+    r = Math.min(r, w / 2, h / 2);
+    ctx.beginPath();
+    ctx.moveTo(x + r, y);
+    ctx.arcTo(x + w, y, x + w, y + h, r);
+    ctx.arcTo(x + w, y + h, x, y + h, r);
+    ctx.arcTo(x, y + h, x, y, r);
+    ctx.arcTo(x, y, x + w, y, r);
+    ctx.closePath();
+  }
+
+  function drawBlock(x, screenY, w, hue, glow) {
+    var top = "hsl(" + hue + ",78%,64%)";
+    var bot = "hsl(" + hue + ",70%,44%)";
+    var grad = ctx.createLinearGradient(0, screenY, 0, screenY + BLOCK_H);
+    grad.addColorStop(0, top);
+    grad.addColorStop(1, bot);
+    if (glow) {
+      ctx.save();
+      ctx.shadowColor = "hsla(" + hue + ",90%,65%,0.9)";
+      ctx.shadowBlur = 26;
+    }
+    ctx.fillStyle = grad;
+    roundRect(x, screenY, w, BLOCK_H - 3, 7);
+    ctx.fill();
+    if (glow) ctx.restore();
+    // top highlight
+    ctx.fillStyle = "hsla(" + hue + ",90%,80%,0.5)";
+    roundRect(x + 3, screenY + 2, w - 6, 5, 3);
+    ctx.fill();
+  }
+
+  // ---------- Loop ----------
+  var lastT = performance.now();
+  function frame(now) {
+    var dt = Math.min((now - lastT) / 16.6667, 3); // normalized to 60fps
+    lastT = now;
+
+    // smooth camera
+    camYCur += (camY - camYCur) * Math.min(1, 0.12 * dt);
+    if (flash > 0) flash = Math.max(0, flash - 0.05 * dt);
+
+    // update moving block
+    if (state === STATE_PLAY && current) {
+      current.x += current.dir * speed * dt;
+      var prev = blocks[blocks.length - 1];
+      // bounce within a range slightly beyond the tower so it fully crosses
+      var leftLimit = -current.w * 0.9;
+      var rightLimit = W - current.w * 0.1;
+      if (current.x < leftLimit) { current.x = leftLimit; current.dir = 1; }
+      if (current.x > rightLimit) { current.x = rightLimit; current.dir = -1; }
+    }
+
+    // update particles
+    for (var i = particles.length - 1; i >= 0; i--) {
+      var p = particles[i];
+      p.x += p.vx * dt;
+      p.wy += p.vy * dt;
+      p.vy += 0.18 * dt;
+      p.life -= 0.02 * dt;
+      if (p.life <= 0) particles.splice(i, 1);
+    }
+    // update slices (falling debris)
+    for (var s = slices.length - 1; s >= 0; s--) {
+      var sl = slices[s];
+      sl.vy += 0.55 * dt;
+      sl.y += sl.vy * dt;
+      sl.x += sl.vx * dt;
+      sl.rot += sl.vr * dt;
+      if (sl.y - camYCur > H + 200) slices.splice(s, 1);
+    }
+
+    draw();
+    requestAnimationFrame(frame);
+  }
+
+  function draw() {
+    ctx.clearRect(0, 0, W, H);
+
+    // subtle drifting bg glow tied to height
+    var topHue = hueForLevel(blocks.length);
+    var g = ctx.createRadialGradient(W / 2, H * 0.2, 40, W / 2, H * 0.2, Math.max(W, H));
+    g.addColorStop(0, "hsla(" + topHue + ",55%,22%,0.55)");
+    g.addColorStop(1, "rgba(5,5,16,0)");
+    ctx.fillStyle = g;
+    ctx.fillRect(0, 0, W, H);
+
+    function worldToScreen(wy) { return wy - camYCur; }
+
+    // placed blocks
+    for (var i = 0; i < blocks.length; i++) {
+      var b = blocks[i];
+      var wy = blockWorldY(i);            // world y of top of this block
+      var sy = worldToScreen(wy) - BLOCK_H;
+      if (sy > H + BLOCK_H || sy < -BLOCK_H) continue;
+      drawBlock(b.x, sy, b.w, b.hue, i === blocks.length - 1 && state === STATE_PLAY ? false : false);
+    }
+
+    // moving block
+    if (state === STATE_PLAY && current) {
+      var wyc = blockWorldY(blocks.length);
+      var syc = worldToScreen(wyc) - BLOCK_H;
+      drawBlock(current.x, syc, current.w, current.hue, true);
+    }
+
+    // slices (debris)
+    for (var s = 0; s < slices.length; s++) {
+      var sl = slices[s];
+      var sy2 = worldToScreen(sl.y);
+      ctx.save();
+      ctx.translate(sl.x + sl.w / 2, sy2 + BLOCK_H / 2);
+      ctx.rotate(sl.rot);
+      ctx.globalAlpha = 0.9;
+      var gr = ctx.createLinearGradient(0, -BLOCK_H / 2, 0, BLOCK_H / 2);
+      gr.addColorStop(0, "hsl(" + sl.hue + ",78%,60%)");
+      gr.addColorStop(1, "hsl(" + sl.hue + ",70%,42%)");
+      ctx.fillStyle = gr;
+      roundRect(-sl.w / 2, -BLOCK_H / 2, sl.w, BLOCK_H - 3, 6);
+      ctx.fill();
+      ctx.restore();
+    }
+
+    // particles
+    ctx.globalAlpha = 1;
+    for (var p = 0; p < particles.length; p++) {
+      var pa = particles[p];
+      ctx.globalAlpha = Math.max(0, pa.life);
+      ctx.fillStyle = "hsl(" + pa.hue + ",90%,68%)";
+      var py = pa.wy - camYCur;
+      ctx.beginPath();
+      ctx.arc(pa.x, py, pa.size, 0, Math.PI * 2);
+      ctx.fill();
+    }
+    ctx.globalAlpha = 1;
+
+    // perfect flash overlay
+    if (flash > 0) {
+      ctx.fillStyle = "rgba(255,255,255," + (flash * 0.18) + ")";
+      ctx.fillRect(0, 0, W, H);
+    }
+  }
+
+  requestAnimationFrame(frame);
+})();
+</script>
+</body>
+</html>
diff --git a/games/whack-a-mole/index.html b/games/whack-a-mole/index.html
new file mode 100644
index 0000000..3d0e27f
--- /dev/null
+++ b/games/whack-a-mole/index.html
@@ -0,0 +1,728 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+<title>Abrams Whack</title>
+<style>
+  :root{
+    --bg0:#080012;
+    --bg1:#12002a;
+    --neon:#00f0ff;
+    --neon2:#ff2bd6;
+    --neon3:#b14bff;
+    --good:#33ff9e;
+    --bad:#ff3b5c;
+    --gold:#ffd23f;
+    --ink:#eafcff;
+  }
+  *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
+  html,body{height:100%;width:100%;overflow:hidden;}
+  body{
+    font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;
+    background:
+      radial-gradient(circle at 20% 10%, rgba(177,75,255,.28), transparent 45%),
+      radial-gradient(circle at 85% 90%, rgba(0,240,255,.24), transparent 45%),
+      linear-gradient(160deg,var(--bg1),var(--bg0));
+    color:var(--ink);
+    user-select:none;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+  }
+  #app{
+    position:relative;
+    width:100%;
+    height:100%;
+    max-width:900px;
+    max-height:900px;
+    margin:auto;
+    display:flex;
+    flex-direction:column;
+    padding:clamp(8px,2vmin,20px);
+  }
+
+  /* HUD */
+  #hud{
+    display:flex;
+    align-items:center;
+    justify-content:space-between;
+    gap:clamp(6px,1.5vmin,14px);
+    flex-wrap:wrap;
+    padding:clamp(6px,1.4vmin,12px) clamp(8px,2vmin,16px);
+    border-radius:16px;
+    background:rgba(10,0,25,.45);
+    border:1px solid rgba(0,240,255,.25);
+    box-shadow:0 0 24px rgba(0,240,255,.12) inset;
+    backdrop-filter:blur(4px);
+  }
+  .stat{
+    display:flex;
+    flex-direction:column;
+    align-items:center;
+    line-height:1.05;
+    min-width:0;
+  }
+  .stat .label{
+    font-size:clamp(8px,1.6vmin,11px);
+    letter-spacing:.14em;
+    text-transform:uppercase;
+    opacity:.7;
+  }
+  .stat .val{
+    font-size:clamp(16px,4vmin,30px);
+    font-weight:800;
+    font-variant-numeric:tabular-nums;
+    text-shadow:0 0 10px currentColor;
+  }
+  #score .val{color:var(--neon);}
+  #best .val{color:var(--gold);}
+  #time .val{color:var(--good);}
+  #combo .val{color:var(--neon2);}
+  #time.low .val{color:var(--bad); animation:pulse .6s infinite;}
+  @keyframes pulse{50%{opacity:.35;}}
+
+  #lives{display:flex;gap:4px;font-size:clamp(14px,3vmin,22px);}
+
+  #muteBtn{
+    cursor:pointer;
+    border:1px solid rgba(255,255,255,.3);
+    background:rgba(255,255,255,.06);
+    color:var(--ink);
+    border-radius:10px;
+    padding:clamp(4px,1vmin,8px) clamp(6px,1.4vmin,10px);
+    font-size:clamp(12px,2.6vmin,18px);
+    line-height:1;
+    transition:.15s;
+  }
+  #muteBtn:hover{background:rgba(0,240,255,.15);border-color:var(--neon);}
+
+  #title{
+    text-align:center;
+    margin:clamp(4px,1.2vmin,10px) 0;
+    font-size:clamp(18px,5vmin,40px);
+    font-weight:900;
+    letter-spacing:.06em;
+    background:linear-gradient(90deg,var(--neon),var(--neon2),var(--neon3),var(--neon));
+    background-size:300% 100%;
+    -webkit-background-clip:text;
+    background-clip:text;
+    color:transparent;
+    animation:shift 6s linear infinite;
+    filter:drop-shadow(0 0 12px rgba(255,43,214,.4));
+  }
+  @keyframes shift{to{background-position:300% 0;}}
+
+  /* Board */
+  #boardWrap{
+    flex:1;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+    min-height:0;
+  }
+  #board{
+    display:grid;
+    grid-template-columns:repeat(3,1fr);
+    grid-template-rows:repeat(3,1fr);
+    gap:clamp(6px,2vmin,18px);
+    width:min(100%,min(72vh,560px));
+    aspect-ratio:1/1;
+  }
+  .hole{
+    position:relative;
+    border-radius:50%;
+    background:
+      radial-gradient(circle at 50% 38%, #1a0033 0%, #0c0018 60%, #05000d 100%);
+    box-shadow:
+      0 0 0 2px rgba(0,240,255,.18),
+      inset 0 10px 26px rgba(0,0,0,.9),
+      inset 0 -6px 14px rgba(177,75,255,.18);
+    overflow:hidden;
+    cursor:pointer;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+    transition:box-shadow .12s;
+  }
+  .hole::after{ /* rim glow */
+    content:"";
+    position:absolute;
+    inset:0;
+    border-radius:50%;
+    box-shadow:inset 0 6px 18px rgba(0,240,255,.10);
+    pointer-events:none;
+  }
+  .hole.hit{
+    box-shadow:
+      0 0 24px 6px rgba(51,255,158,.6),
+      inset 0 10px 26px rgba(0,0,0,.9);
+  }
+  .hole.badhit{
+    box-shadow:
+      0 0 24px 6px rgba(255,59,92,.7),
+      inset 0 10px 26px rgba(0,0,0,.9);
+  }
+
+  /* the pop target */
+  .target{
+    position:absolute;
+    left:50%;
+    bottom:-72%;
+    transform:translateX(-50%);
+    width:78%;
+    height:78%;
+    border-radius:50% 50% 46% 46%;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+    font-size:clamp(22px,9vmin,64px);
+    transition:bottom .13s cubic-bezier(.2,.9,.3,1.4), transform .13s;
+    pointer-events:none;
+    will-change:bottom;
+  }
+  .target.up{ bottom:8%; }
+  .target.mole{
+    background:radial-gradient(circle at 50% 35%, var(--neon3), #4a007a 70%, #22003a);
+    box-shadow:0 0 18px rgba(177,75,255,.7), inset 0 -8px 12px rgba(0,0,0,.5), inset 0 6px 10px rgba(255,255,255,.15);
+    border:2px solid rgba(0,240,255,.5);
+  }
+  .target.bomb{
+    background:radial-gradient(circle at 50% 35%, #ff5a78, #b3001d 72%, #4a000c);
+    box-shadow:0 0 20px rgba(255,59,92,.85), inset 0 -8px 12px rgba(0,0,0,.5);
+    border:2px solid rgba(255,210,63,.7);
+  }
+  .target.gold{
+    background:radial-gradient(circle at 50% 35%, #fff2a8, var(--gold) 68%, #b98900);
+    box-shadow:0 0 26px rgba(255,210,63,.9), inset 0 -8px 12px rgba(0,0,0,.35);
+    border:2px solid #fff;
+  }
+  .target.whacked{ transform:translateX(-50%) scale(.55) rotate(8deg); opacity:0; transition:transform .18s, opacity .18s; }
+
+  /* floating score popup */
+  .pop{
+    position:absolute;
+    left:50%;
+    top:30%;
+    transform:translate(-50%,-50%);
+    font-weight:900;
+    font-size:clamp(14px,4.5vmin,30px);
+    pointer-events:none;
+    text-shadow:0 0 8px currentColor;
+    animation:floatUp .7s ease-out forwards;
+    z-index:5;
+  }
+  @keyframes floatUp{
+    0%{opacity:0; transform:translate(-50%,-30%) scale(.6);}
+    20%{opacity:1;}
+    100%{opacity:0; transform:translate(-50%,-160%) scale(1.1);}
+  }
+
+  /* ripple on whack */
+  .ripple{
+    position:absolute;
+    inset:0;
+    border-radius:50%;
+    pointer-events:none;
+    animation:rip .45s ease-out forwards;
+  }
+  @keyframes rip{
+    from{box-shadow:0 0 0 0 rgba(255,255,255,.6);}
+    to{box-shadow:0 0 0 40px rgba(255,255,255,0);}
+  }
+
+  /* Overlay */
+  #overlay{
+    position:absolute;
+    inset:0;
+    display:flex;
+    align-items:center;
+    justify-content:center;
+    background:rgba(4,0,14,.82);
+    backdrop-filter:blur(6px);
+    z-index:20;
+    text-align:center;
+    padding:20px;
+  }
+  #overlay.hidden{display:none;}
+  .panel{
+    max-width:440px;
+    width:100%;
+    background:linear-gradient(160deg, rgba(28,0,54,.9), rgba(8,0,20,.9));
+    border:1px solid rgba(0,240,255,.4);
+    border-radius:22px;
+    padding:clamp(20px,4vmin,36px);
+    box-shadow:0 0 40px rgba(177,75,255,.35), inset 0 0 30px rgba(0,240,255,.08);
+  }
+  .panel h1{
+    font-size:clamp(26px,7vmin,46px);
+    font-weight:900;
+    letter-spacing:.04em;
+    background:linear-gradient(90deg,var(--neon),var(--neon2));
+    -webkit-background-clip:text;background-clip:text;color:transparent;
+    filter:drop-shadow(0 0 10px rgba(0,240,255,.5));
+    margin-bottom:8px;
+  }
+  .panel p{opacity:.85; font-size:clamp(12px,2.6vmin,15px); line-height:1.5; margin-bottom:16px;}
+  .legend{
+    display:flex;justify-content:center;gap:16px;flex-wrap:wrap;
+    margin-bottom:18px;font-size:clamp(11px,2.4vmin,14px);
+  }
+  .legend span{display:inline-flex;align-items:center;gap:6px;}
+  .dot{width:16px;height:16px;border-radius:50%;display:inline-block;box-shadow:0 0 8px currentColor;}
+  .dot.m{background:var(--neon3);color:var(--neon3);}
+  .dot.b{background:var(--bad);color:var(--bad);}
+  .dot.g{background:var(--gold);color:var(--gold);}
+  #finalScore{
+    font-size:clamp(28px,8vmin,52px);
+    font-weight:900;
+    color:var(--neon);
+    text-shadow:0 0 16px var(--neon);
+    margin:6px 0;
+    font-variant-numeric:tabular-nums;
+  }
+  #finalBest{color:var(--gold);font-weight:700;margin-bottom:16px;}
+  #newBest{
+    color:var(--gold);font-weight:900;letter-spacing:.1em;
+    animation:pulse .8s infinite; margin-bottom:8px; font-size:clamp(13px,3vmin,18px);
+  }
+  #newBest.hidden{display:none;}
+  .btn{
+    cursor:pointer;
+    border:none;
+    border-radius:14px;
+    padding:clamp(12px,2.6vmin,16px) clamp(24px,5vmin,40px);
+    font-size:clamp(15px,3.4vmin,22px);
+    font-weight:800;
+    letter-spacing:.05em;
+    color:#08001a;
+    background:linear-gradient(90deg,var(--neon),var(--neon2));
+    box-shadow:0 6px 24px rgba(255,43,214,.4);
+    transition:transform .1s, box-shadow .15s;
+  }
+  .btn:hover{transform:translateY(-2px); box-shadow:0 10px 30px rgba(0,240,255,.5);}
+  .btn:active{transform:translateY(1px) scale(.98);}
+
+  /* screen shake */
+  @keyframes shake{
+    0%,100%{transform:translate(0,0);}
+    25%{transform:translate(-4px,2px);}
+    50%{transform:translate(4px,-2px);}
+    75%{transform:translate(-3px,-1px);}
+  }
+  #app.shake{animation:shake .28s;}
+</style>
+</head>
+<body>
+<div id="app">
+  <div id="hud">
+    <div id="score" class="stat"><span class="label">Score</span><span class="val">0</span></div>
+    <div id="combo" class="stat"><span class="label">Combo</span><span class="val">x1</span></div>
+    <div id="title" style="flex:1;">ABRAMS WHACK</div>
+    <div id="time" class="stat"><span class="label">Time</span><span class="val">45</span></div>
+    <div id="best" class="stat"><span class="label">Best</span><span class="val">0</span></div>
+    <div id="livesWrap" class="stat"><span class="label">Lives</span><span id="lives" class="val"></span></div>
+    <button id="muteBtn" title="Mute / unmute">🔊</button>
+  </div>
+
+  <div id="boardWrap">
+    <div id="board"></div>
+  </div>
+
+  <div id="overlay">
+    <div class="panel">
+      <h1 id="ovTitle">ABRAMS WHACK</h1>
+      <div id="newBest" class="hidden">★ NEW HIGH SCORE ★</div>
+      <div id="finalScore" style="display:none;">0</div>
+      <div id="finalBest" style="display:none;"></div>
+      <p id="ovDesc">Whack the neon moles before they vanish. Hit fast for combo multipliers.
+        Dodge the red bombs — they cost a life. Grab the gold ones for big points.</p>
+      <div class="legend">
+        <span><i class="dot m"></i>Mole +pts</span>
+        <span><i class="dot g"></i>Gold jackpot</span>
+        <span><i class="dot b"></i>Bomb −life</span>
+      </div>
+      <button id="startBtn" class="btn">START</button>
+    </div>
+  </div>
+</div>
+
+<script>
+(function(){
+  "use strict";
+
+  // ---------- Config ----------
+  const ROUND_TIME = 45;          // seconds
+  const HOLES = 9;                 // 3x3
+  const START_LIVES = 3;
+  const BASE_POINTS = 10;
+  const GOLD_POINTS = 50;
+  const MISS_PENALTY = 5;          // missed mole
+  const BOMB_PENALTY = 15;         // whacking a bomb
+  const COMBO_WINDOW = 1400;       // ms to keep combo alive between hits
+  const MAX_COMBO = 8;
+
+  // ---------- State ----------
+  let board, holes = [];
+  let score = 0, best = 0, timeLeft = ROUND_TIME, lives = START_LIVES;
+  let combo = 0, lastHitAt = 0;
+  let running = false;
+  let spawnTimer = null, tickTimer = null;
+  let elapsed = 0;                 // seconds into round (for difficulty ramp)
+  let muted = false;
+
+  // per-hole occupancy: {kind, el, timeout, whacked}
+  const occ = new Array(HOLES).fill(null);
+
+  // ---------- DOM ----------
+  const $ = s => document.querySelector(s);
+  const app = $("#app");
+  const scoreVal = $("#score .val");
+  const comboVal = $("#combo .val");
+  const timeStat = $("#time");
+  const timeVal = $("#time .val");
+  const bestVal = $("#best .val");
+  const livesEl = $("#lives");
+  const overlay = $("#overlay");
+  const ovTitle = $("#ovTitle");
+  const ovDesc = $("#ovDesc");
+  const finalScore = $("#finalScore");
+  const finalBest = $("#finalBest");
+  const newBest = $("#newBest");
+  const startBtn = $("#startBtn");
+  const muteBtn = $("#muteBtn");
+
+  // ---------- Persistence ----------
+  const LS_KEY = "abramsWhack.best";
+  try { best = parseInt(localStorage.getItem(LS_KEY) || "0", 10) || 0; } catch(e){ best = 0; }
+  try { muted = localStorage.getItem("abramsWhack.muted") === "1"; } catch(e){}
+  bestVal.textContent = best;
+  muteBtn.textContent = muted ? "🔇" : "🔊";
+
+  // ---------- Audio (procedural Web Audio) ----------
+  let actx = null;
+  function audio(){
+    if (muted) return null;
+    if (!actx){
+      try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
+      catch(e){ return null; }
+    }
+    if (actx.state === "suspended") actx.resume();
+    return actx;
+  }
+  function tone(freq, dur, type, gain, slideTo){
+    const ac = audio(); if(!ac) return;
+    const o = ac.createOscillator();
+    const g = ac.createGain();
+    o.type = type || "sine";
+    o.frequency.setValueAtTime(freq, ac.currentTime);
+    if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, ac.currentTime + dur);
+    g.gain.setValueAtTime(0.0001, ac.currentTime);
+    g.gain.exponentialRampToValueAtTime(gain || 0.2, ac.currentTime + 0.01);
+    g.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + dur);
+    o.connect(g); g.connect(ac.destination);
+    o.start(); o.stop(ac.currentTime + dur + 0.02);
+  }
+  function noiseBurst(dur, gain){
+    const ac = audio(); if(!ac) return;
+    const n = Math.floor(ac.sampleRate * dur);
+    const buf = ac.createBuffer(1, n, ac.sampleRate);
+    const data = buf.getChannelData(0);
+    for (let i=0;i<n;i++) data[i] = (Math.random()*2-1) * (1 - i/n);
+    const src = ac.createBufferSource(); src.buffer = buf;
+    const g = ac.createGain(); g.gain.value = gain || 0.25;
+    const f = ac.createBiquadFilter(); f.type="highpass"; f.frequency.value=800;
+    src.connect(f); f.connect(g); g.connect(ac.destination);
+    src.start();
+  }
+  const sfx = {
+    whack(c){ tone(420 + Math.min(c,MAX_COMBO)*55, 0.12, "square", 0.22, 180); },
+    gold(){ tone(660,0.1,"triangle",0.25,1320); setTimeout(()=>tone(990,0.14,"triangle",0.22,1760),70); },
+    bomb(){ noiseBurst(0.28,0.35); tone(120,0.3,"sawtooth",0.3,40); },
+    miss(){ tone(200,0.14,"sine",0.15,120); },
+    pop(){ tone(300,0.06,"sine",0.08,500); },
+    over(){ tone(440,0.2,"triangle",0.2,110); setTimeout(()=>tone(220,0.4,"triangle",0.2,80),160); },
+    start(){ tone(330,0.1,"triangle",0.2,660); setTimeout(()=>tone(660,0.14,"triangle",0.2,990),100); }
+  };
+
+  // ---------- Build board ----------
+  function buildBoard(){
+    board = $("#board");
+    board.innerHTML = "";
+    holes = [];
+    for (let i=0;i<HOLES;i++){
+      const h = document.createElement("div");
+      h.className = "hole";
+      h.dataset.i = i;
+      h.addEventListener("pointerdown", onHolePress, {passive:false});
+      board.appendChild(h);
+      holes.push(h);
+    }
+  }
+
+  // ---------- Difficulty ramp ----------
+  // returns {upTime, gap} in ms. As elapsed grows, moles stay up shorter & spawn faster.
+  function difficulty(){
+    const t = elapsed / ROUND_TIME;               // 0..1
+    const upTime = Math.max(520, 1150 - t*760);   // 1150 -> 520 ms visible
+    const gap = Math.max(340, 900 - t*620);       // spawn interval
+    return { upTime, gap };
+  }
+
+  function chooseKind(){
+    // gold rare, bomb scales up a bit with difficulty
+    const t = elapsed / ROUND_TIME;
+    const r = Math.random();
+    if (r < 0.06) return "gold";
+    if (r < 0.06 + 0.14 + t*0.12) return "bomb";  // 14% -> ~26%
+    return "mole";
+  }
+
+  // ---------- Spawn ----------
+  function spawn(){
+    if (!running) return;
+    // find a free hole
+    const free = [];
+    for (let i=0;i<HOLES;i++) if (!occ[i]) free.push(i);
+    if (free.length){
+      // sometimes spawn 2 at once late-game
+      const t = elapsed / ROUND_TIME;
+      const count = (t > 0.5 && Math.random() < 0.3 && free.length > 1) ? 2 : 1;
+      for (let k=0;k<count && free.length;k++){
+        const idx = free.splice(Math.floor(Math.random()*free.length),1)[0];
+        placeTarget(idx);
+      }
+    }
+    const { gap } = difficulty();
+    spawnTimer = setTimeout(spawn, gap + Math.random()*120);
+  }
+
+  function placeTarget(i){
+    const kind = chooseKind();
+    const { upTime } = difficulty();
+    const el = document.createElement("div");
+    el.className = "target " + kind;
+    el.textContent = kind === "bomb" ? "💣" : (kind === "gold" ? "⭐" : "🐹");
+    holes[i].appendChild(el);
+    sfx.pop();
+    // force reflow then raise
+    // eslint-disable-next-line no-unused-expressions
+    el.offsetHeight;
+    requestAnimationFrame(()=> el.classList.add("up"));
+
+    const record = { kind, el, whacked:false, timeout:null };
+    occ[i] = record;
+
+    record.timeout = setTimeout(()=>{
+      if (record.whacked) return;
+      // duck back down
+      el.classList.remove("up");
+      // a mole that escaped un-whacked = miss penalty (bombs escaping are fine)
+      if (kind === "mole" || kind === "gold"){
+        onMissed();
+      }
+      setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 180);
+      if (occ[i] === record) occ[i] = null;
+    }, upTime);
+  }
+
+  // ---------- Interaction ----------
+  function onHolePress(e){
+    e.preventDefault();
+    if (!running) return;
+    const i = +this.dataset.i;
+    const rec = occ[i];
+    if (!rec || rec.whacked){
+      return; // whacked empty hole -> nothing (no penalty for empties)
+    }
+    whack(i, rec, this);
+  }
+
+  function whack(i, rec, holeEl){
+    rec.whacked = true;
+    clearTimeout(rec.timeout);
+    rec.el.classList.add("whacked");
+    ripple(holeEl);
+    setTimeout(()=>{ if (rec.el.parentNode) rec.el.parentNode.removeChild(rec.el); }, 200);
+    if (occ[i] === rec) occ[i] = null;
+
+    const now = performance.now();
+    if (rec.kind === "bomb"){
+      // penalty + life loss
+      combo = 0; lastHitAt = 0;
+      score = Math.max(0, score - BOMB_PENALTY);
+      loseLife();
+      holeEl.classList.add("badhit");
+      setTimeout(()=>holeEl.classList.remove("badhit"),200);
+      floatPop(holeEl, "−"+BOMB_PENALTY, "var(--bad)");
+      sfx.bomb();
+      shake();
+      updateHUD();
+      return;
+    }
+
+    // good hit -> combo
+    if (now - lastHitAt < COMBO_WINDOW) combo = Math.min(MAX_COMBO, combo + 1);
+    else combo = 1;
+    lastHitAt = now;
+
+    const mult = combo;
+    let gained;
+    if (rec.kind === "gold"){
+      gained = GOLD_POINTS * mult;
+      sfx.gold();
+      floatPop(holeEl, "+"+gained, "var(--gold)");
+    } else {
+      gained = BASE_POINTS * mult;
+      sfx.whack(combo);
+      floatPop(holeEl, "+"+gained, "var(--good)");
+    }
+    score += gained;
+    holeEl.classList.add("hit");
+    setTimeout(()=>holeEl.classList.remove("hit"),160);
+    updateHUD();
+  }
+
+  function onMissed(){
+    combo = 0; lastHitAt = 0;
+    score = Math.max(0, score - MISS_PENALTY);
+    sfx.miss();
+    updateHUD();
+  }
+
+  function loseLife(){
+    lives = Math.max(0, lives - 1);
+    renderLives();
+    if (lives <= 0) endRound();
+  }
+
+  // ---------- FX ----------
+  function ripple(holeEl){
+    const r = document.createElement("div");
+    r.className = "ripple";
+    holeEl.appendChild(r);
+    setTimeout(()=>{ if (r.parentNode) r.parentNode.removeChild(r); }, 460);
+  }
+  function floatPop(holeEl, text, color){
+    const p = document.createElement("div");
+    p.className = "pop";
+    p.textContent = text;
+    p.style.color = color;
+    holeEl.appendChild(p);
+    setTimeout(()=>{ if (p.parentNode) p.parentNode.removeChild(p); }, 720);
+  }
+  function shake(){
+    app.classList.remove("shake");
+    // reflow
+    void app.offsetWidth;
+    app.classList.add("shake");
+  }
+
+  // ---------- HUD ----------
+  function updateHUD(){
+    scoreVal.textContent = score;
+    comboVal.textContent = "x" + Math.max(1, combo);
+    comboVal.style.transform = combo > 1 ? "scale(1.15)" : "scale(1)";
+  }
+  function renderLives(){
+    livesEl.textContent = "❤".repeat(lives) + "♡".repeat(Math.max(0,START_LIVES-lives));
+  }
+  function renderTime(){
+    timeVal.textContent = timeLeft;
+    if (timeLeft <= 10) timeStat.classList.add("low");
+    else timeStat.classList.remove("low");
+  }
+
+  // ---------- Round control ----------
+  function startRound(){
+    // reset
+    score = 0; combo = 0; lastHitAt = 0; lives = START_LIVES;
+    timeLeft = ROUND_TIME; elapsed = 0; running = true;
+    clearAll();
+    updateHUD(); renderLives(); renderTime();
+    bestVal.textContent = best;
+    overlay.classList.add("hidden");
+    sfx.start();
+
+    // ticking clock
+    tickTimer = setInterval(()=>{
+      timeLeft--; elapsed++;
+      renderTime();
+      if (timeLeft <= 0) endRound();
+    }, 1000);
+
+    // start spawning
+    spawn();
+  }
+
+  function endRound(){
+    if (!running) return;
+    running = false;
+    clearTimeout(spawnTimer);
+    clearInterval(tickTimer);
+    clearAll();
+    sfx.over();
+
+    // high score
+    const isNew = score > best;
+    if (isNew){
+      best = score;
+      try { localStorage.setItem(LS_KEY, String(best)); } catch(e){}
+    }
+    bestVal.textContent = best;
+
+    // show overlay in game-over mode
+    ovTitle.textContent = "GAME OVER";
+    ovDesc.style.display = "none";
+    finalScore.style.display = "block";
+    finalScore.textContent = score;
+    finalBest.style.display = "block";
+    finalBest.textContent = "Best: " + best;
+    newBest.classList.toggle("hidden", !isNew);
+    startBtn.textContent = "PLAY AGAIN";
+    overlay.classList.remove("hidden");
+  }
+
+  function clearAll(){
+    for (let i=0;i<HOLES;i++){
+      const rec = occ[i];
+      if (rec){ clearTimeout(rec.timeout); if (rec.el && rec.el.parentNode) rec.el.parentNode.removeChild(rec.el); }
+      occ[i] = null;
+    }
+    holes.forEach(h=>{
+      h.classList.remove("hit","badhit");
+      // strip leftover pops/ripples
+      Array.from(h.querySelectorAll(".pop,.ripple,.target")).forEach(n=>n.remove());
+    });
+  }
+
+  // ---------- Mute ----------
+  muteBtn.addEventListener("click", (e)=>{
+    e.stopPropagation();
+    muted = !muted;
+    muteBtn.textContent = muted ? "🔇" : "🔊";
+    try { localStorage.setItem("abramsWhack.muted", muted ? "1":"0"); } catch(e){}
+    if (!muted) audio(); // wake context
+  });
+
+  // ---------- Start button ----------
+  startBtn.addEventListener("click", ()=>{ audio(); startRound(); });
+
+  // keyboard: space/enter to start when not running
+  window.addEventListener("keydown", (e)=>{
+    if (!running && (e.code === "Space" || e.code === "Enter")){
+      e.preventDefault(); audio(); startRound();
+    }
+  });
+
+  // prevent iOS double-tap zoom on board taps handled via pointerdown already
+  document.addEventListener("gesturestart", e=>e.preventDefault());
+
+  // ---------- Init ----------
+  buildBoard();
+  updateHUD(); renderLives(); renderTime();
+
+})();
+</script>
+</body>
+</html>

← 9a1b82f Add 3 newest Model Arena game winners: DVD Bounce, Verlet Ro  ·  back to Games Agentabrams  ·  Fix stale e2e: decouple cube checks from list order (deep-li f4e6a87 →