Models 44

MiMo-V2.6-Pro

by Xiaomireasoning defaultClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 44 s
Cost
$0.0091
Output tokens
13,753
Spent thinking
0 tokens
Speed
84.9 tok/s
Console errors
0
First token after
2.3 s
First visible text after
2.3 s
Prompt tokens
513
Code size
39.2 KB
Finish reason
stop
Served by
GMICloud
Model id
xiaomi/mimo-v2.6-pro
Reasoning sent
model default
Max output
131,072 tokens
Generated
Nov 13, 2026

What it was asked

The prompt
Build a polished Breakout (brick breaker) game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A paddle controlled with the mouse and with the left/right arrow keys. - The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle). - One level with at least 6 rows of bricks, mixing normal bricks and tougher bricks that need 2 hits and visibly crack after the first one. - Some bricks visibly carry a power-up icon; when broken they drop it, and catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls). - The ball must never pass through bricks or the paddle. - Particles when a brick breaks. - 3 lives, score displayed. Game over and victory screens with a restart option.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 1350 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Neon Breaker</title>
<style>
  :root{
    --bg0:#070912;
    --bg1:#101629;
    --ink:#eef2ff;
    --muted:#8e9ac2;
    --cyan:#42e8ff;
    --pink:#ff4d7d;
    --gold:#ffd166;
  }
  *{box-sizing:border-box;}
  html,body{
    margin:0; padding:0;
    height:100%;
    background:
      radial-gradient(1100px 700px at 20% -10%, #1c2a4d 0%, transparent 60%),
      radial-gradient(900px 600px at 110% 110%, #2a1436 0%, transparent 55%),
      linear-gradient(160deg, var(--bg0), var(--bg1));
    color:var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    overflow:hidden;
    -webkit-font-smoothing:antialiased;
    user-select:none;
    -webkit-user-select:none;
    touch-action:none;
  }
  body{
    display:flex;
    align-items:center;
    justify-content:center;
    padding:clamp(10px, 2.2vh, 26px) clamp(10px, 2vw, 26px);
  }
  .shell{
    width:min(1120px, 100%);
    display:flex;
    flex-direction:column;
    gap:clamp(10px, 1.6vh, 18px);
    position:relative;
    z-index:1;
  }
  header{
    display:flex;
    align-items:flex-end;
    justify-content:space-between;
    gap:18px;
    padding:0 4px;
  }
  .brand{display:flex; align-items:center; gap:12px;}
  .mark{
    width:42px; height:42px; flex:none;
    border-radius:13px;
    background:linear-gradient(145deg, var(--cyan), #6b6cff 55%, var(--pink));
    box-shadow:0 10px 28px rgba(66,232,255,.28), inset 0 -8px 18px rgba(0,0,0,.28);
    position:relative;
    overflow:hidden;
  }
  .mark::after{
    content:""; position:absolute; inset:0;
    background:linear-gradient(120deg, transparent 35%, rgba(255,255,255,.55) 48%, transparent 62%);
    mix-blend-mode:overlay;
  }
  h1{
    margin:0;
    font-size:clamp(18px, 2.4vw, 26px);
    letter-spacing:.12em;
    font-weight:800;
    text-transform:uppercase;
  }
  .subtitle{
    margin:3px 0 0;
    color:var(--muted);
    font-size:clamp(11px, 1.3vw, 13px);
    letter-spacing:.16em;
    text-transform:uppercase;
  }
  .legend{
    display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end;
  }
  .chip{
    display:inline-flex; align-items:center; gap:7px;
    padding:7px 11px;
    border-radius:999px;
    background:rgba(255,255,255,.06);
    border:1px solid rgba(255,255,255,.10);
    color:#c6d2f5;
    font-size:11px;
    letter-spacing:.09em;
    text-transform:uppercase;
    white-space:nowrap;
  }
  .chip b{color:#fff; font-weight:700;}
  .dot{width:9px; height:9px; border-radius:50%;}
  .dot.c{background:var(--cyan); box-shadow:0 0 10px var(--cyan);}
  .dot.p{background:var(--pink); box-shadow:0 0 10px var(--pink);}
  .dot.g{background:var(--gold); box-shadow:0 0 10px var(--gold);}

  .stage{
    position:relative;
    width:100%;
    border-radius:26px;
    padding:clamp(8px, 1.4vw, 16px);
    background:
      linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.03));
    border:1px solid rgba(255,255,255,.12);
    box-shadow:
      0 40px 90px rgba(0,0,0,.45),
      0 2px 0 rgba(255,255,255,.06) inset;
    overflow:hidden;
  }
  .stage::before{
    content:"";
    position:absolute; inset:0;
    background:
      radial-gradient(520px 320px at 50% -10%, rgba(66,232,255,.16), transparent 70%),
      radial-gradient(420px 280px at 8% 110%, rgba(255,77,125,.14), transparent 70%);
    pointer-events:none;
  }
  .canvasWrap{
    position:relative;
    width:100%;
    border-radius:16px;
    overflow:hidden;
    background:rgba(6,9,20,.72);
    box-shadow:0 18px 50px rgba(0,0,0,.35) inset, 0 1px 0 rgba(255,255,255,.08);
  }
  canvas{
    display:block;
    width:100%;
    height:auto;
    aspect-ratio: 16 / 10;
    cursor:crosshair;
    touch-action:none;
  }

  .overlay{
    position:absolute;
    inset:0;
    display:flex;
    align-items:center;
    justify-content:center;
    text-align:center;
    padding:22px;
    background:rgba(5,8,20,.62);
    backdrop-filter: blur(7px) saturate(120%);
    -webkit-backdrop-filter: blur(7px) saturate(120%);
    opacity:1;
    transition:opacity .28s ease;
    z-index:3;
  }
  .overlay.hidden{opacity:0; pointer-events:none;}
  .panel{
    width:min(560px, 94%);
    padding:clamp(20px, 3.4vw, 34px) clamp(18px, 3vw, 32px);
    border-radius:22px;
    background:
      linear-gradient(180deg, rgba(22,32,62,.92), rgba(13,18,38,.94));
    border:1px solid rgba(150,190,255,.22);
    box-shadow:0 30px 80px rgba(0,0,0,.5), 0 0 60px rgba(66,232,255,.12) inset;
  }
  .kicker{
    display:inline-block;
    padding:6px 11px;
    border-radius:999px;
    background:rgba(66,232,255,.12);
    border:1px solid rgba(66,232,255,.28);
    color:var(--cyan);
    font-size:11px;
    letter-spacing:.18em;
    text-transform:uppercase;
    margin-bottom:14px;
  }
  .panel h2{
    margin:0 0 12px;
    font-size:clamp(26px, 5vw, 46px);
    letter-spacing:.04em;
    line-height:1.02;
    background:linear-gradient(180deg, #ffffff, #b9c9ff);
    -webkit-background-clip:text;
    background-clip:text;
    color:transparent;
  }
  .panel p{
    margin:0 auto 22px;
    max-width:440px;
    color:#a9b6da;
    font-size:clamp(13px, 1.8vw, 15px);
    line-height:1.65;
  }
  .controls{
    display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:22px;
  }
  .key{
    padding:8px 12px;
    border-radius:11px;
    background:rgba(255,255,255,.07);
    border:1px solid rgba(255,255,255,.12);
    color:#c9d6f8;
    font-size:12px;
    letter-spacing:.08em;
    text-transform:uppercase;
  }
  .key b{color:#fff; font-weight:700; letter-spacing:.12em;}
  button{
    appearance:none; border:0; cursor:pointer;
    font-family:inherit; font-weight:800;
    letter-spacing:.12em; text-transform:uppercase;
    padding:15px 30px;
    border-radius:13px;
    font-size:13px;
    color:#031022;
    background:linear-gradient(135deg, var(--cyan), #8ef1ff 40%, #b6c2ff);
    box-shadow:0 14px 32px rgba(66,232,255,.28), 0 1px 0 rgba(255,255,255,.5) inset;
    transition:transform .18s ease, box-shadow .18s ease, filter .18s ease;
  }
  button:hover{transform:translateY(-2px); filter:brightness(1.05);}
  button:active{transform:translateY(1px) scale(.985);}
  .hint{
    margin-top:14px;
    color:#6f7fae;
    font-size:11px;
    letter-spacing:.12em;
    text-transform:uppercase;
  }
  .resultRow{
    display:flex; gap:10px; justify-content:center; margin-bottom:22px;
  }
  .stat{
    flex:1; min-width:120px;
    padding:13px 10px;
    border-radius:14px;
    background:rgba(255,255,255,.07);
    border:1px solid rgba(255,255,255,.12);
  }
  .stat .v{font-size:clamp(20px,4vw,28px); font-weight:800;}
  .stat .l{color:#8b9bc6; font-size:10px; letter-spacing:.16em; text-transform:uppercase; margin-top:4px;}

  .foot{
    display:flex; justify-content:space-between; gap:14px;
    color:#7a89b3; font-size:11px; letter-spacing:.12em; text-transform:uppercase;
    padding:0 6px;
  }
  .foot .right{color:#65749c;}

  @media (max-width: 620px){
    header{flex-direction:column; align-items:flex-start; gap:12px;}
    .legend{justify-content:flex-start;}
    .foot{flex-direction:column; gap:6px;}
    .panel{padding:22px 16px;}
  }
  @media (max-height: 620px){
    header{display:none;}
    .foot{display:none;}
    body{padding:10px;}
  }
</style>
</head>
<body>
<div class="shell">
  <header>
    <div class="brand">
      <div class="mark" aria-hidden="true"></div>
      <div>
        <h1>Neon Breaker</h1>
        <p class="subtitle">Single-level arcade demolition</p>
      </div>
    </div>
    <div class="legend" aria-hidden="true">
      <span class="chip"><span class="dot c"></span> Catch the <b>W</b> for wide paddle</span>
      <span class="chip"><span class="dot g"></span> Catch the <b>M</b> for multi-ball</span>
      <span class="chip"><span class="dot p"></span> Cracked bricks take <b>2 hits</b></span>
    </div>
  </header>

  <div class="stage">
    <div class="canvasWrap">
      <canvas id="game" width="1280" height="800" aria-label="Breakout game"></canvas>

      <div class="overlay" id="overlay">
        <div class="panel">
          <div class="kicker" id="kicker">Click or press a key to start</div>
          <h2 id="overlayTitle">Break every brick</h2>
          <p id="overlayText">
            Steer the paddle with your mouse or the arrow keys. The ball bounces at sharper angles
            when it catches the edge of the paddle — use that to carve through the wall.
          </p>
          <div class="controls" id="controls">
            <div class="key"><b>Mouse</b> / <b>← →</b> move</div>
            <div class="key"><b>Space</b> launch</div>
            <div class="key"><b>P</b> pause</div>
          </div>
          <button id="startBtn">Start Game</button>
          <div class="hint" id="hint">A self-playing demo runs until you take control.</div>
        </div>
      </div>
    </div>
  </div>

  <div class="foot">
    <div>3 lives · score &amp; lives tracked above the field</div>
    <div class="right">Vanilla JS · Canvas 2D</div>
  </div>
</div>

<script>
(() => {
  "use strict";

  const canvas = document.getElementById("game");
  const ctx = canvas.getContext("2d", { alpha: true });

  const overlay = document.getElementById("overlay");
  const overlayTitle = document.getElementById("overlayTitle");
  const overlayText = document.getElementById("overlayText");
  const kicker = document.getElementById("kicker");
  const controls = document.getElementById("controls");
  const hint = document.getElementById("hint");
  const startBtn = document.getElementById("startBtn");

  const W = 1280, H = 800;

  // ---------- Physics (fixed step) ----------
  const STEP = 1 / 120;          // 120 Hz physics
  const SUBSTEPS = 8;            // swept ball -> collision samples
  const MAX_BALL_SPEED = 1060;

  // ---------- Layout ----------
  const HUD_H = 88;
  const FIELD_TOP = HUD_H;
  const WALL = 14;
  const BRICK_COLS = 12;
  const BRICK_ROWS = 7;
  const BRICK_GAP = 10;
  const BRICK_H = 38;
  const BRICK_TOP = FIELD_TOP + 54;
  const BRICK_AREA_W = W - WALL * 2 - 26;
  const BRICK_W = (BRICK_AREA_W - (BRICK_COLS - 1) * BRICK_GAP) / BRICK_COLS;
  const BRICK_LEFT = WALL + 13;

  // ---------- Palette ----------
  const PALETTE = [
    ["#ff5f7e", "#ff2f65"],
    ["#ff9052", "#ff5d2e"],
    ["#ffd05c", "#ff9d2e"],
    ["#8fe86b", "#35c96b"],
    ["#42e8ff", "#2b8fff"],
    ["#7c8dff", "#4b4dff"],
    ["#d477ff", "#8b3dff"]
  ];

  // ---------- State ----------
  let state = "demo";            // demo | playing | paused | gameover | victory
  let score = 0;
  let lives = 3;
  let combo = 0;
  let lastTime = 0;
  let accumulator = 0;
  let flash = 0;
  let shake = 0;
  let message = null;
  let messageTimer = 0;

  const keys = Object.create(null);
  let mouseActive = false;

  const paddle = {
    x: W / 2,
    y: H - 74,
    w: 190,
    baseW: 190,
    maxW: 320,
    h: 22,
    speed: 900,
    wideTimer: 0,
    glow: 0
  };

  let balls = [];
  let bricks = [];
  let drops = [];
  let particles = [];
  let sparkles = [];

  // ---------- Utility ----------
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  const lerp = (a, b, t) => a + (b - a) * t;
  const rand = (a, b) => a + Math.random() * (b - a);

  function roundRectPath(c, x, y, w, h, r) {
    r = Math.min(r, w / 2, h / 2);
    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();
  }

  function hexToRgb(hex) {
    const h = hex.replace("#", "");
    const n = parseInt(h, 16);
    return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
  }
  function shade(hex, amt) {
    const [r, g, b] = hexToRgb(hex);
    const f = (v) => clamp(Math.round(v + amt), 0, 255);
    return `rgb(${f(r)},${f(g)},${f(b)})`;
  }

  // ---------- Brick build ----------
  function buildBricks() {
    bricks = [];
    for (let r = 0; r < BRICK_ROWS; r++) {
      for (let c = 0; c < BRICK_COLS; c++) {
        const x = BRICK_LEFT + c * (BRICK_W + BRICK_GAP);
        const y = BRICK_TOP + r * (BRICK_H + BRICK_GAP);
        // tougher bricks toward the middle of the wall
        const centerBias = Math.abs(c - (BRICK_COLS - 1) / 2);
        const toughChance = 0.16 + (3.4 - centerBias) * 0.10;
        const tough = Math.random() < toughChance;
        let power = null;
        if (!tough && Math.random() < 0.16) {
          power = Math.random() < 0.55 ? "wide" : "multi";
        }
        bricks.push({
          x, y, w: BRICK_W, h: BRICK_H,
          hp: tough ? 2 : 1,
          maxHp: tough ? 2 : 1,
          colors: PALETTE[r % PALETTE.length],
          power,
          alive: true,
          hitFlash: 0,
          row: r, col: c
        });
      }
    }
  }

  // ---------- Balls ----------
  function makeBall(x, y, vx, vy, stuck = false) {
    return {
      x, y,
      r: 12,
      vx, vy,
      stuck,
      stuckOffset: 0,
      trail: [],
      lastPaddleHit: -1
    };
  }

  function launchBall(b, angleBias = 0) {
    const angle = -Math.PI / 2 + angleBias;
    const speed = 520;
    b.vx = Math.cos(angle) * speed;
    b.vy = Math.sin(angle) * speed;
    b.stuck = false;
    b.trail.length = 0;
  }

  function resetBallOnPaddle() {
    balls = [];
    const b = makeBall(paddle.x, paddle.y - paddle.h / 2 - 13, 0, -520, true);
    b.stuckOffset = 0;
    balls.push(b);
  }

  function totalBricksAlive() {
    let n = 0;
    for (const b of bricks) if (b.alive) n++;
    return n;
  }

  // ---------- Effects ----------
  function burst(x, y, colors, count = 18, speed = 420) {
    for (let i = 0; i < count; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = speed * (0.35 + Math.random() * 0.9);
      particles.push({
        x, y,
        vx: Math.cos(a) * s,
        vy: Math.sin(a) * s - rand(0, 90),
        life: 0.5 + Math.random() * 0.55,
        maxLife: 0.5 + Math.random() * 0.55,
        size: 3 + Math.random() * 7,
        color: colors[Math.floor(Math.random() * colors.length)],
        rot: Math.random() * Math.PI,
        vr: rand(-8, 8),
        gravity: 420 + Math.random() * 380,
        square: Math.random() < 0.65
      });
    }
    for (let i = 0; i < 8; i++) {
      const a = Math.random() * Math.PI * 2;
      sparkles.push({
        x, y,
        vx: Math.cos(a) * speed * 0.35,
        vy: Math.sin(a) * speed * 0.35,
        life: 0.35 + Math.random() * 0.35,
        maxLife: 0.5,
        size: 2 + Math.random() * 3,
        color: "#ffffff"
      });
    }
  }

  function showMessage(text, duration = 1.4) {
    message = text;
    messageTimer = duration;
  }

  // ---------- Collisions ----------
  function collidePaddle(b, dt) {
    const px = paddle.x - paddle.w / 2;
    const py = paddle.y - paddle.h / 2;

    if (b.vy > 0 &&
        b.x + b.r > px &&
        b.x - b.r < px + paddle.w &&
        b.y + b.r > py &&
        b.y - b.r < py + paddle.h + 6) {

      b.y = py - b.r - 0.5;
      const rel = clamp((b.x - paddle.x) / (paddle.w / 2), -1, 1);
      const angle = rel * (Math.PI * 0.42);   // center = straight up, edges = sharp
      const speed = clamp(Math.hypot(b.vx, b.vy) * 1.012, 470, MAX_BALL_SPEED);
      b.vx = Math.sin(angle) * speed + paddle.vx * 0.16;
      b.vy = -Math.abs(Math.cos(angle) * speed);
      const norm = Math.hypot(b.vx, b.vy);
      b.vx = b.vx / norm * speed;
      b.vy = b.vy / norm * speed;
      combo = 0;
      paddle.glow = 1;
      burst(b.x, py, ["#42e8ff", "#8ef1ff", "#ffffff"], 7, 180);
    }
  }

  function collideBrick(b, brick) {
    if (!brick.alive) return false;
    const bx = brick.x, by = brick.y, bw = brick.w, bh = brick.h;
    const closestX = clamp(b.x, bx, bx + bw);
    const closestY = clamp(b.y, by, by + bh);
    const dx = b.x - closestX;
    const dy = b.y - closestY;
    const d2 = dx * dx + dy * dy;
    if (d2 > b.r * b.r) return false;

    // Determine dominant axis of penetration
    const overlapX = (bw / 2 + b.r) - Math.abs(b.x - (bx + bw / 2));
    const overlapY = (bh / 2 + b.r) - Math.abs(b.y - (by + bh / 2));

    if (overlapX < overlapY) {
      if (b.x < bx + bw / 2) b.x = bx - b.r - 0.2;
      else b.x = bx + bw + b.r + 0.2;
      b.vx = -b.vx;
    } else {
      if (b.y < by + bh / 2) b.y = by - b.r - 0.2;
      else b.y = by + bh + b.r + 0.2;
      b.vy = -b.vy;
    }

    hitBrick(brick);
    return true;
  }

  function hitBrick(brick) {
    brick.hp--;
    brick.hitFlash = 1;
    const colors = [brick.colors[0], brick.colors[1], "#ffffff"];
    burst(brick.x + brick.w / 2, brick.y + brick.h / 2, colors, 7, 220);

    if (brick.hp <= 0) {
      brick.alive = false;
      combo++;
      const base = 100 + (brick.maxHp - 1) * 60;
      const gain = base + Math.min(combo, 12) * 12;
      score += gain;
      burst(brick.x + brick.w / 2, brick.y + brick.h / 2, colors, 20, 430);
      shake = Math.min(shake + 0.12, 0.42);
      if (brick.power) {
        drops.push({
          x: brick.x + brick.w / 2,
          y: brick.y + brick.h / 2,
          vy: 180,
          vx: 0,
          r: 19,
          type: brick.power,
          wobble: Math.random() * Math.PI * 2
        });
      }
      if (totalBricksAlive() === 0 && state === "playing") {
        state = "victory";
        showEndScreen(true);
      }
    } else {
      // tough brick cracked after first hit
      score += 25;
      burst(brick.x + brick.w / 2, brick.y + brick.h / 2, ["#ffffff", brick.colors[0]], 5, 180);
    }
  }

  function hitWall(b) {
    if (b.x - b.r < WALL) {
      b.x = WALL + b.r;
      b.vx = Math.abs(b.vx);
      burst(b.x, b.y, ["#42e8ff", "#8ef1ff"], 3, 120);
    } else if (b.x + b.r > W - WALL) {
      b.x = W - WALL - b.r;
      b.vx = -Math.abs(b.vx);
      burst(b.x, b.y, ["#42e8ff", "#8ef1ff"], 3, 120);
    }
    if (b.y - b.r < FIELD_TOP) {
      b.y = FIELD_TOP + b.r;
      b.vy = Math.abs(b.vy);
      burst(b.x, b.y, ["#42e8ff", "#8ef1ff"], 3, 120);
    }
  }

  // ---------- Power-ups ----------
  function collectDrop(d) {
    if (d.type === "wide") {
      paddle.wideTimer = 10;
      paddle.w = Math.min(paddle.w + 110, paddle.maxW);
      showMessage("WIDE PADDLE · 10s");
      burst(d.x, d.y, ["#42e8ff", "#8ef1ff", "#ffffff"], 18, 320);
    } else {
      // Multi-ball: top up to 3 live balls
      const existing = balls.filter(b => !b.stuck).length;
      const target = 3;
      const need = Math.max(0, target - existing);
      const source = balls.find(b => !b.stuck) || balls[0];
      for (let i = 0; i < need; i++) {
        const angle = -Math.PI / 2 + (i % 2 === 0 ? -1 : 1) * (0.32 + i * 0.12);
        const speed = Math.hypot(source.vx, source.vy) || 520;
        balls.push(makeBall(
          source.x, source.y,
          Math.cos(angle) * speed,
          Math.sin(angle) * speed,
          false
        ));
      }
      showMessage("MULTI-BALL · 3 balls");
      burst(d.x, d.y, ["#ffd166", "#fff1b8", "#ffffff"], 22, 380);
    }
  }

  // ---------- Update ----------
  function update(dt) {
    // Paddle
    let dir = 0;
    if (keys["ArrowLeft"] || keys["a"] || keys["A"]) dir -= 1;
    if (keys["ArrowRight"] || keys["d"] || keys["D"]) dir += 1;

    const prevX = paddle.x;
    if (dir !== 0) {
      paddle.x += dir * paddle.speed * dt;
      mouseActive = false;
    }
    paddle.x = clamp(paddle.x, WALL + paddle.w / 2 + 4, W - WALL - paddle.w / 2 - 4);
    paddle.vx = (paddle.x - prevX) / Math.max(dt, 0.0001);

    // Wide paddle timer
    if (paddle.wideTimer > 0) {
      paddle.wideTimer -= dt;
      if (paddle.wideTimer <= 0) {
        paddle.wideTimer = 0;
        paddle.w = paddle.baseW;
      }
    }
    paddle.glow = Math.max(0, paddle.glow - dt * 2.2);

    // Balls
    for (const b of balls) {
      if (b.stuck) {
        b.x = paddle.x + b.stuckOffset;
        b.y = paddle.y - paddle.h / 2 - b.r - 1;
        continue;
      }

      const speed = Math.hypot(b.vx, b.vy);
      const steps = clamp(Math.ceil(speed * dt / 4), 1, SUBSTEPS);
      const sdt = dt / steps;

      for (let s = 0; s < steps; s++) {
        const prevX2 = b.x, prevY2 = b.y;
        b.x += b.vx * sdt;
        b.y += b.vy * sdt;

        hitWall(b);
        collidePaddle(b, sdt);

        // Bricks: test all bricks, resolve only nearest overlap
        let best = null, bestDist = Infinity;
        for (const brick of bricks) {
          if (!brick.alive) continue;
          const cx = clamp(b.x, brick.x, brick.x + brick.w);
          const cy = clamp(b.y, brick.y, brick.y + brick.h);
          const dx = b.x - cx, dy = b.y - cy;
          const d = dx * dx + dy * dy;
          if (d <= b.r * b.r && d < bestDist) {
            bestDist = d;
            best = brick;
          }
        }
        if (best) {
          collideBrick(b, best);
          // one resolution per substep keeps tunneling out
        }

        // avoid weird full vertical lock
        if (Math.abs(b.vy) < 80) b.vy = b.vy >= 0 ? 80 : -80;

        b.trail.push({ x: b.x, y: b.y });
        if (b.trail.length > 10) b.trail.shift();
      }
    }

    // Ball removal
    for (let i = balls.length - 1; i >= 0; i--) {
      const b = balls[i];
      if (b.y - b.r > H + 40) balls.splice(i, 1);
    }

    if (balls.length === 0 && state === "playing") {
      lives--;
      if (lives <= 0) {
        state = "gameover";
        showEndScreen(false);
      } else {
        resetBallOnPaddle();
        showMessage("LIFE LOST", 1.1);
      }
    }

    // Drops
    for (let i = drops.length - 1; i >= 0; i--) {
      const d = drops[i];
      d.wobble += dt * 3;
      d.x += Math.sin(d.wobble) * 26 * dt;
      d.y += d.vy * dt;
      const px = paddle.x - paddle.w / 2;
      const py = paddle.y - paddle.h / 2;
      if (d.y + d.r > py && d.y - d.r < py + paddle.h &&
          d.x > px - d.r && d.x < px + paddle.w + d.r) {
        collectDrop(d);
        drops.splice(i, 1);
      } else if (d.y - d.r > H) {
        drops.splice(i, 1);
      }
    }

    // Particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life -= dt;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      p.vy += p.gravity * dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.rot += p.vr * dt;
    }
    for (let i = sparkles.length - 1; i >= 0; i--) {
      const s = sparkles[i];
      s.life -= dt;
      if (s.life <= 0) { sparkles.splice(i, 1); continue; }
      s.x += s.vx * dt;
      s.y += s.vy * dt;
      s.vx *= 0.94;
      s.vy *= 0.94;
    }

    flash = Math.max(0, flash - dt * 1.8);
    shake = Math.max(0, shake - dt * 0.9);
    if (messageTimer > 0) {
      messageTimer -= dt;
      if (messageTimer <= 0) message = null;
    }

    // Demo autopilot
    if (state === "demo") {
      const target = nearestBallTarget();
      const desired = clamp(target, WALL + paddle.w / 2, W - WALL - paddle.w / 2);
      paddle.x += (desired - paddle.x) * Math.min(1, dt * 2.6);
      for (const b of balls) {
        if (b.stuck) {
          b.stuck = false;
          launchBall(b, rand(-0.35, 0.35));
        }
      }
    }
  }

  function nearestBallTarget() {
    let best = balls[0];
    if (!best) return W / 2;
    // aim toward a gap: use ball x plus a little steering
    const lead = best.vx * 0.10;
    return best.x + lead;
  }

  // ---------- Drawing ----------
  function draw() {
    ctx.clearRect(0, 0, W, H);

    // Background glow
    const bg = ctx.createLinearGradient(0, 0, W, H);
    bg.addColorStop(0, "#0b1226");
    bg.addColorStop(0.55, "#0a1022");
    bg.addColorStop(1, "#120c22");
    ctx.fillStyle = bg;
    ctx.fillRect(0, 0, W, H);

    // Subtle grid
    ctx.save();
    ctx.globalAlpha = 0.16;
    ctx.strokeStyle = "#28406d";
    ctx.lineWidth = 1;
    for (let x = WALL; x <= W - WALL; x += 42) {
      ctx.beginPath();
      ctx.moveTo(x, FIELD_TOP);
      ctx.lineTo(x, H);
      ctx.stroke();
    }
    for (let y = FIELD_TOP; y <= H; y += 42) {
      ctx.beginPath();
      ctx.moveTo(WALL, y);
      ctx.lineTo(W - WALL, y);
      ctx.stroke();
    }
    ctx.restore();

    // Field vignette
    const vign = ctx.createRadialGradient(W / 2, H * 0.42, 120, W / 2, H * 0.5, W * 0.72);
    vign.addColorStop(0, "rgba(30,60,120,0.18)");
    vign.addColorStop(1, "rgba(0,0,0,0.42)");
    ctx.fillStyle = vign;
    ctx.fillRect(0, FIELD_TOP, W, H - FIELD_TOP);

    drawWalls();
    drawBricks();
    drawDrops();
    drawPaddle();
    drawBalls();
    drawParticles();
    drawHUD();

    if (flash > 0) {
      ctx.fillStyle = `rgba(120,220,255,${flash * 0.16})`;
      ctx.fillRect(0, 0, W, H);
    }

    if (message) {
      ctx.save();
      ctx.globalAlpha = clamp(messageTimer * 1.8, 0, 1);
      ctx.font = "800 22px ui-sans-serif, system-ui, sans-serif";
      ctx.textAlign = "center";
      ctx.fillStyle = "#ffffff";
      ctx.shadowColor = "rgba(66,232,255,0.8)";
      ctx.shadowBlur = 18;
      ctx.fillText(message, W / 2, FIELD_TOP + 34);
      ctx.restore();
    }

    if (state === "paused") {
      ctx.save();
      ctx.fillStyle = "rgba(8,12,28,0.55)";
      ctx.fillRect(0, 0, W, H);
      ctx.fillStyle = "#ffffff";
      ctx.font = "800 30px ui-sans-serif, system-ui, sans-serif";
      ctx.textAlign = "center";
      ctx.fillText("PAUSED", W / 2, H / 2);
      ctx.font = "500 15px ui-sans-serif, system-ui, sans-serif";
      ctx.fillStyle = "#9fb0dc";
      ctx.fillText("Press P to resume", W / 2, H / 2 + 30);
      ctx.restore();
    }
  }

  function drawWalls() {
    const wallGrad = ctx.createLinearGradient(0, FIELD_TOP, 0, H);
    wallGrad.addColorStop(0, "rgba(66,232,255,0.22)");
    wallGrad.addColorStop(1, "rgba(66,232,255,0.06)");
    ctx.fillStyle = wallGrad;
    ctx.fillRect(0, FIELD_TOP, WALL, H - FIELD_TOP);
    ctx.fillRect(W - WALL, FIELD_TOP, WALL, H - FIELD_TOP);
    ctx.fillRect(0, FIELD_TOP, W, 8);

    ctx.strokeStyle = "rgba(120,220,255,0.42)";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(WALL, FIELD_TOP); ctx.lineTo(WALL, H);
    ctx.moveTo(W - WALL, FIELD_TOP); ctx.lineTo(W - WALL, H);
    ctx.moveTo(WALL, FIELD_TOP + 4); ctx.lineTo(W - WALL, FIELD_TOP + 4);
    ctx.stroke();
  }

  function drawBricks() {
    for (const b of bricks) {
      if (!b.alive) continue;

      const [c1, c2] = b.colors;
      const g = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
      g.addColorStop(0, shade(c1, 24));
      g.addColorStop(0.42, c1);
      g.addColorStop(1, c2);

      ctx.save();
      ctx.shadowColor = b.hp > 1 ? "rgba(255,120,160,0.32)" : "rgba(80,170,255,0.25)";
      ctx.shadowBlur = b.hitFlash > 0 ? 22 : 12;
      ctx.shadowOffsetY = 3;
      roundRectPath(ctx, b.x, b.y, b.w, b.h, 9);
      ctx.fillStyle = g;
      ctx.fill();

      // top highlight
      roundRectPath(ctx, b.x + 3, b.y + 3, b.w - 6, 9, 5);
      ctx.fillStyle = "rgba(255,255,255,0.22)";
      ctx.fill();
      ctx.restore();

      // border
      ctx.strokeStyle = "rgba(255,255,255,0.16)";
      ctx.lineWidth = 1.2;
      roundRectPath(ctx, b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1, 9);
      ctx.stroke();

      // cracked state after first hit
      if (b.hp < b.maxHp) {
        ctx.save();
        ctx.strokeStyle = "rgba(255,255,255,0.72)";
        ctx.lineWidth = 2;
        ctx.shadowColor = "rgba(255,255,255,0.55)";
        ctx.shadowBlur = 6;
        ctx.beginPath();
        ctx.moveTo(b.x + b.w * 0.16, b.y + 4);
        ctx.lineTo(b.x + b.w * 0.36, b.y + b.h * 0.42);
        ctx.lineTo(b.x + b.w * 0.27, b.y + b.h - 4);
        ctx.moveTo(b.x + b.w * 0.36, b.y + b.h * 0.42);
        ctx.lineTo(b.x + b.w * 0.56, b.y + b.h * 0.28);
        ctx.moveTo(b.x + b.w * 0.36, b.y + b.h * 0.42);
        ctx.lineTo(b.x + b.w * 0.62, b.y + b.h * 0.76);
        ctx.moveTo(b.x + b.w * 0.72, b.y + 5);
        ctx.lineTo(b.x + b.w * 0.62, b.y + b.h * 0.76);
        ctx.stroke();
        ctx.restore();
      }

      // power-up icon badge
      if (b.power) {
        const cx = b.x + b.w - 15;
        const cy = b.y + 14;
        ctx.save();
        ctx.beginPath();
        ctx.arc(cx, cy, 11, 0, Math.PI * 2);
        ctx.fillStyle = b.power === "wide" ? "#42e8ff" : "#ffd166";
        ctx.fill();
        ctx.lineWidth = 2;
        ctx.strokeStyle = "rgba(255,255,255,0.7)";
        ctx.stroke();
        ctx.fillStyle = "#0b1730";
        ctx.font = "800 11px ui-sans-serif, system-ui, sans-serif";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText(b.power === "wide" ? "W" : "M", cx, cy + 0.5);
        ctx.restore();
      }

      if (b.hitFlash > 0) {
        ctx.save();
        ctx.globalAlpha = b.hitFlash * 0.55;
        roundRectPath(ctx, b.x, b.y, b.w, b.h, 9);
        ctx.fillStyle = "#ffffff";
        ctx.fill();
        ctx.restore();
        b.hitFlash = Math.max(0, b.hitFlash - 0.06);
      }
    }
  }

  function drawDrops() {
    for (const d of drops) {
      const col = d.type === "wide" ? "#42e8ff" : "#ffd166";
      ctx.save();
      ctx.translate(d.x, d.y);
      ctx.rotate(Math.sin(d.wobble) * 0.12);

      ctx.shadowColor = col;
      ctx.shadowBlur = 22;
      const g = ctx.createLinearGradient(-d.r, -d.r, d.r, d.r);
      g.addColorStop(0, shade(col, 36));
      g.addColorStop(1, shade(col, -30));
      roundRectPath(ctx, -d.r, -d.r * 0.72, d.r * 2, d.r * 1.44, 10);
      ctx.fillStyle = g;
      ctx.fill();
      ctx.shadowBlur = 0;

      ctx.strokeStyle = "rgba(255,255,255,0.7)";
      ctx.lineWidth = 2;
      roundRectPath(ctx, -d.r + 1, -d.r * 0.72 + 1, d.r * 2 - 2, d.r * 1.44 - 2, 9);
      ctx.stroke();

      ctx.fillStyle = "#0a1730";
      ctx.font = "800 16px ui-sans-serif, system-ui, sans-serif";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillText(d.type === "wide" ? "W" : "M", 0, 1);

      // glow trail
      ctx.restore();
    }
  }

  function drawPaddle() {
    const x = paddle.x - paddle.w / 2;
    const y = paddle.y - paddle.h / 2;

    // glow
    ctx.save();
    const glow = ctx.createRadialGradient(paddle.x, paddle.y, 10, paddle.x, paddle.y, paddle.w * 0.8);
    glow.addColorStop(0, `rgba(66,232,255,${0.22 + paddle.glow * 0.18})`);
    glow.addColorStop(1, "rgba(66,232,255,0)");
    ctx.fillStyle = glow;
    ctx.fillRect(x - 80, y - 50, paddle.w + 160, paddle.h + 100);
    ctx.restore();

    const g = ctx.createLinearGradient(x, y, x, y + paddle.h);
    g.addColorStop(0, "#8ef1ff");
    g.addColorStop(0.35, "#42e8ff");
    g.addColorStop(1, "#2b6fff");
    ctx.save();
    ctx.shadowColor = "rgba(66,232,255,0.55)";
    ctx.shadowBlur = 22;
    roundRectPath(ctx, x, y, paddle.w, paddle.h, 11);
    ctx.fillStyle = g;
    ctx.fill();
    ctx.restore();

    // top gloss
    roundRectPath(ctx, x + 6, y + 3, paddle.w - 12, 7, 4);
    ctx.fillStyle = "rgba(255,255,255,0.55)";
    ctx.fill();

    // dark underside
    roundRectPath(ctx, x + 8, y + paddle.h - 6, paddle.w - 16, 3, 2);
    ctx.fillStyle = "rgba(10,30,80,0.45)";
    ctx.fill();

    if (paddle.wideTimer > 0) {
      const pct = paddle.wideTimer / 10;
      ctx.fillStyle = "rgba(66,232,255,0.18)";
      roundRectPath(ctx, x, y + paddle.h + 8, paddle.w, 4, 2);
      ctx.fill();
      ctx.fillStyle = "#42e8ff";
      roundRectPath(ctx, x, y + paddle.h + 8, paddle.w * pct, 4, 2);
      ctx.fill();
    }
  }

  function drawBalls() {
    for (const b of balls) {
      // trail
      for (let i = 0; i < b.trail.length; i++) {
        const t = b.trail[i];
        const alpha = (i + 1) / b.trail.length * 0.22;
        const r = b.r * (0.35 + (i / b.trail.length) * 0.65);
        ctx.beginPath();
        ctx.arc(t.x, t.y, r, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(120,230,255,${alpha})`;
        ctx.fill();
      }

      const g = ctx.createRadialGradient(b.x - 4, b.y - 5, 2, b.x, b.y, b.r * 1.1);
      g.addColorStop(0, "#ffffff");
      g.addColorStop(0.32, "#b9f4ff");
      g.addColorStop(1, "#2b7dff");
      ctx.save();
      ctx.shadowColor = "rgba(80,210,255,0.85)";
      ctx.shadowBlur = 22;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.fillStyle = g;
      ctx.fill();
      ctx.restore();
    }
  }

  function drawParticles() {
    for (const p of particles) {
      const a = clamp(p.life / p.maxLife, 0, 1);
      ctx.save();
      ctx.globalAlpha = a;
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);
      ctx.fillStyle = p.color;
      if (p.square) {
        ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
      } else {
        ctx.beginPath();
        ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();
    }
    for (const s of sparkles) {
      const a = clamp(s.life / s.maxLife, 0, 1);
      ctx.save();
      ctx.globalAlpha = a;
      ctx.fillStyle = s.color;
      ctx.shadowColor = s.color;
      ctx.shadowBlur = 10;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  }

  function drawHUD() {
    // background strip
    const hud = ctx.createLinearGradient(0, 0, 0, HUD_H);
    hud.addColorStop(0, "rgba(8,14,32,0.95)");
    hud.addColorStop(1, "rgba(10,18,40,0.72)");
    ctx.fillStyle = hud;
    ctx.fillRect(0, 0, W, HUD_H);
    ctx.strokeStyle = "rgba(120,190,255,0.18)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, HUD_H - 1); ctx.lineTo(W, HUD_H - 1);
    ctx.stroke();

    ctx.textBaseline = "middle";
    ctx.textAlign = "left";

    ctx.fillStyle = "#7f92c1";
    ctx.font = "700 13px ui-sans-serif, system-ui, sans-serif";
    ctx.fillText("SCORE", 34, 28);
    ctx.fillStyle = "#ffffff";
    ctx.font = "800 30px ui-sans-serif, system-ui, sans-serif";
    ctx.fillText(String(score).padStart(6, "0"), 34, 58);

    ctx.fillStyle = "#7f92c1";
    ctx.font = "700 13px ui-sans-serif, system-ui, sans-serif";
    ctx.fillText("BRICKS", 260, 28);
    ctx.fillStyle = "#ffffff";
    ctx.font = "800 30px ui-sans-serif, system-ui, sans-serif";
    ctx.fillText(String(totalBricksAlive()), 260, 58);

    // lives
    ctx.fillStyle = "#7f92c1";
    ctx.font = "700 13px ui-sans-serif, system-ui, sans-serif";
    ctx.fillText("LIVES", W - 220, 28);
    for (let i = 0; i < 3; i++) {
      const cx = W - 208 + i * 38;
      const cy = 58;
      const alive = i < lives;
      ctx.save();
      ctx.globalAlpha = alive ? 1 : 0.22;
      const g = ctx.createLinearGradient(cx - 14, cy - 12, cx + 14, cy + 12);
      g.addColorStop(0, "#ffffff");
      g.addColorStop(1, "#42b7ff");
      ctx.fillStyle = g;
      ctx.shadowColor = "rgba(66,232,255,0.7)";
      ctx.shadowBlur = alive ? 16 : 0;
      ctx.beginPath();
      ctx.arc(cx, cy, 12, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    // center title / power status
    ctx.textAlign = "center";
    ctx.fillStyle = "rgba(170,205,255,0.72)";
    ctx.font = "700 12px ui-sans-serif, system-ui, sans-serif";
    ctx.fillText("NEON BREAKER", W / 2, 26);

    if (paddle.wideTimer > 0) {
      ctx.fillStyle = "#42e8ff";
      ctx.font = "700 12px ui-sans-serif, system-ui, sans-serif";
      ctx.fillText(`WIDE ${paddle.wideTimer.toFixed(1)}s`, W / 2, 56);
    } else {
      ctx.fillStyle = "rgba(150,180,235,0.62)";
      ctx.font = "500 12px ui-sans-serif, system-ui, sans-serif";
      ctx.fillText("MOUSE / ARROWS TO MOVE", W / 2, 56);
    }
  }

  // ---------- End screens ----------
  function showEndScreen(won) {
    overlay.classList.remove("hidden");
    if (won) {
      kicker.textContent = "Victory · wall demolished";
      overlayTitle.textContent = "Perfect clear!";
      overlayText.textContent =
        `You smashed every brick for a final score of ${score}. Try again and chase a higher combo bonus.`;
      controls.innerHTML = `
        <div class="key"><b>Score</b> ${score}</div>
        <div class="key"><b>Lives left</b> ${lives}</div>
        <div class="key"><b>Bricks</b> 0 remaining</div>`;
      hint.textContent = "Press Enter or click below to play again.";
    } else {
      kicker.textContent = "Game over";
      overlayTitle.textContent = "Out of lives";
      overlayText.textContent =
        `The wall got the better of you. Final score: ${score}. Ready for another run?`;
      controls.innerHTML = `
        <div class="key"><b>Score</b> ${score}</div>
        <div class="key"><b>Lives</b> 0</div>
        <div class="key"><b>Bricks left</b> ${totalBricksAlive()}</div>`;
      hint.textContent = "Press Enter or click below to try again.";
    }
    startBtn.textContent = "Restart Game";
    startBtn.focus();
  }

  // ---------- Flow ----------
  function startGame() {
    score = 0;
    lives = 3;
    combo = 0;
    paddle.w = paddle.baseW;
    paddle.wideTimer = 0;
    paddle.x = W / 2;
    drops = [];
    particles = [];
    sparkles = [];
    buildBricks();
    resetBallOnPaddle();
    state = "playing";
    overlay.classList.add("hidden");
    flash = 0.8;
    showMessage("GET READY", 1.2);
  }

  function pauseToggle() {
    if (state === "playing") state = "paused";
    else if (state === "paused") state = "playing";
  }

  // ---------- Input ----------
  window.addEventListener("keydown", (e) => {
    keys[e.key] = true;

    if (["ArrowLeft", "ArrowRight", " ", "Spacebar"].includes(e.key)) {
      e.preventDefault();
    }

    if (state === "demo") {
      // any key takes control
      startGame();
      return;
    }

    if (e.key === " " || e.key === "Spacebar") {
      if (state === "playing") {
        for (const b of balls) if (b.stuck) launchBall(b, rand(-0.12, 0.12));
      }
    }

    if (e.key === "p" || e.key === "P") {
      pauseToggle();
    }

    if (e.key === "Enter") {
      if (state === "gameover" || state === "victory" || state === "paused") {
        startGame();
      } else if (state === "playing") {
        for (const b of balls) if (b.stuck) launchBall(b, rand(-0.12, 0.12));
      }
    }
  }, { passive: false });

  window.addEventListener("keyup", (e) => {
    keys[e.key] = false;
  });

  function pointerToGame(e) {
    const rect = canvas.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * W;
    return x;
  }

  canvas.addEventListener("pointermove", (e) => {
    if (state === "demo") return;
    const x = pointerToGame(e);
    paddle.x = clamp(x, WALL + paddle.w / 2 + 4, W - WALL - paddle.w / 2 - 4);
    mouseActive = true;
  });

  canvas.addEventListener("pointerdown", (e) => {
    if (state === "demo") {
      startGame();
      return;
    }
    if (state === "playing") {
      for (const b of balls) if (b.stuck) launchBall(b, rand(-0.12, 0.12));
    }
  });

  startBtn.addEventListener("click", () => {
    if (state === "demo" || state === "gameover" || state === "victory" || state === "paused") {
      startGame();
    }
  });

  // ---------- Boot ----------
  function boot() {
    buildBricks();
    resetBallOnPaddle();
    // give the demo ball motion immediately
    launchBall(balls[0], -0.18);
    state = "demo";
    kicker.textContent = "Click or press a key to start";
    overlayTitle.textContent = "Break every brick";
    overlayText.textContent =
      "Steer the paddle with your mouse or the arrow keys. The ball bounces at sharper angles when it catches the edge of the paddle — use that to carve through the wall.";
    hint.textContent = "A self-playing demo runs until you take control.";
  }

  function frame(now) {
    if (!lastTime) lastTime = now;
    let dt = (now - lastTime) / 1000;
    lastTime = now;
    if (dt > 0.25) dt = 0.25;

    if (state !== "paused") {
      accumulator += dt;
      let guard = 0;
      while (accumulator >= STEP && guard < 40) {
        update(STEP);
        accumulator -= STEP;
        guard++;
      }
    }

    draw();
    requestAnimationFrame(frame);
  }

  boot();
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>