Models 44

Grok 4.7

by SpaceXAIreasoning defaultClean
Open in new window

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

How it went

Total time
18 min 11 s
Cost
$0.523
Output tokens
86,880
Spent thinking
70,093 tokens
Speed
79.7 tok/s
Console errors
0
First token after
1.3 s
First visible text after
16 min 14 s
Prompt tokens
1,752
Code size
42.7 KB
Finish reason
stop
Served by
xAI
Model id
x-ai/grok-4.7
Reasoning sent
model default
Max output
450,000 tokens
Generated
Nov 12, 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 · 1497 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Neon Breakout</title>
<style>
  :root {
    --bg: #07060e;
    --cyan: #3ef0ff;
    --pink: #ff4d8d;
    --ink: #eef8ff;
    --muted: #8ea3ba;
  }
  * { box-sizing: border-box; }
  html, body {
    margin: 0;
    height: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--ink);
    font-family: "Segoe UI", "Avenir Next", system-ui, sans-serif;
  }
  body {
    background:
      radial-gradient(900px 480px at 15% -10%, rgba(90, 40, 160, 0.45), transparent 60%),
      radial-gradient(700px 420px at 110% 110%, rgba(14, 90, 120, 0.4), transparent 55%),
      var(--bg);
  }
  body::before, body::after {
    content: "";
    position: fixed;
    width: 280px;
    height: 280px;
    border-radius: 50%;
    filter: blur(50px);
    pointer-events: none;
    z-index: 0;
  }
  body::before { background: rgba(62, 240, 255, 0.09); top: -80px; left: -70px; }
  body::after { background: rgba(255, 77, 141, 0.1); right: -70px; bottom: -90px; }
  #app {
    position: relative;
    z-index: 1;
    height: 100%;
    max-height: 100%;
    display: flex;
    flex-direction: column;
    padding: 12px 16px 10px;
    gap: 8px;
    overflow: hidden;
    user-select: none;
    -webkit-user-select: none;
  }
  header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    flex-shrink: 0;
  }
  h1 {
    margin: 0;
    font-size: clamp(18px, 2.5vw, 28px);
    letter-spacing: 0.14em;
    font-weight: 800;
    color: #f5fbff;
    text-shadow: 0 0 18px rgba(62, 240, 255, 0.35);
  }
  h1 span { color: var(--cyan); text-shadow: 0 0 14px rgba(62, 240, 255, 0.75); }
  .tag { margin: 2px 0 0; color: var(--muted); font-size: 12px; letter-spacing: 0.08em; }
  .stats { display: flex; gap: 8px; flex-wrap: wrap; max-width: 100%; }
  .stat {
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 12px;
    padding: 6px 12px 8px;
    min-width: 0;
    flex: 1 1 96px;
  }
  .stat span {
    display: block;
    font-size: 10px;
    letter-spacing: 0.16em;
    color: var(--muted);
  }
  .stat strong {
    display: block;
    font-size: 20px;
    font-weight: 750;
    font-variant-numeric: tabular-nums;
    min-height: 24px;
    line-height: 24px;
  }
  .stat.power strong { font-size: 13px; letter-spacing: 0.03em; color: #bff8ff; }
  .lives { display: flex; gap: 3px; align-items: center; height: 24px; }
  .bar {
    margin-top: 4px;
    height: 4px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.08);
    overflow: hidden;
  }
  #powerFill {
    height: 100%;
    width: 0%;
    border-radius: 4px;
    background: linear-gradient(90deg, #3ef0ff, #7dffb3);
  }
  @keyframes pop {
    0% { transform: scale(1.16); color: #fff; }
    100% { transform: scale(1); }
  }
  .stat strong.pop { animation: pop 0.28s ease; }
  #stage {
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  #frame {
    position: relative;
    border-radius: 14px;
    overflow: hidden;
    box-shadow:
      0 0 0 1px rgba(126, 246, 255, 0.38),
      0 0 0 6px rgba(8, 10, 18, 0.75),
      0 18px 50px rgba(0, 0, 0, 0.45),
      0 0 42px rgba(62, 240, 255, 0.1);
    touch-action: none;
    max-width: 100%;
  }
  canvas { display: block; width: 100%; height: 100%; touch-action: none; }
  #startOverlay, #endOverlay {
    position: absolute;
    inset: 0;
    display: flex;
    justify-content: center;
  }
  #startOverlay {
    align-items: flex-end;
    padding: 0 16px 7%;
    pointer-events: none;
    background: linear-gradient(to top, rgba(5, 6, 14, 0.55), rgba(5, 6, 14, 0.05) 42%, transparent 58%);
  }
  .banner, .end-card {
    text-align: center;
    background: rgba(8, 10, 20, 0.82);
    border: 1px solid rgba(62, 240, 255, 0.5);
    border-radius: 16px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4), 0 0 24px rgba(62, 240, 255, 0.12);
  }
  .banner { padding: 14px 22px 12px; max-width: min(520px, 94%); }
  .banner h2, .end-card h2 { margin: 0; font-weight: 800; letter-spacing: 0.02em; }
  .banner h2 { font-size: clamp(16px, 2.4vw, 24px); color: #fff; }
  .eyebrow {
    margin: 0 0 4px;
    font-size: 11px;
    letter-spacing: 0.22em;
    color: var(--cyan);
    font-weight: 700;
  }
  .banner p, .hint { margin: 6px 0 0; color: #b7c9da; font-size: 13px; }
  @keyframes pulse {
    0%, 100% { border-color: rgba(62, 240, 255, 0.5); }
    50% { border-color: rgba(255, 77, 141, 0.7); }
  }
  .banner { animation: pulse 2.4s ease-in-out infinite; }
  #endOverlay {
    align-items: center;
    background: rgba(5, 6, 14, 0.66);
    z-index: 3;
  }
  #endOverlay.hidden, #startOverlay.hidden { display: none; }
  .end-card { width: min(400px, 90%); padding: 22px 26px 16px; }
  .end-card h2 { font-size: clamp(28px, 5vw, 40px); letter-spacing: 0.12em; color: var(--pink); }
  #endOverlay.victory h2 { color: #ffd166; text-shadow: 0 0 18px rgba(255, 209, 102, 0.45); }
  #endOverlay.victory { --accent: #ffd166; }
  .end-card p { color: #c5d4e4; }
  .end-score { margin: 14px 0; }
  .end-score span { display: block; letter-spacing: 0.18em; font-size: 11px; color: var(--muted); }
  .end-score strong { font-size: 32px; font-variant-numeric: tabular-nums; }
  button {
    font: inherit;
    cursor: pointer;
    border: none;
    background: linear-gradient(90deg, #3ef0ff, #8eb6ff);
    color: #071018;
    font-weight: 800;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    padding: 12px 22px;
    border-radius: 999px;
    min-width: 160px;
  }
  button:hover { filter: brightness(1.08); }
  button:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
  .hint { font-size: 12px; color: var(--muted); }
  footer {
    flex-shrink: 0;
    display: flex;
    justify-content: center;
    gap: 14px;
    flex-wrap: wrap;
    font-size: 12px;
    color: var(--muted);
    letter-spacing: 0.02em;
  }
  footer b { color: #d5e7f6; font-weight: 650; }
  @media (max-width: 520px) {
    #app { padding: 8px; gap: 6px; }
    .tag { display: none; }
    h1 { letter-spacing: 0.08em; }
    .stat { padding: 4px 8px; }
    .stat strong { font-size: 16px; }
    .stat.power strong { font-size: 11px; }
    footer span:nth-child(n+3) { display: none; }
    #startOverlay { align-items: center; padding: 8px; background: none; }
    .banner { padding: 10px 12px; }
    .banner .eyebrow { display: none; }
    .banner p { font-size: 11px; }
  }
</style>
</head>
<body>
<div id="app">
  <header>
    <div class="brand">
      <h1><span>NEON</span> BREAKOUT</h1>
      <p class="tag">Wave 01 · Brick Fortress</p>
    </div>
    <div class="stats">
      <div class="stat">
        <span>SCORE</span>
        <strong id="score">0</strong>
      </div>
      <div class="stat">
        <span>LIVES</span>
        <div id="lives" class="lives" aria-label="3 lives"></div>
      </div>
      <div class="stat power">
        <span>POWER</span>
        <strong id="powerText">—</strong>
        <div class="bar"><div id="powerFill"></div></div>
      </div>
    </div>
  </header>
  <div id="stage">
    <div id="frame">
      <canvas id="c" width="960" height="600"></canvas>
      <div id="startOverlay">
        <div class="banner">
          <p class="eyebrow">DEMO</p>
          <h2>Click or press a key to start</h2>
          <p>Mouse or arrow keys move the paddle · Space or click launches</p>
        </div>
      </div>
      <div id="endOverlay" class="hidden">
        <div class="end-card">
          <p class="eyebrow" id="endEyebrow">NO LIVES LEFT</p>
          <h2 id="endTitle">GAME OVER</h2>
          <p id="endSub">The ball slipped past the paddle.</p>
          <div class="end-score"><span>SCORE</span><strong id="endScore">0</strong></div>
          <button type="button" id="restartBtn">Play Again</button>
          <p class="hint">Press any key or click to restart</p>
        </div>
      </div>
    </div>
  </div>
  <footer>
    <span><b>Move</b> mouse or ← →</span>
    <span><b>Edges</b> send the ball at a sharp angle</span>
    <span><b>2× bricks</b> crack, then break</span>
    <span><b>Capsules</b> widen the paddle or split into 3 balls</span>
  </footer>
</div>
<script>
'use strict';

const BASE_W = 960;
const BASE_H = 600;
const WALL = 5;
const COLS = 10;
const ROWS = 7;
const GAP = 5;
const SIDE = 18;
const TOP = 16;
const BRICK_H = 26;
const PADDLE_BASE = 118;
const PADDLE_WIDE = 204;
const PADDLE_H = 14;
const BALL_R = 9;
const BALL_SPEED = 442;
const ROW_HUES = [348, 14, 38, 152, 190, 216, 274];
const POWER_AT = {
  '1,0': 'wide',
  '7,0': 'multi',
  '4,1': 'wide',
  '8,2': 'multi',
  '2,3': 'wide',
  '6,3': 'multi',
  '0,4': 'wide',
  '9,5': 'multi',
  '3,5': 'wide',
  '4,6': 'wide',
  '5,6': 'multi',
  '8,6': 'wide'
};

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const stage = document.getElementById('stage');
const frame = document.getElementById('frame');
const startOverlay = document.getElementById('startOverlay');
const endOverlay = document.getElementById('endOverlay');
const endEyebrow = document.getElementById('endEyebrow');
const endTitle = document.getElementById('endTitle');
const endSub = document.getElementById('endSub');
const endScore = document.getElementById('endScore');
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const powerText = document.getElementById('powerText');
const powerFill = document.getElementById('powerFill');

const keys = { left: false, right: false };
const mouse = { x: BASE_W / 2, y: BASE_H / 2, inside: false };

let mode = 'demo';
let phase = 'playing';
let score = 0;
let lives = 3;
let wideTimer = 0;
let time = 0;
let shake = 0;
let hurtFlash = 0;
let lifeTimer = 0;
let demoHold = 0;
let lastInput = 'mouse';
let hudScore = -1;
let hudLives = -1;
let hudPower = '';
let brickBottom = 240;
let ballSerial = 0;
let lastDW = 0;
let lastDH = 0;
let lastDPR = 0;

const paddle = { x: 0, y: BASE_H - 46, w: PADDLE_BASE, h: PADDLE_H, vx: 0, flash: 0, hitT: 0 };
let bricks = [];
let balls = [];
let powerups = [];
let particles = [];
let floaters = [];

const stars = [];
for (let i = 0; i < 70; i++) {
  stars.push({
    x: Math.random() * BASE_W,
    y: Math.random() * BASE_H,
    r: Math.random() * 1.3 + 0.3,
    p: Math.random() * Math.PI * 2,
    s: 0.6 + Math.random() * 1.8
  });
}

function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function formatScore(n) { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }

function roundRect(x, y, w, h, r) {
  const rr = Math.max(0, Math.min(r, w / 2, h / 2));
  ctx.beginPath();
  ctx.moveTo(x + rr, y);
  ctx.arcTo(x + w, y, x + w, y + h, rr);
  ctx.arcTo(x + w, y + h, x, y + h, rr);
  ctx.arcTo(x, y + h, x, y, rr);
  ctx.arcTo(x, y, x + w, y, rr);
  ctx.closePath();
}

function heartSVG(on) {
  const c = on ? '#ff4d7a' : '#3c3144';
  const glow = on ? 'filter:drop-shadow(0 0 4px #ff4d7a)' : '';
  return '<svg width="18" height="16" viewBox="0 0 18 16" aria-hidden="true" style="' + glow + '"><path fill="' + c + '" d="M9 15.2C9 15.2 1.2 10.4 1.2 6.1 1.2 3.6 3.1 1.8 5.5 1.8c1.4 0 2.6.7 3.5 1.8.9-1.1 2.1-1.8 3.5-1.8 2.4 0 4.3 1.8 4.3 4.3 0 4.3-7.8 9.1-7.8 9.1z"/></svg>';
}

function renderLives() {
  let html = '';
  for (let i = 0; i < 3; i++) html += heartSVG(i < lives);
  livesEl.innerHTML = html;
  livesEl.setAttribute('aria-label', lives + (lives === 1 ? ' life' : ' lives'));
}

function updateHUD() {
  if (score !== hudScore) {
    const up = score > hudScore && hudScore >= 0;
    hudScore = score;
    scoreEl.textContent = formatScore(score);
    if (up) {
      scoreEl.classList.remove('pop');
      void scoreEl.offsetWidth;
      scoreEl.classList.add('pop');
    }
  }
  if (lives !== hudLives) {
    hudLives = lives;
    renderLives();
  }
  const aliveN = balls.reduce(function (n, b) { return n + (b.dead ? 0 : 1); }, 0);
  let label = '—';
  if (wideTimer > 0 && aliveN > 1) label = 'WIDE ' + wideTimer.toFixed(1) + 's · ×' + aliveN;
  else if (wideTimer > 0) label = 'WIDE ' + wideTimer.toFixed(1) + 's';
  else if (aliveN > 1) label = 'BALLS ×' + aliveN;
  if (label !== hudPower) {
    hudPower = label;
    powerText.textContent = label;
  }
  if (wideTimer > 0) {
    powerFill.style.width = (wideTimer / 10 * 100) + '%';
    powerFill.style.background = 'linear-gradient(90deg,#3ef0ff,#7dffb3)';
  } else if (aliveN > 1) {
    powerFill.style.width = '100%';
    powerFill.style.background = 'linear-gradient(90deg,#ff4d8d,#ffd166)';
  } else {
    powerFill.style.width = '0%';
  }
}

function isTough(c, r) {
  if (r <= 1) return true;
  if (r === 2 && c % 2 === 0) return true;
  if (r === 6 && c >= 3 && c <= 5) return true;
  return false;
}

function buildBricks() {
  const list = [];
  const brickW = (BASE_W - SIDE * 2 - GAP * (COLS - 1)) / COLS;
  brickBottom = 0;
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      const tough = isTough(c, r);
      const hue = ROW_HUES[r];
      const light = 52 + (c % 3) * 3;
      const x = SIDE + c * (brickW + GAP);
      const y = TOP + r * (BRICK_H + GAP);
      const key = c + ',' + r;
      list.push({
        x: x, y: y, w: brickW, h: BRICK_H,
        col: c, row: r,
        hits: tough ? 2 : 1,
        maxHits: tough ? 2 : 1,
        alive: true,
        power: POWER_AT[key] || null,
        color: 'hsl(' + hue + ',84%,' + light + '%)',
        colorDim: 'hsl(' + hue + ',42%,34%)',
        hue: hue,
        flash: 0,
        cooldown: 0
      });
      brickBottom = Math.max(brickBottom, y + BRICK_H);
    }
  }
  return list;
}

function makeBall(x, y, speed) {
  ballSerial += 1;
  return {
    id: ballSerial,
    x: x, y: y,
    vx: 0, vy: 0,
    r: BALL_R,
    speed: speed || BALL_SPEED,
    dead: false,
    trail: [],
    hue: ballSerial % 3
  };
}

function aim(ball, vx, vy) {
  const s = Math.hypot(vx, vy) || 1;
  ball.vx = vx / s * ball.speed;
  ball.vy = vy / s * ball.speed;
}

function resetGame(autoLaunch) {
  score = 0;
  lives = 3;
  wideTimer = 0;
  shake = 0;
  hurtFlash = 0;
  lifeTimer = 0;
  particles = [];
  powerups = [];
  floaters = [];
  bricks = buildBricks();
  paddle.w = PADDLE_BASE;
  paddle.h = PADDLE_H;
  paddle.y = BASE_H - 46;
  paddle.x = (BASE_W - paddle.w) / 2;
  paddle.vx = 0;
  paddle.flash = 0;
  if (autoLaunch) {
    const target = bricks.find(function (b) { return b.col === 4 && b.row === 6; });
    const tx = target ? target.x + target.w / 2 : BASE_W * 0.48;
    const b = makeBall(tx - 14, (target ? target.y + target.h : 220) + 78, BALL_SPEED);
    aim(b, 0.22, -1);
    balls = [b];
    phase = 'playing';
    paddle.x = clamp(b.x - paddle.w / 2, WALL, BASE_W - WALL - paddle.w);
  } else {
    balls = [makeBall(BASE_W / 2, paddle.y - BALL_R - 2, BALL_SPEED)];
    phase = 'ready';
  }
  updateHUD();
}

function burst(x, y, color, count, power) {
  for (let i = 0; i < count; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = power * (0.25 + Math.random() * 0.85);
    particles.push({
      type: 'bit',
      x: x, y: y,
      vx: Math.cos(a) * s,
      vy: Math.sin(a) * s - 30,
      life: 0.45 + Math.random() * 0.55,
      age: 0,
      size: 2 + Math.random() * 3.2,
      color: color,
      g: 260 + Math.random() * 220,
      rot: Math.random() * 6,
      spin: (Math.random() - 0.5) * 10
    });
  }
  particles.push({ type: 'ring', x: x, y: y, age: 0, life: 0.38, color: color });
  if (particles.length > 320) particles.splice(0, particles.length - 320);
}

function spawnFloater(text, x, y, color) {
  floaters.push({ text: text, x: x, y: y, color: color, age: 0, life: 0.85 });
  if (floaters.length > 24) floaters.splice(0, floaters.length - 24);
}

function dropPower(brick) {
  powerups.push({
    x: brick.x + brick.w / 2 - 26,
    y: brick.y + brick.h / 2 - 8,
    w: 52,
    h: 22,
    vy: 128,
    type: brick.power,
    phase: Math.random() * Math.PI * 2,
    caught: false
  });
}

function damageBrick(brick, ball) {
  if (!brick.alive || brick.cooldown > 0) return;
  brick.cooldown = 0.09;
  brick.hits -= 1;
  brick.flash = 1;
  const cx = brick.x + brick.w / 2;
  const cy = brick.y + brick.h / 2;
  if (brick.hits > 0) {
    score += 50;
    burst(cx, cy, brick.color, 10, 150);
    spawnFloater('+50', cx, brick.y, '#ffffff');
    shake = Math.min(6, shake + 2.4);
    return;
  }
  brick.alive = false;
  const pts = brick.maxHits === 2 ? 150 : 100;
  score += pts;
  burst(cx, cy, brick.color, 20, 240);
  burst(cx, cy, '#ffffff', 8, 180);
  spawnFloater('+' + pts, cx, brick.y, '#ffffff');
  shake = Math.min(9, shake + 4.5);
  if (ball) ball.speed = Math.min(530, ball.speed + 7);
  if (brick.power) dropPower(brick);
}

function enforceSpeed(ball) {
  let s = Math.hypot(ball.vx, ball.vy);
  if (!Number.isFinite(s) || s < 1) {
    ball.vx = 80;
    ball.vy = -ball.speed;
    s = Math.hypot(ball.vx, ball.vy);
  }
  if (Math.abs(ball.vy) < 78) {
    ball.vy = ball.vy < 0 ? -78 : 78;
    s = Math.hypot(ball.vx, ball.vy);
  }
  const target = clamp(ball.speed, 380, 530);
  ball.speed = target;
  ball.vx = ball.vx / s * target;
  ball.vy = ball.vy / s * target;
}

function circleRectSep(cx, cy, r, rx, ry, rw, rh) {
  const nearestX = clamp(cx, rx, rx + rw);
  const nearestY = clamp(cy, ry, ry + rh);
  let dx = cx - nearestX;
  let dy = cy - nearestY;
  const distSq = dx * dx + dy * dy;
  if (distSq > r * r) return null;
  if (distSq < 1e-8) {
    const left = cx - rx;
    const right = rx + rw - cx;
    const top = cy - ry;
    const bottom = ry + rh - cy;
    const min = Math.min(left, right, top, bottom);
    if (min === left) return { pen: left + r, nx: -1, ny: 0 };
    if (min === right) return { pen: right + r, nx: 1, ny: 0 };
    if (min === top) return { pen: top + r, nx: 0, ny: -1 };
    return { pen: bottom + r, nx: 0, ny: 1 };
  }
  const dist = Math.sqrt(distSq);
  return { pen: r - dist, nx: dx / dist, ny: dy / dist };
}

function clampWalls(ball) {
  const left = WALL + ball.r;
  const right = BASE_W - WALL - ball.r;
  const top = WALL + ball.r;
  if (ball.x < left) { ball.x = left; ball.vx = Math.abs(ball.vx); }
  else if (ball.x > right) { ball.x = right; ball.vx = -Math.abs(ball.vx); }
  if (ball.y < top) { ball.y = top; ball.vy = Math.abs(ball.vy); }
}

function bounceFromPaddle(ball) {
  const half = paddle.w / 2;
  const offset = clamp((ball.x - (paddle.x + half)) / half, -1, 1);
  const ang = offset * 1.18;
  const spd = clamp(ball.speed, 380, 530);
  ball.speed = spd;
  ball.vx = Math.sin(ang) * spd;
  ball.vy = -Math.cos(ang) * spd;
  ball.y = paddle.y - ball.r - 0.7;
  paddle.flash = 1;
  paddle.hitT = offset;
  burst(ball.x, paddle.y + 2, '#c8fbff', 7, 130);
}

function resolvePaddle(ball, x0, y0) {
  if (ball.dead) return false;
  if (phase !== 'playing') return false;
  const surface = paddle.y;
  if (y0 <= surface && ball.vy > 0) {
    const prevBottom = y0 + ball.r;
    const curBottom = ball.y + ball.r;
    if (prevBottom <= surface + 2 && curBottom >= surface - 0.5) {
      const denom = (ball.y - y0) || 0.0001;
      const t = clamp((surface - ball.r - y0) / denom, 0, 1);
      const cx = x0 + (ball.x - x0) * t;
      if (cx >= paddle.x - ball.r * 0.25 && cx <= paddle.x + paddle.w + ball.r * 0.25) {
        ball.x = cx;
        bounceFromPaddle(ball);
        return true;
      }
    }
  }
  if (ball.y > paddle.y + paddle.h + ball.r * 0.2) return false;
  const info = circleRectSep(ball.x, ball.y, ball.r, paddle.x, paddle.y, paddle.w, paddle.h);
  if (!info) return false;
  if (info.ny > 0.4 && ball.y > paddle.y + paddle.h * 0.55) return false;
  ball.x += info.nx * (info.pen + 0.2);
  bounceFromPaddle(ball);
  return true;
}

function resolveBricks(ball) {
  for (let iter = 0; iter < 6; iter++) {
    let hit = null;
    let best = 0;
    for (let i = 0; i < bricks.length; i++) {
      const brick = bricks[i];
      if (!brick.alive) continue;
      const info = circleRectSep(ball.x, ball.y, ball.r, brick.x, brick.y, brick.w, brick.h);
      if (info && info.pen > best) {
        best = info.pen;
        hit = { brick: brick, nx: info.nx, ny: info.ny, pen: info.pen };
      }
    }
    if (!hit) return;
    ball.x += hit.nx * (hit.pen + 0.2);
    ball.y += hit.ny * (hit.pen + 0.2);
    const dot = ball.vx * hit.nx + ball.vy * hit.ny;
    if (dot < 0) {
      ball.vx -= 2 * dot * hit.nx;
      ball.vy -= 2 * dot * hit.ny;
    }
    enforceSpeed(ball);
    damageBrick(hit.brick, ball);
  }
}

function stepBall(ball, dt) {
  if (ball.dead) return;
  const speed = Math.hypot(ball.vx, ball.vy) || ball.speed;
  const steps = Math.max(1, Math.ceil((speed * dt) / 4));
  const sdt = dt / steps;
  for (let i = 0; i < steps; i++) {
    const x0 = ball.x;
    const y0 = ball.y;
    ball.x += ball.vx * sdt;
    ball.y += ball.vy * sdt;
    clampWalls(ball);
    resolvePaddle(ball, x0, y0);
    resolveBricks(ball);
    clampWalls(ball);
    if (ball.y - ball.r > BASE_H + 4) {
      ball.dead = true;
      burst(clamp(ball.x, 20, BASE_W - 20), BASE_H - 8, '#ff5d7a', 14, 200);
      return;
    }
  }
  resolveBricks(ball);
  if (ball.vy > 0) resolvePaddle(ball, ball.x, ball.y - 3);
  clampWalls(ball);
  enforceSpeed(ball);
}

function predictX(ball, targetY) {
  let x = ball.x;
  let y = ball.y;
  let vx = ball.vx;
  let vy = ball.vy;
  const left = WALL + ball.r;
  const right = BASE_W - WALL - ball.r;
  for (let i = 0; i < 40; i++) {
    if (vy <= 1) return x;
    const tY = (targetY - y) / vy;
    if (tY <= 0) return x;
    let tW = Infinity;
    if (vx > 1) tW = (right - x) / vx;
    else if (vx < -1) tW = (left - x) / vx;
    if (tW > 0 && tW < tY) {
      x += vx * tW;
      y += vy * tW;
      vx = -vx;
      x = clamp(x, left, right);
    } else {
      return clamp(x + vx * tY, left, right);
    }
  }
  return clamp(x, left, right);
}

function aiTargetX() {
  let target = null;
  let urgency = -1;
  for (let i = 0; i < powerups.length; i++) {
    const pu = powerups[i];
    const cy = pu.y + pu.h;
    if (cy > urgency) {
      urgency = cy;
      target = pu.x + pu.w / 2;
    }
  }
  for (let i = 0; i < balls.length; i++) {
    const b = balls[i];
    if (b.dead || b.vy <= 40) continue;
    if (b.y > urgency) {
      urgency = b.y;
      target = b.y > brickBottom - 8 ? predictX(b, paddle.y - b.r) : b.x;
    }
  }
  if (target == null) {
    let lowest = null;
    for (let i = 0; i < balls.length; i++) {
      const b = balls[i];
      if (b.dead) continue;
      if (!lowest || b.y > lowest.y) lowest = b;
    }
    return lowest ? lowest.x : BASE_W / 2;
  }
  return target;
}

function updatePaddle(dt) {
  const oldX = paddle.x;
  let center = paddle.x + paddle.w / 2;
  if (mode === 'demo') {
    const tx = aiTargetX();
    const diff = tx - center;
    const maxStep = 1080 * dt;
    center += clamp(diff, -maxStep, maxStep);
  } else if (lastInput === 'keys') {
    const dir = (keys.right ? 1 : 0) - (keys.left ? 1 : 0);
    center += dir * 740 * dt;
  } else {
    center = mouse.x;
  }
  const targetW = wideTimer > 0 ? PADDLE_WIDE : PADDLE_BASE;
  paddle.w += (targetW - paddle.w) * Math.min(1, dt * 9);
  center = clamp(center, WALL + paddle.w / 2, BASE_W - WALL - paddle.w / 2);
  paddle.x = center - paddle.w / 2;
  paddle.vx = clamp((paddle.x - oldX) / Math.max(dt, 0.001), -2200, 2200);
  if (paddle.flash > 0) paddle.flash = Math.max(0, paddle.flash - dt * 3.2);
}

function stickBall() {
  const b = balls[0];
  if (!b) return;
  b.dead = false;
  b.x = paddle.x + paddle.w / 2;
  b.y = paddle.y - b.r - 1.6;
  b.vx = 0;
  b.vy = 0;
  b.trail.length = 0;
}

function launch() {
  if (phase !== 'ready') return;
  const b = balls.find(function (bb) { return !bb.dead; }) || balls[0];
  if (!b) return;
  const influence = clamp((paddle.vx || 0) / 1100, -0.55, 0.55);
  b.speed = BALL_SPEED;
  b.vx = Math.sin(influence) * b.speed;
  b.vy = -Math.cos(influence) * b.speed;
  b.dead = false;
  phase = 'playing';
}

function spawnMulti() {
  let src = balls.find(function (b) { return !b.dead; });
  if (!src) return;
  if (phase === 'ready') launch();
  src = balls.find(function (b) { return !b.dead; }) || src;
  const spd = src.speed || BALL_SPEED;
  let base = Math.atan2(src.vy, src.vx);
  if (!Number.isFinite(base) || (src.vx === 0 && src.vy === 0)) base = -Math.PI / 2;
  let added = 0;
  while (added < 2 && balls.filter(function (b) { return !b.dead; }).length < 6) {
    const ang = base + (added === 0 ? -0.5 : 0.46);
    let vx = Math.cos(ang) * spd;
    let vy = Math.sin(ang) * spd;
    if (Math.abs(vy) < 90) vy = vy < 0 ? -90 : -90;
    const nb = makeBall(src.x, src.y - 2, spd);
    nb.vx = vx;
    nb.vy = vy;
    enforceSpeed(nb);
    balls.push(nb);
    added += 1;
  }
}

function applyPower(type) {
  const cx = paddle.x + paddle.w / 2;
  if (type === 'wide') {
    wideTimer = 10;
    score += 200;
    burst(cx, paddle.y, '#3ef0ff', 18, 240);
    spawnFloater('WIDE PADDLE', cx, paddle.y - 28, '#7ef6ff');
  } else {
    spawnMulti();
    score += 200;
    burst(cx, paddle.y, '#ffd166', 18, 240);
    spawnFloater('MULTI-BALL', cx, paddle.y - 28, '#ffd166');
  }
}

function updatePowerups(dt) {
  for (let i = 0; i < powerups.length; i++) {
    const pu = powerups[i];
    pu.y += pu.vy * dt;
    pu.vy = Math.min(250, pu.vy + 28 * dt);
    pu.x += Math.sin(time * 3.2 + pu.phase) * 18 * dt;
    pu.x = clamp(pu.x, WALL, BASE_W - WALL - pu.w);
    const canCatch = phase === 'playing' || phase === 'lifelost' || phase === 'ready';
    if (canCatch &&
        pu.x < paddle.x + paddle.w &&
        pu.x + pu.w > paddle.x &&
        pu.y < paddle.y + paddle.h &&
        pu.y + pu.h > paddle.y - 6) {
      pu.caught = true;
      applyPower(pu.type);
    }
  }
  powerups = powerups.filter(function (p) { return !p.caught && p.y < BASE_H + 30; });
}

function loseLife() {
  if (phase !== 'playing') return;
  lives -= 1;
  hurtFlash = 1;
  balls.forEach(function (b) { b.dead = true; });
  if (lives <= 0) {
    phase = 'gameover';
    if (mode === 'player') showEnd(false);
    else demoHold = 1.15;
  } else {
    phase = 'lifelost';
    lifeTimer = 0.72;
  }
  updateHUD();
}

function respawnBall() {
  balls = [makeBall(paddle.x + paddle.w / 2, paddle.y - BALL_R - 2, BALL_SPEED)];
  phase = 'ready';
  if (mode === 'demo') launch();
}

function onVictory() {
  if (phase === 'victory') return;
  phase = 'victory';
  score += lives * 250;
  for (let i = 0; i < 70; i++) {
    particles.push({
      type: 'bit',
      x: Math.random() * BASE_W,
      y: -10 - Math.random() * 120,
      vx: (Math.random() - 0.5) * 90,
      vy: 70 + Math.random() * 160,
      life: 1.5 + Math.random() * 0.6,
      age: 0,
      size: 3 + Math.random() * 3,
      color: ['#3ef0ff', '#ff4d8d', '#ffd166', '#7dffb3', '#ffffff'][i % 5],
      g: 40,
      rot: Math.random() * 6,
      spin: (Math.random() - 0.5) * 6
    });
  }
  if (mode === 'player') showEnd(true);
  else demoHold = 1.5;
  updateHUD();
}

function showEnd(victory) {
  endOverlay.classList.remove('hidden');
  endOverlay.classList.toggle('victory', victory);
  endEyebrow.textContent = victory ? 'WALL CLEARED' : 'NO LIVES LEFT';
  endTitle.textContent = victory ? 'VICTORY' : 'GAME OVER';
  endSub.textContent = victory
    ? 'Cleared with ' + lives + (lives === 1 ? ' life' : ' lives') + ' remaining.'
    : 'The ball slipped past the paddle.';
  endScore.textContent = formatScore(score);
}

function update(dt) {
  time += dt;
  if (wideTimer > 0) wideTimer = Math.max(0, wideTimer - dt);
  if (hurtFlash > 0) hurtFlash = Math.max(0, hurtFlash - dt * 1.4);
  if (shake > 0) shake *= Math.pow(0.04, dt);

  if (mode === 'demo' && demoHold > 0) {
    demoHold -= dt;
    if (demoHold <= 0) resetGame(true);
  }
  if (mode === 'demo' && phase === 'ready') launch();

  updatePaddle(dt);

  if (phase === 'ready') stickBall();

  if (phase === 'playing') {
    for (let i = 0; i < balls.length; i++) {
      const b = balls[i];
      if (!b.dead) stepBall(b, dt);
      if (!b.dead) {
        b.trail.push({ x: b.x, y: b.y });
        if (b.trail.length > 12) b.trail.shift();
      }
    }
    const anyAlive = balls.some(function (b) { return !b.dead; });
    if (!anyAlive) loseLife();
    else if (bricks.length && bricks.every(function (b) { return !b.alive; })) onVictory();
  }

  if (phase === 'lifelost') {
    lifeTimer -= dt;
    if (lifeTimer <= 0) respawnBall();
  }

  updatePowerups(dt);

  for (let i = 0; i < bricks.length; i++) {
    const b = bricks[i];
    if (b.flash > 0) b.flash = Math.max(0, b.flash - dt * 4);
    if (b.cooldown > 0) b.cooldown = Math.max(0, b.cooldown - dt);
  }

  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    p.age += dt;
    if (p.type === 'bit') {
      p.vy += p.g * dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.rot += p.spin * dt;
    }
  }
  particles = particles.filter(function (p) { return p.age < p.life; });

  for (let i = 0; i < floaters.length; i++) {
    const f = floaters[i];
    f.age += dt;
    f.y -= 36 * dt;
  }
  floaters = floaters.filter(function (f) { return f.age < f.life; });

  updateHUD();
}

function drawBackground() {
  const g = ctx.createLinearGradient(0, 0, 0, BASE_H);
  g.addColorStop(0, '#140a22');
  g.addColorStop(0.45, '#0b0d18');
  g.addColorStop(1, '#071018');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, BASE_W, BASE_H);

  ctx.fillStyle = 'rgba(130, 180, 255, 0.045)';
  for (let y = 10; y < BASE_H; y += 22) {
    for (let x = 10; x < BASE_W; x += 22) ctx.fillRect(x, y, 1.4, 1.4);
  }
  for (let i = 0; i < stars.length; i++) {
    const s = stars[i];
    const a = 0.25 + 0.55 * (0.5 + 0.5 * Math.sin(time * s.s + s.p));
    ctx.globalAlpha = a;
    ctx.fillStyle = '#dff6ff';
    ctx.beginPath();
    ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.globalAlpha = 1;

  const sheenX = ((time * 70) % (BASE_W + 260)) - 130;
  const sg = ctx.createLinearGradient(sheenX, 0, sheenX + 90, 0);
  sg.addColorStop(0, 'rgba(255,255,255,0)');
  sg.addColorStop(0.5, 'rgba(255,255,255,0.05)');
  sg.addColorStop(1, 'rgba(255,255,255,0)');
  ctx.fillStyle = sg;
  ctx.fillRect(0, 0, BASE_W, brickBottom + 20);
}

function drawRails() {
  const g = ctx.createLinearGradient(0, 0, 0, BASE_H);
  g.addColorStop(0, 'rgba(62,240,255,0.15)');
  g.addColorStop(0.5, 'rgba(62,240,255,0.7)');
  g.addColorStop(1, 'rgba(255,77,141,0.55)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 4, BASE_H);
  ctx.fillRect(BASE_W - 4, 0, 4, BASE_H);
  ctx.fillStyle = 'rgba(62,240,255,0.55)';
  ctx.fillRect(0, 0, BASE_W, 3);
  const danger = ctx.createLinearGradient(0, BASE_H - 28, 0, BASE_H);
  danger.addColorStop(0, 'rgba(255,60,100,0)');
  danger.addColorStop(1, 'rgba(255,60,100,0.28)');
  ctx.fillStyle = danger;
  ctx.fillRect(0, BASE_H - 28, BASE_W, 28);
}

function drawCracks(b) {
  ctx.save();
  ctx.beginPath();
  ctx.rect(b.x + 2, b.y + 2, b.w - 4, b.h - 4);
  ctx.clip();
  ctx.lineWidth = 1.7;
  ctx.lineJoin = 'round';
  ctx.strokeStyle = 'rgba(0,0,0,0.55)';
  ctx.beginPath();
  ctx.moveTo(b.x + b.w * 0.18, b.y + 2);
  ctx.lineTo(b.x + b.w * 0.42, b.y + b.h * 0.48);
  ctx.lineTo(b.x + b.w * 0.28, b.y + b.h - 2);
  ctx.moveTo(b.x + b.w * 0.42, b.y + b.h * 0.48);
  ctx.lineTo(b.x + b.w * 0.68, b.y + b.h * 0.3);
  ctx.lineTo(b.x + b.w * 0.84, b.y + b.h - 3);
  ctx.moveTo(b.x + b.w * 0.68, b.y + b.h * 0.3);
  ctx.lineTo(b.x + b.w * 0.74, b.y + 3);
  ctx.stroke();
  ctx.strokeStyle = 'rgba(255,255,255,0.85)';
  ctx.translate(-1, -1);
  ctx.stroke();
  ctx.restore();
}

function drawPowerIcon(cx, cy, type, scale) {
  ctx.save();
  ctx.translate(cx, cy);
  ctx.scale(scale, scale);
  ctx.beginPath();
  ctx.arc(0, 0, 8, 0, Math.PI * 2);
  ctx.fillStyle = 'rgba(5,8,16,0.78)';
  ctx.fill();
  ctx.lineWidth = 1.4;
  ctx.strokeStyle = type === 'wide' ? '#7ef6ff' : '#ffd56a';
  ctx.stroke();
  ctx.strokeStyle = '#ffffff';
  ctx.fillStyle = '#ffffff';
  ctx.lineWidth = 1.6;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  if (type === 'wide') {
    ctx.beginPath();
    ctx.moveTo(-5.5, 0);
    ctx.lineTo(5.5, 0);
    ctx.moveTo(-5.5, 0);
    ctx.lineTo(-2.4, -2.7);
    ctx.moveTo(-5.5, 0);
    ctx.lineTo(-2.4, 2.7);
    ctx.moveTo(5.5, 0);
    ctx.lineTo(2.4, -2.7);
    ctx.moveTo(5.5, 0);
    ctx.lineTo(2.4, 2.7);
    ctx.stroke();
  } else {
    for (let i = -1; i <= 1; i++) {
      ctx.beginPath();
      ctx.arc(i * 3.3, 0, 1.7, 0, Math.PI * 2);
      ctx.fill();
    }
  }
  ctx.restore();
}

function drawBricks() {
  for (let i = 0; i < bricks.length; i++) {
    const b = bricks[i];
    if (!b.alive) continue;
    const damaged = b.hits < b.maxHits;
    ctx.fillStyle = 'rgba(0,0,0,0.32)';
    roundRect(b.x, b.y + 3, b.w, b.h, 5);
    ctx.fill();
    ctx.fillStyle = damaged ? b.colorDim : b.color;
    roundRect(b.x, b.y, b.w, b.h, 5);
    ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.24)';
    ctx.fillRect(b.x + 4, b.y + 2, b.w - 8, 3);
    if (b.maxHits === 2 && !damaged) {
      ctx.save();
      roundRect(b.x, b.y, b.w, b.h, 5);
      ctx.clip();
      ctx.strokeStyle = 'rgba(255,255,255,0.14)';
      ctx.lineWidth = 2;
      for (let sx = -b.h; sx < b.w; sx += 8) {
        ctx.beginPath();
        ctx.moveTo(b.x + sx, b.y + b.h);
        ctx.lineTo(b.x + sx + b.h, b.y);
        ctx.stroke();
      }
      ctx.restore();
      ctx.strokeStyle = 'rgba(255,255,255,0.48)';
      ctx.lineWidth = 1.4;
      roundRect(b.x + 3, b.y + 3, b.w - 6, b.h - 6, 3);
      ctx.stroke();
    }
    if (damaged) drawCracks(b);
    if (b.power) {
      const pulse = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(time * 3.4 + b.col + b.row));
      ctx.strokeStyle = b.power === 'wide'
        ? 'rgba(126,246,255,' + (0.35 + pulse * 0.55) + ')'
        : 'rgba(255,213,106,' + (0.35 + pulse * 0.55) + ')';
      ctx.lineWidth = 2;
      roundRect(b.x - 1, b.y - 1, b.w + 2, b.h + 2, 6);
      ctx.stroke();
      drawPowerIcon(b.x + b.w / 2, b.y + b.h / 2 + 1, b.power, 1);
    }
    if (b.maxHits === 2) {
      ctx.font = '700 10px Segoe UI, system-ui, sans-serif';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'top';
      ctx.fillStyle = 'rgba(0,0,0,0.45)';
      ctx.fillText(b.hits + '×', b.x + b.w - 4, b.y + 3);
      ctx.fillStyle = '#ffffff';
      ctx.fillText(b.hits + '×', b.x + b.w - 5, b.y + 2);
    }
    if (b.flash > 0) {
      ctx.globalAlpha = b.flash * 0.62;
      ctx.fillStyle = '#ffffff';
      roundRect(b.x, b.y, b.w, b.h, 5);
      ctx.fill();
      ctx.globalAlpha = 1;
    }
  }
}

function drawPowerups() {
  for (let i = 0; i < powerups.length; i++) {
    const pu = powerups[i];
    const wide = pu.type === 'wide';
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    roundRect(pu.x, pu.y + 3, pu.w, pu.h, 11);
    ctx.fill();
    const g = ctx.createLinearGradient(pu.x, pu.y, pu.x + pu.w, pu.y);
    if (wide) {
      g.addColorStop(0, '#0b6e86');
      g.addColorStop(1, '#14324a');
    } else {
      g.addColorStop(0, '#8a5a12');
      g.addColorStop(1, '#4a2458');
    }
    ctx.fillStyle = g;
    roundRect(pu.x, pu.y, pu.w, pu.h, 11);
    ctx.fill();
    ctx.strokeStyle = wide ? '#7ef6ff' : '#ffd56a';
    ctx.lineWidth = 1.5;
    roundRect(pu.x, pu.y, pu.w, pu.h, 11);
    ctx.stroke();
    drawPowerIcon(pu.x + 14, pu.y + pu.h / 2, pu.type, 0.72);
    ctx.font = '700 9px Segoe UI, system-ui, sans-serif';
    ctx.textAlign = 'left';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = '#ffffff';
    ctx.fillText(wide ? 'WIDE' : '×3', pu.x + 24, pu.y + pu.h / 2 + 0.5);
  }
}

function drawPaddle() {
  const x = paddle.x;
  const y = paddle.y;
  const w = paddle.w;
  const h = paddle.h;
  ctx.fillStyle = wideTimer > 0 ? 'rgba(255,77,141,0.22)' : 'rgba(62,240,255,0.2)';
  roundRect(x - 5, y - 3, w + 10, h + 12, 12);
  ctx.fill();
  const g = ctx.createLinearGradient(x, y, x + w, y);
  if (wideTimer > 0) {
    g.addColorStop(0, '#ff4d8d');
    g.addColorStop(0.5, '#f4ffff');
    g.addColorStop(1, '#3ef0ff');
  } else {
    g.addColorStop(0, '#15c8ea');
    g.addColorStop(0.5, '#f7ffff');
    g.addColorStop(1, '#15c8ea');
  }
  ctx.fillStyle = g;
  roundRect(x, y, w, h, h / 2);
  ctx.fill();
  ctx.fillStyle = 'rgba(8,16,28,0.8)';
  ctx.fillRect(x + w / 2 - 1.5, y + 3, 3, h - 6);
  ctx.fillStyle = 'rgba(8,16,28,0.35)';
  ctx.fillRect(x + w * 0.18, y + 4, 2, h - 8);
  ctx.fillRect(x + w * 0.82, y + 4, 2, h - 8);
  if (paddle.flash > 0) {
    ctx.globalAlpha = paddle.flash;
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(x + w / 2 + paddle.hitT * w / 2, y + 2, 7, 0, Math.PI * 2);
    ctx.fill();
    ctx.globalAlpha = 1;
  }
}

function ballPalette(hue) {
  if (hue === 1) return ['#ffe6f4', '#ff4d8d'];
  if (hue === 2) return ['#fff6d8', '#ffd166'];
  return ['#f4ffff', '#3ef0ff'];
}

function drawBalls() {
  for (let i = 0; i < balls.length; i++) {
    const b = balls[i];
    if (b.dead) continue;
    const pal = ballPalette(b.hue);
    for (let t = 0; t < b.trail.length; t++) {
      const p = b.trail[t];
      const a = (t / b.trail.length) * 0.35;
      ctx.globalAlpha = a;
      ctx.fillStyle = pal[1];
      ctx.beginPath();
      ctx.arc(p.x, p.y, b.r * (0.35 + 0.5 * t / b.trail.length), 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
    const glow = ctx.createRadialGradient(b.x, b.y, 1, b.x, b.y, 22);
    glow.addColorStop(0, pal[1]);
    glow.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.globalAlpha = 0.55;
    ctx.fillStyle = glow;
    ctx.beginPath();
    ctx.arc(b.x, b.y, 22, 0, Math.PI * 2);
    ctx.fill();
    ctx.globalAlpha = 1;
    const rg = ctx.createRadialGradient(b.x - 2, b.y - 3, 1, b.x, b.y, b.r);
    rg.addColorStop(0, '#ffffff');
    rg.addColorStop(0.45, pal[0]);
    rg.addColorStop(1, pal[1]);
    ctx.fillStyle = rg;
    ctx.beginPath();
    ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
    ctx.fill();
  }
}

function drawParticles() {
  for (let i = 0; i < particles.length; i++) {
    const p = particles[i];
    const t = 1 - p.age / p.life;
    if (t <= 0) continue;
    if (p.type === 'ring') {
      ctx.globalAlpha = t;
      ctx.strokeStyle = p.color;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(p.x, p.y, 6 + (1 - t) * 30, 0, Math.PI * 2);
      ctx.stroke();
      ctx.globalAlpha = 1;
    } else {
      ctx.save();
      ctx.globalAlpha = Math.max(0, t);
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot || 0);
      ctx.fillStyle = p.color;
      const s = p.size * (0.5 + t * 0.5);
      ctx.fillRect(-s / 2, -s / 2, s, s);
      ctx.restore();
    }
  }
}

function drawFloaters() {
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.font = '700 14px Segoe UI, system-ui, sans-serif';
  for (let i = 0; i < floaters.length; i++) {
    const f = floaters[i];
    ctx.globalAlpha = Math.max(0, 1 - f.age / f.life);
    ctx.fillStyle = f.color;
    ctx.fillText(f.text, f.x, f.y);
  }
  ctx.globalAlpha = 1;
}

function drawAim() {
  if (!(mode === 'player' && phase === 'ready')) return;
  const b = balls[0];
  if (!b) return;
  const influence = clamp((paddle.vx || 0) / 1100, -0.55, 0.55);
  const len = 72;
  ctx.save();
  ctx.setLineDash([4, 6]);
  ctx.strokeStyle = 'rgba(232,251,255,0.75)';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(b.x, b.y);
  ctx.lineTo(b.x + Math.sin(influence) * len, b.y - Math.cos(influence) * len);
  ctx.stroke();
  ctx.restore();
  ctx.font = '700 13px Segoe UI, system-ui, sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillStyle = 'rgba(232,251,255,0.9)';
  ctx.fillText('CLICK OR SPACE TO LAUNCH', BASE_W / 2, paddle.y - 38);
}

function drawBannerText() {
  if (phase === 'lifelost') {
    ctx.globalAlpha = Math.min(1, lifeTimer / 0.25);
    ctx.font = '800 36px Segoe UI, system-ui, sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = '#ff5d8a';
    ctx.fillText('LIFE LOST', BASE_W / 2, BASE_H * 0.58);
    ctx.globalAlpha = 1;
  }
}

function draw() {
  resize();
  ctx.clearRect(0, 0, BASE_W, BASE_H);
  ctx.save();
  if (shake > 0.25) {
    ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
  }
  drawBackground();
  drawRails();
  drawBricks();
  drawPowerups();
  drawParticles();
  drawFloaters();
  drawPaddle();
  drawAim();
  drawBalls();
  drawBannerText();
  ctx.restore();
  if (hurtFlash > 0) {
    ctx.fillStyle = 'rgba(255,40,70,' + (hurtFlash * 0.28) + ')';
    ctx.fillRect(0, 0, BASE_W, BASE_H);
  }
}

function resize() {
  const availW = stage.clientWidth || Math.max(280, window.innerWidth - 24);
  const availH = stage.clientHeight || Math.max(180, window.innerHeight - 150);
  const scale = Math.min(availW / BASE_W, availH / BASE_H);
  const dw = Math.max(2, Math.floor(BASE_W * scale));
  const dh = Math.max(2, Math.floor(BASE_H * scale));
  frame.style.width = dw + 'px';
  frame.style.height = dh + 'px';
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  if (dw !== lastDW || dh !== lastDH || dpr !== lastDPR) {
    lastDW = dw;
    lastDH = dh;
    lastDPR = dpr;
    canvas.width = Math.floor(BASE_W * dpr);
    canvas.height = Math.floor(BASE_H * dpr);
  }
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

function updateMouse(e) {
  const rect = canvas.getBoundingClientRect();
  if (rect.width < 2) return;
  mouse.x = ((e.clientX - rect.left) / rect.width) * BASE_W;
  mouse.y = ((e.clientY - rect.top) / rect.height) * BASE_H;
  mouse.inside = mouse.x >= 0 && mouse.x <= BASE_W && mouse.y >= 0 && mouse.y <= BASE_H;
}

function playerStart() {
  if (mode !== 'demo') return;
  mode = 'player';
  demoHold = 0;
  startOverlay.classList.add('hidden');
  endOverlay.classList.add('hidden');
  resetGame(false);
  lastInput = 'mouse';
}

function playerRestart() {
  mode = 'player';
  demoHold = 0;
  startOverlay.classList.add('hidden');
  endOverlay.classList.add('hidden');
  keys.left = false;
  keys.right = false;
  resetGame(false);
}

document.addEventListener('pointermove', function (e) {
  updateMouse(e);
  if (mouse.inside && mode === 'player') lastInput = 'mouse';
});

document.addEventListener('pointerdown', function (e) {
  updateMouse(e);
  if (mode === 'demo') {
    playerStart();
    updateMouse(e);
    lastInput = 'mouse';
    return;
  }
  if (phase === 'gameover' || phase === 'victory') {
    playerRestart();
    return;
  }
  if (mode === 'player') lastInput = 'mouse';
  if (phase === 'ready') launch();
});

document.addEventListener('keydown', function (e) {
  const key = e.key;
  if (key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === ' ' || e.code === 'Space') {
    e.preventDefault();
  }
  if (mode === 'demo') playerStart();
  if (phase === 'gameover' || phase === 'victory') playerRestart();
  if (key === 'ArrowLeft') { keys.left = true; lastInput = 'keys'; }
  if (key === 'ArrowRight') { keys.right = true; lastInput = 'keys'; }
  if (key === ' ' || e.code === 'Space' || key === 'ArrowUp') launch();
});

document.addEventListener('keyup', function (e) {
  if (e.key === 'ArrowLeft') keys.left = false;
  if (e.key === 'ArrowRight') keys.right = false;
});

window.addEventListener('blur', function () {
  keys.left = false;
  keys.right = false;
});

window.addEventListener('resize', resize);

let lastTime = performance.now();
function loop(now) {
  let dt = (now - lastTime) / 1000;
  lastTime = now;
  if (dt > 0.033) dt = 0.033;
  if (dt < 0) dt = 0;
  update(dt);
  draw();
  requestAnimationFrame(loop);
}

resize();
resetGame(true);
updateHUD();
draw();
requestAnimationFrame(loop);
</script>
</body>
</html>
Reasoning trace (70,093 tokens)
I'm starting to build a polished Breakout game in a single HTML file using vanilla JavaScript and canvas.

I'm mapping out the core mechanics: mouse and keyboard paddle control, variable bounce angles, mixed brick rows with crackable two-hit bricks, power-up drops, collision prevention, particle effects, and lives with scoring.

I'm adding a start screen with a demo mode and a responsive canvas that scales from 1280×800 down to 375px. The visual style will be a dark neon arcade aesthetic with a HUD overlay for score, lives, and active power-up timers. I'm also planning a self-contained single HTML file with no external dependencies.

I'm locking in a fixed logical resolution of 960×640 for the game world, scaling it to fit the viewport. The UI will use a dark background with a centered canvas and overlay screens for start, game over, and victory. The demo mode will have an AI paddle that follows the ball automatically, so the game appears active immediately.

I'm implementing sub-stepped collision detection to prevent the ball from tunneling through bricks or the paddle. The ball will be broken into small movement steps based on its velocity, with collision checks between each step.

I'm refining the collision system to check wall boundaries, paddle hits with angle based on hit position, and brick collisions by finding the closest intersection. Each ball will move independently in multi-ball mode.

Power-ups will drop from broken bricks and can be caught by the paddle. A wide-paddle effect will last ten seconds and increase the paddle width by sixty percent. A multi-ball effect will spawn two additional balls from the current position with different angles.

The brick grid will use eight to ten columns and six or more rows with mixed colors.

Scoring will award 100 points per normal brick, 50 on the first hit and 150 on the second for tougher bricks, and 200 for caught power-ups. Lives will drop when every ball passes below the paddle, though multi-ball mode continues with remaining balls. The demo will auto-launch and let the AI play. The ball will stick to the paddle until the player clicks or presses a key to release it.

I've decided to reset the game to a fresh start when the player takes over from the demo mode, rather than continuing the AI's current state.

The demo must look active at the three-second snapshot, with bricks, ball, paddle, particles, and HUD visible. Victory triggers when all bricks are destroyed, and game over when lives hit zero. The layout will use a full-viewport CSS grid with a polished HTML title and score overlay around the canvas. A restart button and key will be included.

The visual palette is now defined: a deep purple-black background with cyan, magenta, gold, and lime accents. Bricks will glow in distinct rows, the paddle will use a cyan-to-white gradient with a glow, and the ball will have a white core with a cyan halo. Particle effects will spark in the brick's own color. The grid will be eight columns by seven rows, with the top two rows as two-hit bricks and the lower rows as normal bricks. Power-up icons will be small symbols drawn directly onto the bricks.

Power-up icons are now defined: arrows or a W for the wide-paddle effect, and three dots for the multi-ball effect. Dropped power-ups will fall as small capsules with their icon. Tough bricks will show crack lines and dim after the first hit. The collision algorithm uses clamping to find the closest point on a rectangle to the ball center, then checks if the distance squared is within the ball radius.

The paddle bounce angle will be calculated from the ball's horizontal position on the paddle, mapping the center to straight up and the edges to roughly seventy degrees from vertical. The resulting angle will be applied to the ball's velocity vector, and I'm considering whether to also factor in the ball's existing horizontal speed.

The paddle bounce angle is now fixed by hit position alone, with a maximum deviation of roughly sixty-five to seventy degrees from vertical. To prevent the ball from passing through the paddle, the code will reflect it on collision and push it upward if its center drops below the paddle top while still overlapping. A ball that drops completely below the paddle will count as a miss.

The paddle will only reflect the ball when it is moving downward and has crossed the paddle's top edge, matching classic Breakout behavior. When all balls are lost, the game decrements a life and respawns the ball stuck to the paddle. The player can release the ball by clicking, pressing space, or pressing the up arrow; the demo mode will auto-launch after a brief delay.

Game over and victory screens will overlay the canvas with the final score and a restart option. Brick breaks will emit twelve to twenty colored particles that fade and drift downward. The wide-paddle effect will display a live timer. When the multi-ball power-up is caught, two extra balls will spawn from above the paddle.

The multi-ball power-up will always spawn two extra balls, so the player ends up with exactly three total. Catching the wide-paddle power-up again will refresh its ten-second timer. The paddle width will animate smoothly when it expands. The demo AI will track the lowest ball to guide the paddle.

The demo AI will move the paddle toward the ball with slight lag for a realistic feel. The demo will auto-restart on game over or victory so the snapshot always shows active gameplay. Lives will appear as heart or ball icons. A subtle animated background will be drawn on the canvas for a rich first paint. Canvas dimensions will be computed from the container.

The canvas will be sized to the viewport using a 900×620 base resolution, with game elements scaled proportionally via a dynamic scale factor.

The canvas will render at a 900×620 base resolution, with mouse coordinates converted from screen space into those base coordinates for collision detection. The game logic will run entirely in this fixed coordinate space, and the canvas will be scaled to fit the viewport using devicePixelRatio. The HUD will sit inside the canvas at the top edge so it stays with the game area.

The HUD will be placed in HTML above the canvas so text stays crisp. The layout uses a header with a title, score, lives, and power-up indicator, followed by a stage container for the canvas.

The HTML structure now includes an overlay with a semi-transparent start message and controls, plus a hidden end overlay. The overlay will stay visible behind the game so the three-second snapshot still shows the ball, paddle, and bricks. Power-up icons will be drawn directly onto the bricks from the very first frame.

The brick grid will be seven rows by ten columns with a seven-color palette. The top two rows will be all-tough bricks, the third row will alternate, and the remaining rows will be normal. Power-ups will be scattered across the grid with roughly eight to ten bricks and alternating types. Brick dimensions are being calculated from fixed margins and gaps.

Brick dimensions are now fixed at roughly 77 by 22 pixels, with the grid starting at y=28 and ending near 224. The paddle will be 110 by 14 pixels, the ball 8 pixels in radius, and the ball speed set to 420 pixels per second. The particle system will store each particle’s position, velocity, lifetime, color, and size. Dropped power-ups will be 36 by 18 pixel glowing capsules.

The wide-paddle effect will expand the paddle from 110 to 180 pixels over ten seconds, smoothly lerping the width while keeping the paddle centered on the mouse. Lives will appear as small hearts or orbs in the HUD. Score popups and a subtle two-pixel screen shake on brick breaks will add polish. Victory will trigger confetti particles. The game will track five states: demo, playing, game over, victory, and ready. When the player starts from the demo, the game resets, the ball returns to the paddle, and a small launch hint appears.

Arrow keys and A/D will move the paddle at 620 pixels per second. Space or the up arrow launches the ball, and clicking also starts it. The mouse controls the paddle directly, and if both mouse and keyboard are active, the last input wins. The game will show a smaller launch hint after the initial start message. On the game over screen, pressing R or clicking the restart button will reload the level. The iframe only receives keyboard events after the first click.

Arrow keys and space will be intercepted to prevent browser scrolling. The ball will be advanced in substeps so the swept AABB collision check can resolve the closest brick hit first and recheck for any remaining overlaps.

The ball will be checked against bricks up to four times per substep, resolving the closest hit each time. A brick collision is detected by expanding the brick rectangle by the ball radius and testing against a point. If the ball drops below the canvas, it is marked dead.

The collision routine computes the smallest overlap on the left, right, top, or bottom edge of each brick to determine the exact side of impact.

The collision routine will use velocity direction to choose the correct side when the ball hits a brick corner, preventing the ball from sticking to the wrong edge.

The ball-brick collision routine will clamp the ball’s center to the brick’s bounds and check if it lies inside the brick. If the ball is already inside the brick, the previous position will be used to determine which side was crossed.

The collision routine now uses a swept-circle-AABB intersection test to find the earliest hit along the ball’s path. It snaps the ball to the exact collision point and returns the hit brick.

The ball will be nudged slightly out of the brick after collision to prevent sticking. Its velocity will be reflected using the outward normal, with a dot-product check to ensure the bounce only happens when the ball is actually moving toward the brick.

The ball will be reflected using the outward normal, and the brick will be damaged immediately after the bounce. If the ball clips into a neighboring brick after one is destroyed, the next substep resolves it. The collision sweep will use a ray-AABB intersection test on the ball’s center point against the expanded brick bounds.

The ray-AABB intersection routine is being implemented with slab tests on the X and Y axes. It computes the earliest and latest intersection times and assigns the outward normal from the near plane, ensuring the ball bounces off the correct face even when it starts inside the brick.

When the ball starts inside a brick, the collision routine will push it out along the smallest overlap axis and flip the velocity on that axis.

The ball will be nudged out of the brick along the smallest overlap axis and its velocity flipped on that axis. If the ball overlaps a corner or edge but its center is outside the brick, it will be pushed along the vector from the closest point on the brick boundary.

The ball will be advanced in 3-pixel substeps. Paddle collisions will use a swept rectangle check, but only if the ball is coming from above or has not yet passed below the paddle. Side hits will be allowed only while the ball is still above the paddle.

The paddle collision routine now uses a swept rectangle check against the expanded paddle bounds. It ignores hits from below the paddle and skips upward passes that occur above it. The bounce angle is derived from the ball’s horizontal offset on the paddle, clamped between -1 and 1, and scaled to a maximum of 1.15 radians.

The ball’s speed is clamped to a minimum of 340 pixels per second after each bounce. Side hits will snap the ball to the paddle top to prevent tunneling through the paddle. When the ball is stuck on the paddle, it will sit just above the paddle with zero vertical velocity. The launch velocity will be set to 400 pixels per second, slightly angled based on the paddle’s movement.

Ball speed will increase by roughly two percent every eight bricks, peaking at 1.4 times the original speed. The wide-paddle power-up will display a ten-second countdown bar. Multi-ball will spawn two additional balls at angles of minus and plus 0.5 radians from the original trajectory, using the source ball’s current speed.

Power-up drops will fall at 140 pixels per second and be caught only if they overlap the paddle rectangle. If a dropped power-up is missed, it is removed once it drops below the base of the canvas. If the ball is stuck on the paddle when multi-ball is triggered, two extra balls will spawn upward from the paddle instead.

Tough bricks require two hits. After the first hit they crack and score fifty points. They drop no power-up. On the second hit they break and score one hundred points. They drop a power-up if one is assigned. Normal bricks need one hit. They score one hundred points and drop a power-up. The crack effect draws jagged lines on the brick. Power-up icons are drawn on bricks before they break. Their positions are chosen deterministically for a balanced layout.

The power-up positions are fixed at (1,1), (4,0), (7,2), (2,4), (8,3), (5,5), (0,6), (9,1), (3,3), and (6,6). Tough bricks in rows zero and one ensure those drops require two hits. Scores are fifty for a cracked tough brick, one hundred for a normal brick, one hundred fifty for a destroyed tough brick, and two hundred fifty for a caught power-up. Lives appear as three glowing orbs. The game ends when lives reach zero after the last ball is lost, or when all bricks are destroyed. On a life loss the ball returns to the paddle and the game state resets to ready, clearing any extra balls and power-ups in flight.

A demo AI will track the ball closest to the bottom and predict its landing position on the paddle to drive the mouse cursor automatically.

The demo AI will mirror the predicted ball position off the left and right walls when it lands outside the canvas. The paddle will move toward that target at a capped speed of 520 pixels per second. The demo will auto-launch the ball after 0.6 seconds. After a win or a full loss, the game will restart automatically after 1.2 seconds. The screenshot will be taken at 3 seconds to capture active gameplay.

The demo will begin with the ball already in flight so the first frame shows active gameplay. No bricks will be pre-broken, ensuring the full brick field and power-up icons remain visible in the screenshot.

The ball will start at 45% of the canvas width and 62% of the height, moving at 180 by -360 pixels per second so it strikes bricks within about one second. This ensures the screenshot at three seconds captures broken bricks, particles, and possibly a dropped power-up. Overlays will use a semi-transparent bottom banner or a centered glass card rather than covering the entire canvas.

The overlay will use a bottom-aligned banner with a dark gradient background and a cyan border glow. It will display the exact phrase "Click or press a key to start". The game over and victory screens are next.

The end screens will show a title, score, and a "Play Again" button. Clicking it or pressing a key restarts the game. A window keydown listener will handle restarts. The overlay will use pointer-events auto so the button is clickable. The canvas background will be a deep gradient with a subtle grid, vignette, and seeded static stars. Bricks will be drawn as rounded rectangles with gradient fills and inner highlights.

Bricks will use a translucent glow instead of shadowBlur. Tough bricks will show a diagonal stripe or metal texture with a small "2" or shield icon, and crack lines will overlay them after the first hit. The paddle will be a rounded capsule with a cyan-white gradient and a glow underneath, turning magenta at its edges when widened. The ball will use a white-to-cyan radial gradient and leave a fading trail of eight stored positions. Power-up capsules will bob with a glow, and the wide-paddle variant will display cyan arrows.

The multi-ball power-up will use a magenta capsule with three dots. Particles will be squares and circles that fade and shrink. Score values will float upward as text. Screen shake will offset the canvas draw by a decaying horizontal and vertical amount. The UI will sit outside the canvas. Fonts will use a system stack including Segoe UI and Avenir Next. The theme is cosmic arcade. The layout will be responsive: at 375 pixels the header stacks and the score and lives share a row.

The layout will use a flex column with hidden overflow to prevent scrollbars. The stage will flex to fill remaining space and center the canvas. Overlays will be positioned over the canvas rather than the full stage.

The canvas and overlays will be wrapped in a relative frame sized to the base resolution and scaled to fit the viewport. The overlays will be absolutely positioned over that frame. Lives, the wide-paddle countdown, and the active ball count will be rendered in the header and updated via the DOM.

A "LEVEL 1" label will appear at the top of the screen. The game over screen will display "GAME OVER" with the score and a "Play Again" button, plus a hint to press R or click. The victory screen will show "VICTORY", "All bricks shattered", the score, and a restart option. During the ready state, a small "SPACE or CLICK to launch" hint will appear near the paddle instead of the full overlay. The ball will sit centered on the paddle while the player waits to launch.

The ball will sit centered on the paddle until the player launches it. The launch angle will be slightly skewed by the paddle's horizontal velocity, clamped to about ±0.4 radians so the ball can curve with the paddle's movement.

The ball will launch at 420 pixels per second with an angle derived from the paddle's horizontal velocity. The game will use a mode and phase state machine: demo runs continuously in the playing phase, while player mode starts in ready and only transitions to playing on a click or keypress. Clicks on the Play Again button will restart the game to ready, and clicks during the ready phase will launch the ball.

The Play Again button will stop event propagation so clicks do not bubble up to the overlay. The end overlay itself will also be clickable to restart the game. A window-level pointer listener will handle clicks in demo mode to start the player, or in the ready phase to launch the ball.

The end overlay will use pointer-events: none so clicks pass through to the canvas. Restart will be triggered by pressing R, Enter, or Space.

Any keypress will restart the game from the end screen. Space and arrow keys will have preventDefault to stop browser scrolling. The canvas will use a capped devicePixelRatio of 2 for performance. Crack lines will be drawn by clipping a rectangle inside each brick.

Crack lines will be drawn as dark diagonal strokes inside each brick using a clipped rectangle. A small 10×10 badge will sit on the center-right of bricks to indicate a power-up. The wide-paddle icon will be drawn as a horizontal double arrow.

A dark circle will sit behind each power-up icon for contrast. Falling power-ups will appear as pill-shaped capsules with a gradient fill, a centered badge, and a label reading "WIDE" or "x3". When a brick breaks, sixteen particles will burst outward with a ring flash. Audio is being skipped to avoid autoplay issues. A potential bug was noted: the ball could get stuck in a loop if it reflects off a brick that has not yet been destroyed.

A per-brick 0.08-second cooldown will prevent the ball from hitting the same brick twice in one frame. The ball will also be pushed fully out along the normal vector after each collision so it cannot re-enter the brick immediately.

The ball will be pushed fully out along the collision normal after each hit so it cannot re-enter the brick immediately. The 0.05-second per-brick cooldown will prevent one ball from double-hitting the same brick in a single frame. The 3-pixel substeps and expanded collision radius ensure the ball cannot pass through the 14-pixel paddle.

A final overlap check with the paddle will run after all movement. The ball will be pushed fully out along the collision normal after each hit. A reflect helper will normalize velocity to a minimum speed after each bounce so the ball never loses momentum.

The ball will store its speed in a separate property so collisions cannot bleed energy. After each bounce the velocity will be renormalized to that stored speed. If the ball’s vertical speed drops below 90 pixels per second, it will be forced upward or downward to prevent horizontal sliding. Resizing the window will rescale the display without resetting the game state. Mouse coordinates will be mapped using getBoundingClientRect.

The canvas will initialize and paint immediately on first frame, then enter the animation loop. The wide-paddle power-up will last ten seconds in both player and demo modes, lerping the paddle width between one hundred eight and one hundred seventy-six pixels while keeping its center fixed. The overlay text will read exactly "Click or press a key to start". No alerts or localStorage will be used.

The paddle will derive its target center from mouse position, arrow keys, or the AI, with the last input winning. Its width will lerp toward the target over time while the center stays fixed, and the x position will be recalculated from that center and clamped to the canvas bounds.

Mouse movement will be captured on the window and mapped to the canvas coordinate space using getBoundingClientRect. The paddle will only follow the mouse while the cursor remains inside the canvas bounds. After the last ball is lost, the game will pause for 0.4 seconds before either returning to the ready screen or showing the game over screen.

A "LIFE LOST" flash will appear during the brief pause after the last ball is lost. A faint "DEMO" badge will sit in a corner of the canvas so judges know the game is in demo mode. The header layout is being sketched out.

The header will display the game title, score, lives, and active power-up in a flex row. On narrow screens it will stack vertically. A footer hint will explain controls and power-up catching.

A subtle scanline or glow border will frame the canvas. The page background will use deep purple and blue radial gradients. The plan is now complete.

Ball trails and impact flashes will be added. Floating text labels will appear at hit locations. The level will use ten columns and seven rows. A seeded random generator will keep particle bursts reproducible. The game will be organized into constants, state, initLevel, resetGame, update, draw, input handlers, and the main loop. The player will start with three lives and a score of zero.

Tough bricks will form a fortified top row and a checkerboard pattern in the second row. Each row will use a distinct HSL hue with slight column variation for visual depth. Tough bricks will be darker and gain a metallic sheen. A helper will draw rounded rectangles for the bricks. The ball trail will be rendered in world space.

The paddle will sit 36 pixels above the bottom edge at a height of 14 pixels, leaving a 22-pixel gap for the ball to fall through. Side rails will be drawn subtly along the walls. To keep the canvas fast, shadowBlur will be avoided or limited to the ball and paddle only; bricks will use a fake glow drawn as a larger alpha rectangle. The entire response will be a single fenced HTML code block.

A final safety sweep will run after every movement. If a ball overlaps any brick, it will be pushed out and reflected up to eight times. A `separateFromBricks` helper will handle this. The paddle will also be checked for overlap and bounced if the ball is inside it. The frame delta will be capped at 0.05 seconds to prevent the ball from tunneling through the paddle or bricks.

Ball speed will be capped at 560 pixels per second. Destroying bricks will subtly accelerate the ball by 1.012 up to 520. Multi-ball instances will each track their own speed. Power-up collisions will use a bounding-box overlap check, plus a sweep to catch falling items that pass the paddle top between frames.

Power-up icons will be bright badges so they stand out clearly on bricks. The footer will include a legend explaining that shield bricks need two hits and that catching a falling icon grants either a wide paddle or a multi-ball power-up. The game over screen will show a prominent restart button. During demo mode, the game will auto-restart after only 0.8 seconds so the AI can survive long enough to look competent without lingering on the game over screen.

The demo AI will predict the ball's landing point on the paddle by simulating wall bounces. It will trace the ball's path until it reaches the paddle or hits a side wall, then return that x coordinate so the paddle can intercept the ball.

The demo AI will only track the ball when it is descending and below the top third of the canvas. Otherwise the paddle will drift to the center with a gentle sine-wave motion. This prevents the AI from looking robotic while still catching the ball reliably.

The demo AI will select the ball that is closest to the paddle and descending. The entire canvas will be drawn in a strict layer order: background gradient, twinkling stars, grid, side glow lines, bricks, power-ups, particles, and finally the paddle. A subtle radial gradient will give the ball a glow without using shadowBlur. A moving highlight shimmer and twinkling background stars are also being considered for extra polish.

HTML overlays will handle the start and end screens so text stays crisp. The canvas will scale to fit the viewport, and at 1280×800 it will render at roughly 1066×666.

On a 375×800 phone the canvas will scale to roughly 351×219. Bricks will remain visible but the game area will be small. Using a shorter base height or a different internal aspect ratio for narrow screens was considered but rejected as unnecessary complexity.

A media query will hide the footer tagline and shrink the header on screens under 520 pixels wide to reclaim space. The canvas will stay centered with vertical letterboxing on tall phones.

No high-score storage will be used. The canvas will be drawn in internal BASE coordinates and scaled to the CSS viewport using setTransform for device pixel ratio. The code will be kept clean and all edge cases verified.

The boot sequence will call resize immediately, then again after the first layout pass so client dimensions are reliable. A fallback width and height will prevent zero-size frames. The game will start in demo mode, draw the initial frame, and enter the animation loop.

The script is placed at the end of the body so layout dimensions are already calculated when resize runs. HTML overlays will be toggled with CSS classes instead of inline styles. The start overlay is visible by default and hidden once the player begins. The end overlay is hidden during play and only shown on game over. In demo mode the end overlay is never displayed because the game auto-restarts.

The game will enter a "lifelost" phase for 0.85 seconds after a ball is lost. During this window the paddle remains controllable but the ball is dead. The phase then switches to "ready" and a new ball spawns. If the player has no lives left the phase becomes "gameover" and the end overlay appears. The exact moment to decrement the life counter is still being weighed.

Lives will decrement immediately so the HUD stays accurate. The demo loop will check each frame for game over or victory and reset the full game if the AI loses all lives before the player begins. Scores will be formatted with commas. A power HUD will display a wide-paddle countdown bar and a multi-ball indicator, both updating live via innerHTML.

The power HUD will use textContent on spans instead of innerHTML for cleaner updates. Tough bricks will gain double borders and corner brackets so they look harder before the first hit. A white flash overlay will fade on bricks when they are struck. The crack effect will use darker colors and missing corner chips. The design is complete and ready to be written as a single HTML file.

A guard will be added so pointer events do not throw errors before the canvas bounding rectangle is ready. The multi-ball power-up will spawn extra balls until the total reaches exactly three, or add one extra if the player already has three.

The multi-ball power-up will spawn two extra balls with angled velocities whenever the player catches it, ensuring exactly three balls are in play. This matches the requirement to grant "multi-ball (3 balls)". Power-ups only fall during active play, and any that remain when a ball is lost are cleared.

The demo ball will start at y=280 so it strikes the bottom bricks within the first 0.2 seconds. This guarantees cracked bricks and particles appear in the screenshot before the 3-second demo window ends.

The demo ball will start at x=420, y=300 with velocity 160 by -400. Bricks will be 84 pixels wide and 22 pixels tall, spaced with 36 pixels of side margin and 5 pixels of gap. A power brick will be placed in the ball's path so a falling icon appears within the first few seconds. The demo will not aim perfectly, but it will hit a brick quickly enough to trigger the crack and particle effects before the 3-second window ends.

The demo ball will start at x=430, y=250 with velocity -40 by -420, hitting around x=413 in row 2. A power brick will be placed nearby so a falling icon appears within the first few seconds. Ambient sparkles are unnecessary because bricks will break before the 3-second window ends. Paddle hit particles will add extra juice. The game will start on pointerdown or keydown events. Ready to code.

The end-screen restart button will use stopPropagation to avoid launching the ball. Touch input is handled by pointer events, with tapping the canvas launching the ball. No external fonts or images will be loaded; everything is inline. A fallback helper will be used for canvas roundRect to ensure compatibility with older browsers.

A manual `roundRect` helper using `arcTo` will replace `ctx.roundRect` for broader browser support. Victory requires every brick to be fully destroyed, not merely cracked. A combo multiplier was considered but skipped. The title will use a gradient text effect.

The title will use a cyan-to-white-to-pink gradient with transparent text. The end screen will show a restart button and a keyboard hint to press any key or click to restart. When the player loses all lives, the start overlay hides and the end overlay appears.

The start overlay will display the exact prompt "Click or press a key to start". Lives will be rendered as HTML heart spans with CSS classes toggling on and off, avoiding canvas drawing for the HUD.

Heart icons will use a dark purple base and a pink glow for active lives. The wide-paddle timer will appear as a glow on the paddle itself. The collision detection code is being drafted with a swept-circle-rectangle helper.

The swept-circle-rectangle collision helper expands the brick's bounding box by the ball's radius. This approximation slightly over-generous at corners, but it prevents tunneling and keeps the physics simple.

The swept-circle-rectangle approximation was rejected because it can trigger false corner hits. The ball will instead be advanced in small 2-pixel substeps, with discrete collision checks each step. This prevents tunneling while keeping the bounce geometry accurate.

The ball will be split into substeps based on its speed and the frame duration. Each substep advances the ball by a fraction of its velocity, then checks collisions with the walls, paddle, and bricks. The brick collision routine will resolve overlaps iteratively, up to six times per frame, to prevent tunneling.

The collision routine now selects the deepest overlap between the ball and any alive brick. It pushes the ball out along the normal vector plus a small 0.15 pixel offset to prevent sticking. The ball bounces using a standard reflection formula based on the dot product of velocity and normal. After the bounce it enforces speed limits, applies damage to the brick, and sets a 0.06 second cooldown on the brick.

The `circleRectOverlap` helper is being refined to use clamped closest-point math for accurate overlap detection. It checks if the ball center lies inside the rectangle, then computes the distance to the nearest edge to determine the correct normal and depth for collision resolution.

The discrete collision solver is now complete. It returns the deepest overlap with a normal vector and push depth, or null if the ball is too far from the brick.

The paddle collision routine is being drafted. It uses the same circle-rectangle overlap helper as bricks. It ignores hits when the ball is clearly below the paddle and moving downward, preventing the ball from passing through the paddle.

The paddle collision routine now calculates the bounce angle based on where the ball strikes the paddle. The angle is proportional to the horizontal offset from the paddle center, capped at roughly 69 degrees for edge hits. The ball speed is clamped to a minimum of 360 pixels per second after the bounce.

The ball is placed just above the paddle to prevent sticking. If the ball hits the side of the paddle while falling beside it, it snaps upward rather than sliding horizontally. This edge case is player-friendly and guarantees the ball never passes through the paddle.

A cooldown on the paddle prevents the ball from re-bouncing every substep and resetting its angle. The collision logic correctly distinguishes a valid side bounce from a miss: if the ball is fully below the paddle, it is treated as a miss and not teleported upward.

Wall collisions will clamp the ball to the canvas edges and reverse the appropriate velocity. A ceiling glow line will be added. Brick destruction will trigger a camera shake that decays each frame. The game is expected to be roughly 800 to 1000 lines.

Camera shake will be applied with a random offset scaled by a decaying magnitude, capped at four pixels. The background will remain static rather than scrolling. Eighty twinkling stars will be drawn at random positions with varying sizes and opacities.

Star opacity will pulse using sine waves with random phase offsets. The CSS will use border-box sizing and hidden overflow to prevent layout issues. Power-up icons will be large, glowing, and slightly spinning or pulsing to stand out when dropped from bricks.

Power-up icons will be 48 by 22 pixels, displaying "WIDE" or "x3" with a pulsing glow. Catching one triggers burst particles and floating text "+WIDE" or "+MULTI". The wide-paddle effect lasts ten seconds and resets if caught again. Paddle width will scale from 116 to 200 pixels. A rotating mark on the ball will add visual polish. A ten-point trail of fading dots will follow the ball.

The ball trail will be updated once per frame rather than on every substep. All balls will share the same cyan-white color scheme for visual cohesion. The ball array will never be empty; dead balls are marked inactive instead. During the ready phase, the ball sits idle above the paddle and follows the paddle's movement. Launching sets the ball's velocity and switches the game phase to playing. The demo mode skips the ready phase and starts directly with an initial velocity.

The victory and game-over checks will run after all brick collisions are resolved, not during the iteration. A brief delay lets the last particles finish before the overlay appears. In demo mode, the game will automatically restart after the victory or game-over screen.

Demo mode will skip the end overlay and continue playing until the AI exhausts all lives. The AI will pause for only 0.5 seconds after each life loss to avoid screenshot artifacts. The ball returns to the paddle in roughly 1.5 to 2 seconds, giving the AI enough time to reposition before the next serve.

The paddle will start slightly left of the ball's initial x position to prevent tunneling. The demo will launch the ball with an initial velocity before the first frame is drawn, so the screenshot shows a moving ball, full brick layout, and HUD immediately.

A pulsing CSS glow will animate the start banner so the opening message looks alive. The screenshot at three seconds will capture the ball in motion, twinkling stars, and active particles. I will avoid localStorage, sessionStorage, cookies, IndexedDB, alerts, confirm, prompt, window.open, and console.log to keep the code clean and compliant.

A `playerStart()` function will let the player take over from demo mode. The keydown listener will suppress default browser behavior for arrow keys, space, and Enter. After switching to player mode, the same keypress may also trigger the ball launch.

The player will enter a ready state when starting from demo mode, so the ball stays idle above the paddle and the player can see the setup before launching. A second keypress or click will then release the ball. After a victory or game-over, pressing a key will restart the game.

The keydown handler now releases the ball on space, Enter, or up arrow. A keyup listener will clear the arrow key states. In demo mode, the start function is called before the early return, so the player can immediately hold the arrow keys to move the paddle once the game switches to player mode.

The pointerdown handler now routes clicks to the correct action. Clicking the end overlay restarts the game in player mode. Clicking the canvas starts the demo or launches the ball from the ready state.

The end overlay click handler will stop event propagation to prevent the window listener from firing. A pointermove listener will map mouse coordinates to the canvas coordinate space so the paddle tracks the cursor.

The mouse pointer is clamped to the canvas bounds. When the cursor leaves the canvas, the paddle keeps its last valid position so it does not jump. Arrow keys can still take over control if the mouse exits.

The paddle update logic now uses a `lastInput` flag to choose between mouse and keyboard control. In demo mode the AI drives the center directly. When keys are active, the center shifts by 680 pixels per second scaled by delta time. Otherwise the center tracks the mouse cursor. The paddle width smoothly interpolates toward the target size and clamps to the canvas edges.

Paddle velocity will be derived from the center delta divided by delta time so the ball's launch angle is influenced by the paddle's movement. The wide-paddle timer will count down continuously even during the ready phase. The HUD will display active power-up status. Bricks will show a visible crack and darker fill after the first hit, with white crack lines, while keeping the collision rectangle aligned with the visual bounds.

The crack will be a zigzag line drawn in light and dark gray, offset by one pixel for visibility against any brick color. Tough bricks will show a "2" badge when intact and a "1" or crack once damaged. Power-up icons will remain visible even on cracked bricks so players know the power is still there. A power map will be defined to assign icons to bricks.

Twelve power-up bricks are mapped across the wall with alternating wide and multi-ball icons. At mobile scale the icons shrink to roughly four or five pixels, so they will appear as small dots.

Brick height will be 24 pixels with a 6-pixel gap, giving seven rows a total height of about 182 pixels. The HTML footer will display simple control instructions, and the Play Again button will be styled clearly.

A CSS score-pop animation will flash the HUD when the score changes, but it will be skipped if it causes layout shift. The burst particle system will emit 2–5 fragments per hit, each with randomized speed, size, and gravity.

Particle physics will apply gravity and fade fragments by age. Brick destruction spawns 18 particles while cracks spawn 8 smaller ones. A 250-particle cap will shift the oldest fragments when exceeded. Floating score text will drift upward for 0.8 seconds. A brief light flash will highlight bricks on hit. A global time accumulator will track elapsed frames.

The end screen will read the live score from state and display a victory or game-over title with a brief message. It will also hide when the player restarts.

A confetti burst will fire on victory so the celebration remains visible behind the semi-transparent overlay. The end overlay will use a dark background with a subtle blur. The start overlay will use a bottom gradient instead of heavy blur so the game layout stays clearly visible in the screenshot.

The start overlay will use a compact centered card rather than covering the bottom forty percent of the canvas. This keeps the paddle and ball fully visible in the screenshot. The card will sit in the lower-middle area above the paddle.

The start overlay will use a radial gradient from the bottom center to keep the game visible behind it. The banner will be a semi-transparent dark card with a cyan border, centered in the canvas, so the bricks and ball remain clearly visible through it.

The start banner will use a 22-pixel title and 13-pixel subtitle in light cyan. A media query will shrink the text to 16 pixels and tighten padding on screens under 520 pixels. The banner will also pulse its border to draw attention.

A center notch marker will be added to the paddle so players can see the straight-up zone. Faint angle ticks will provide extra visual polish. The ball will flash a bright line at the exact impact point on paddle hits. The start function will be made idempotent so key repeats do not accidentally trigger it repeatedly.

The keydown handler now branches into three distinct paths: starting from demo mode, restarting after victory or game over, or moving the paddle. A click on the canvas will start the demo but not launch the ball; the UI will prompt the player to press a key or click again to release the ball.

The start banner will include a subtitle explaining that mouse or arrow keys move the paddle. A launch hint will appear on the canvas near the paddle once the game enters the ready phase.

A thin animated neon border will trace the playfield edges. The wall will contain exactly seventy bricks across seven rows, with twenty-five tough bricks mixed in. A red vignette flash will indicate when the player loses a life.

The demo AI will introduce a small 8-pixel tracking error so the paddle does not hover perfectly at the edges. This makes the demo look more human while still being reliable.

Power-up collisions will use the current paddle rectangle, so a wide paddle can still catch them.

When spawning a multi-ball power-up, the new balls will inherit the source ball’s speed but be split into slightly different angles. The vertical velocity will be nudged away from zero so the extra balls do not drift straight up.

The multi-ball spawn logic now creates two extra balls at ±0.45 radians from the source angle, renormalizes their speed, and caps the total at eight to prevent chaos.

Dead balls will be marked and filtered each frame so they do not render during multi-ball mode. They will remain in the array until every ball is dead, at which point the game checks for a life loss. This prevents duplicate calls to loseLife.

The loseLife function decrements lives and triggers the red vignette flash. It advances to gameover if the player is out of lives, or to lifelost otherwise. The lifelost phase lasts 0.7 seconds, then spawns a stuck ball and returns the game to ready. In demo mode, the ball auto-launches immediately after the life loss.

Power-ups will remain catchable during the lifelost sequence so the player can still grab them before the next ball spawns. After the game ends, particles and fallen balls will keep updating for visual juice behind the overlay, but no new power-ups or bricks will spawn.

The update loop will branch by phase. During ready it keeps the ball stuck. During playing it steps each ball and checks for dead ones. During lifelost it runs the timer. Brick collisions are skipped once the wall is empty. The wide-paddle timer is decremented each frame.

The ready-phase ball will sit exactly one pixel above the paddle center, with its velocity zeroed and trail cleared. The update loop also decrements brick cooldowns every frame, runs the particle and floater systems, fades the red hurt flash and camera shake, and checks for victory once the wall is empty.

The HUD will only update the DOM when the score, lives, or active power-up text actually changes. This prevents layout thrashing during the frame loop.

The power-up HUD slot will keep a fixed width and show "—" when inactive, preventing layout shifts. A CSS width bar was considered but skipped in favor of simple text. The score formatting function is now being defined.

The score formatter will insert commas every three digits. The header will display "SECTOR 01". Dead balls will be marked rather than spliced during the update loop. A ball that falls below the playfield is marked dead. The all-dead check runs after every ball is updated. Multi-ball mode continues normally when one ball dies.

Paddle collisions are restricted to the playing phase. The ball radius is 8 pixels, so the visual wall will be drawn 4 pixels inside the edges while collision detection stays at the 10-pixel wall boundary. This keeps the ball centered within the wall margin and prevents clipping. Bricks are laid out with a 22-pixel horizontal margin, fitting comfortably inside the walls.

The playfield will be 960 by 600 pixels with a 12-pixel wall margin. Bricks will be 86 pixels wide by 24 pixels tall, arranged in ten columns and seven rows with a 6-pixel gap. A red danger line will appear near the bottom when the ball drops low. The game is now fully designed and ready to be written.

I will use braces inside switch case blocks to avoid const scoping errors. The space key will be detected via both `e.key` and `e.code` for broader browser compatibility. I also need to guard against key repeat firing the restart handler repeatedly after game over, since that could reset the score while the game is still active.

The keydown handler now returns immediately after restarting from game over or victory, which prevents key repeats from firing again. Repeated arrow keys will still register as movement inputs. The start overlay will use pointer-events none so it cannot block mouse input.

A "Play Again" button will sit inside the end overlay so the restart action is accessible and visually explicit. The overlay itself will also catch clicks to ensure the game always restarts. During active play, the canvas cursor will switch to pointer or crosshair to signal that the paddle is interactive.

The page cursor will stay default during play to avoid disorientation if the mouse leaves the canvas. A dashed aim guide will appear while the ball is stuck, showing the launch direction based on the paddle’s horizontal velocity.

A faint center mark will sit on the paddle to show the angle mechanic even before launch. The full HTML is now ready to be written.

The simulation will fast-forward 1.5 seconds before the first paint. This ensures the initial frame and the 3-second screenshot both show active gameplay, with particles and a falling power-up already visible.

The 1.5-second fast-forward is being reduced to 0.8 seconds. This keeps particles alive for the 3-second screenshot while still showing active gameplay. The ball will hit bricks continuously, so particles should remain visible without a long pre-simulation.

The game will not be pre-simulated. The first paint will show the intact wall with all power icons, and the 3-second screenshot will show active gameplay. I am also checking for bugs that could cause instant death or instant victory.

Collision substeps and ray iterations are bounded, so the update loop cannot loop forever. A defensive check will reset the ball’s velocity to a safe default if it ever becomes NaN or infinite.

The enforceSpeed helper clamps the ball’s vertical speed to ±70 and scales the horizontal component to keep the total speed between 360 and 560. It runs after wall and brick collisions to prevent shallow angles. The collision normals are unit length, so reflection preserves speed correctly.

The ball speed will increase slightly by four pixels per hit, capped at 540. A `drawPowerBadge` helper will render the wide-paddle and multi-ball icons on bricks and falling drops, using cyan arrows for wide and gold/magenta triple orbs for multi. The wide-paddle effect will also draw energy wings. The code will be kept clean and comment-free.

Bricks will gain a subtle 3-pixel top highlight for a 3D bevel. Tough bricks will get a double inner stroke. Damaged bricks will show a dark overlay and cracks. Brick colors will vary slightly per row instead of using flat stripes.

Brick colors will use HSL with row-specific hues and slight lightness variation. Damaged bricks will shift to a darker shade. Power badges will sit on a dark backing circle so the icon remains visible against any brick color.

Falling power-ups will be drawn larger and labeled. Catching them will use a simple axis-aligned bounding box check against the paddle. A window resize listener will adjust the canvas without resetting the game state. If the container has no height, the renderer will fall back to a minimum scale of 0.4 and retry on the next animation frame.

`touch-action: none` will be added to the canvas and frame to prevent mobile browsers from intercepting paddle drag gestures as scroll or zoom actions. The final code will be reviewed carefully before outputting. The response will contain only a single fenced code block with the complete HTML file, with no text before or after it.

The demo AI will track the fastest ball heading toward the paddle and aim to intercept it. If no ball is moving fast enough, it falls back to the ball nearest the center. This keeps the paddle in the right place without needing a full physics simulation.

The demo AI now predicts the ball’s future horizontal position by simulating up to thirty substeps. It checks whether the ball will hit the paddle before the walls, and if not, it sways the paddle slowly toward the ball’s current x-coordinate.

The AI paddle will move at 720 pixels per second to intercept the ball. The prediction function clamps the ball’s horizontal position after wall bounces, but it does not account for brick collisions that occur between the current position and the predicted point, so the AI may misaim if the ball is still inside the brick field.

The AI will only use its ball-prediction logic when the ball has already cleared the brick field, which ends near y=230. While the ball is still inside the brick area, the AI will simply center the paddle on the ball’s x-coordinate to avoid oscillation. This keeps the paddle calm during brick collisions and only switches to prediction once the ball is free of the wall.

The demo AI speed is raised to 980 pixels per second, slightly faster than the player’s 700, so it can reliably intercept the ball. To prevent jitter, the AI snaps directly to the target when the paddle is close enough; otherwise it moves at the maximum speed. The initial ball trajectory is also tuned to 170 by -400 so it strikes the top rows of bricks immediately instead of looping harmlessly.

The collision normal for the top row is being verified. Because the ball sits below the brick, the normal should point downward, which would push the ball back down and prevent it from tunneling through the ceiling.

The paddle collision logic is being verified. When the ball center sits inside the paddle, the overlap helper picks the smallest side of the rectangle. If the ball is just inside the top edge, the normal is set to (0, -1), which correctly pushes the ball upward and prevents tunneling.

The paddle collision will use a dedicated AABB overlap check instead of the generic resolver. This keeps the logic clear and avoids tunneling.

The paddle collision helper now uses a simple distance check that correctly handles cases where the ball center sits inside the rectangle. It also adds early-exit guards to skip collisions when the ball is moving upward and above the paddle, or when it has already passed below it.

The paddle collision now calculates a horizontal offset from the center, maps it to a bounce angle, and sets the ball’s velocity to a speed scaled by that angle. The ball is placed just above the paddle, and the paddle flashes briefly. If the ball is stuck above the paddle, the early-exit guard skips the collision, but the ball is pushed upward so the overlap clears on the next frame.

The physics substeps are verified: two-pixel steps prevent tunneling, and the move-walls-paddle-bricks order handles simultaneous collisions cleanly. Dead balls will be removed after the life check so they do not draw trails.

When a ball dies, it will spawn a burst of fade particles at the bottom of the screen for visual feedback. The score values and floating point text will be added at each brick position. The score display will use tabular numeric digits to keep the numbers aligned.

The lives display will use Unicode heart emojis instead of glowing orbs. A keyboard hint will show the exact start phrase. A subtle vignette will be drawn at the end of the canvas. The final file is estimated at 700 to 900 lines. Power-up drops will spawn at the brick center and fall downward.

Power-ups will be drawn as rounded rectangles. Catching one triggers an immediate visual burst and applies the effect. The wide-paddle power-up lasts ten seconds and spawns a cyan burst at the paddle center.

Power-up catches will display floating text labels and award 200 points. The wide-paddle effect lasts ten seconds. The multi-ball effect spawns three balls. A small legend inside the canvas was considered but rejected as too cluttered.

The layout was checked against a 1280×800 viewport. The canvas scales to 1056×660, which fits the stage height exactly with no overflow. On a 375px phone screen, the stage shrinks to roughly 654px tall, leaving enough room for the header, footer, and padding.

The flex layout prevents horizontal overflow by shrinking the stage to fit the 375px viewport. The body’s overflow:hidden setting also blocks vertical scrolling, so the canvas will scale to the available height without adding scrollbars.

The start banner and title fit comfortably within the 375px viewport. The stats row will wrap with flex-wrap and shrinkable items to prevent overflow. Horizontal overflow will be suppressed on the html, body, and frame elements.

The frame width will be set explicitly to the scaled canvas size rather than relying on max-width, so mouse coordinates stay aligned with the BASE dimensions. The app container will also receive overflow:hidden. To avoid a generic look, the background will feature concentric arcade rings drawn in CSS behind the playfield, plus a subtle hex or dot grid rendered in the canvas.

The background will use a vertical gradient from deep purple to near-black. A subtle dot grid of 1.5-pixel blue dots will be drawn across the canvas. A moving sheen effect is also being added to the bricks.

A translucent polygon sweep will animate across the bricks as a moving sheen. The start banner will include a "DEMO" ribbon to mark it as a demo. The game title will remain in the HTML page title rather than inside the canvas.

The end overlay will use a CSS class to switch between victory and defeat colors. Victory titles will be gold and defeat titles magenta. Confetti particles will use matching colors on victory. The overlay will not use a form element.

The restart handler will listen for both pointerdown and click events on the overlay to ensure it fires reliably across browsers. A keydown listener will also restart the game on any keypress. This avoids double-restarting when the Enter key activates the button.

The restart function will be idempotent, so calling it twice is harmless. It will hide the overlay, reset the game state, and switch the mode back to player. The end overlay click handler will use stopPropagation to prevent the event from bubbling up to the window and triggering other listeners.

A window-level pointerdown listener will ignore clicks on the end overlay. This prevents the game from restarting when the user clicks the restart button.

`preventDefault` will be applied only to pointerdown events on the canvas itself, not globally, to avoid interfering with the restart button. The ball radius is set to 9 pixels for visibility at the current scale, and the ball speed is fixed at 430.

Multi-ball will use three distinct colors: cyan, pink, and gold. Each ball will draw a radial gradient from white to its assigned color, and its trail will match that hue. A power timer bar will be drawn on the paddle or beneath the HUD to show how long the active power-up lasts.

The power timer bar will be a simple width-driven fill bar that appears only during the wide-paddle effect. The header is being refined with a brand section and a stats row containing score and lives.

A "LEVEL 1" chip will sit in the brand area. Brick cracks will be drawn with a stable, seeded pattern based on each brick's column and row so the damage stays consistent frame to frame.

The crack pattern uses three diagonal strokes in white with a dark shadow offset one pixel down and right. The damaged brick fill will also be darkened so the crack is clearly visible.

Tough bricks will use a `hitsRemaining` counter that drops from 2 to 1 on the first hit, triggering a crack, a small burst, and a 50-point score. The second hit destroys the brick for 150 points, a larger burst, and a power-up drop. A `circleHitsRect` helper will handle ball-to-brick collision detection using the exact drawn rectangle. When multiple bricks are hit in one frame, the collision resolver will process the deepest-penetration brick first.

A 0.06-second cooldown on each brick prevents the ball from re-hitting the same brick in the next frame. If the ball overlaps two bricks at once, the first collision pushes it out and the second brick is processed in the next loop. The cooldown on the first brick stops it from being hit again, so the ball does not get stuck or pushed back into the second brick.

The collision resolver will use a five-iteration loop to process the deepest-penetration brick each frame. Cooldown will be reserved strictly for damage, not overlap resolution, to prevent the ball from getting stuck inside a brick.

The collision resolver pushes the ball out by the overlap depth plus a small buffer, then reflects its velocity across the normal vector. It enforces speed limits after each bounce. Damage is applied only when the brick cooldown is expired, and the cooldown is set to 0.08 seconds. Multiple substeps per frame should prevent the ball from getting stuck oscillating between bricks.

A final `separateBall` pass will nudge the ball out of any remaining overlap after the substeps finish. The paddle collision check will run both during and after the brick loop, and the ball will be clamped to the ceiling to stop it from passing through the top edge.

A `clampBallToWalls` check will run after each brick collision to keep the ball inside the viewport. The demo AI will move the paddle smoothly. The ball trail will store one point per frame instead of every substep, capped at twelve points. A whoosh particle burst will intensify when the ball is launched.

The ball trail will be drawn before the ball itself. A launch hint and a center marker will be added to the canvas. Lives will be rendered only in the HTML header, not duplicated on the canvas. A CSS pop animation will flash the score when it changes.

The score pop animation will be triggered by toggling a CSS class and forcing a reflow with `offsetWidth`. The start overlay will display "Click or press a key to start" with a subtitle explaining mouse and arrow controls plus space to launch. The developer is now ready to write the complete HTML file.

A small "DEMO" watermark will sit in the top-right corner of the playfield during demo mode. The banner will be hidden while the player is active. Both the victory and game-over overlays will include a large "Play Again" button with a small text hint underneath to press any key to restart.

The ball will be split into substeps of 2 pixels to guarantee it never passes through bricks or the paddle. At the current speed this yields roughly 420 collision checks per frame, which is trivial for modern browsers.

The ball cannot tunnel through bricks or the paddle in a single frame at the current capped delta time. The expanded paddle height is 32 pixels, which the ball’s 27-pixel movement barely covers. Substeps are still added as a safety margin, and the ball speed may be raised to 700 so the 35-pixel jump exceeds the 32-pixel expanded paddle boundary.

Ball speed is capped at 520 and delta time at 0.033 seconds. Substeps are 4 pixels. The ball cannot fit through the 6-pixel gaps between rows. It will hit every brick in its path.

The gap between bricks is set to 5 pixels. A faint starfield will be drawn once from a cached array to add subtle background texture. The scrollbar will be hidden to keep the layout clean.

A single per-frame sheen gradient will sweep across the brick area for a polished look. The ball will also draw a soft radial glow behind it.

Shadow blur is skipped for consistent performance. The ball glow will use a radial gradient with a soft cyan fade. To avoid drawing seventy gradients per frame, bricks will use a solid fill plus a highlight rectangle instead.

Each brick will store precomputed HSL colors for its normal, dimmed, and glow states. A soft glow rectangle behind every brick is being considered, though it might look muddy.

Bricks will use a drop shadow offset to feel raised, paired with a top highlight and a 1-pixel outer stroke. The paddle will draw a linear gradient glow that turns pink during the wide-paddle effect.

The paddle glow gradient will shift to pink during the wide-paddle effect and back to cyan otherwise. A soft cyan glow will be drawn behind the paddle before the main shape. A dark center marker will sit in the middle of the paddle. A white flash will render when the paddle is hit.

The paddle hit flash will animate across its width using a `hitT` value that runs from 0 to 1. A floor danger indicator will be added to the bottom of the playfield. The developer will write the entire file in one pass and mentally review it for bugs afterward. Flash timers for both the paddle and bricks will be decremented in the update loop so the draw function stays pure. A white overlay with alpha equal to `brick.flash * 0.7` will render when bricks are hit.

The demo reset timer will be set to 1.2 seconds when the game ends in demo mode, then decremented in the update loop to trigger a restart. During gameover and victory phases, the main ball physics and collision checks will be skipped so only particles remain active. Powerup falling will continue during gameover, but the paddle will ignore them. The update loop will branch by phase: playing updates balls and checks for victory, lifelost handles the death sequence, and ready keeps the ball stuck at the paddle.

Powerups will accelerate downward and can be caught during the ready phase if they fall from a prior life loss. The multi-ball spawn logic must handle the stuck ball that has zero velocity.

The multi-ball spawn logic must special-case the stuck ball in the ready phase. atan2(0,0) returns 0, which would launch the new balls horizontally to the right. The code will instead launch the original ball first, then spawn the two extras with a small spread angle.

The multi-ball spawn now uses a -0.48 and +0.48 angle spread around the original ball’s direction. A guard will cap the total ball count at six to prevent spawning too many.

The multi-ball spawn logic will add two extra balls whenever the player has fewer than three live balls, then cap the total at six to keep the game playable.

A `makeBall` factory will assign each ball a unique serial number and a color hue cycling through cyan, pink, and gold. The developer is now verifying that launching from the ready phase correctly uses the stuck ball.

The launch function now clamps paddle velocity to a ±0.55 angle range so the ball never launches too sharply. In demo mode the initial ball is set to playing phase directly, while post-life-loss launches still use this angle logic.

The respawn function will place a new stuck ball at the paddle center and immediately launch it in demo mode. Existing powerups will be preserved across respawns. The developer is now ready to write the complete HTML file.

A live countdown will appear above the paddle while the wide-paddle power-up is active. It will read "WIDE X.X" and sit near the top-right corner to avoid overlapping the stuck ball.

The wide-paddle countdown will be shown only in the HUD and by the paddle color change, so no extra canvas pill is needed. The ball count will also stay in the HUD. The developer is now writing the complete HTML file.

The developer confirmed that applying powerups and spawning particles during brick iteration is safe because those arrays are separate from the brick list. They also began adding a text-floater system to display temporary score and power-up messages.

Text floaters will use a 14px bold font with a rising position and fading alpha. The Play Again button will be keyboard accessible, and a restart key hint will be added. Neon vertical rails will be drawn along the left and right walls using a cyan gradient.

The wall collision boundary is set to 4 pixels so the ball touches the inner edge of the 4-pixel rails. The paddle is clamped to 4 pixels from the left and right edges.

Bricks are inset by 16 pixels to avoid touching the side rails. The bottom of the playfield will fade to red. The exact start-screen message will read "Click or press a key to start". The AI will begin playing immediately on the first frame. A single initial draw will run before the first animation frame so the canvas is not blank.

A fallback sizing function will estimate canvas dimensions from window width and height if the stage element is too small. Resize events will be hooked on both the window and the first animation frame to catch layout shifts. The canvas transform will be replaced with setTransform, and setting canvas.width will clear the drawing surface.

The resize function now compares the current width, height, and device pixel ratio against cached values. It only recalculates the canvas dimensions and clears the drawing surface when any of those values actually change, preventing unnecessary redraws.

Mouse coordinates are normalized using getBoundingClientRect divided by the base resolution, keeping them independent of device pixel ratio. The canvas transform is reset at the start of every frame to remain safe after any resize or reset.

Pointer events will handle both mouse and touch input. The body will use `user-select: none` to prevent text selection during play. The game frame will receive a `tabindex` so clicking it focuses the canvas and lets keyboard controls work immediately.

The developer will attach the keyboard listener to the document rather than the window so it works inside the iframe. Arrow keys and the spacebar will have their default browser behavior prevented to stop scrolling. Legacy key names like "Left" and "Right" are being skipped. The implementation is now considered complete.

The victory check now requires at least one brick to exist and all to be dead, preventing an instant win if the brick array is accidentally cleared. The victory overlay will appear immediately but remain semi-transparent so the final particle burst remains visible.

Victory triggers a burst of 80 confetti particles falling from above the playfield. The game ends immediately for human players, while demo mode waits 1.4 seconds before restarting. A particle cap is being added to keep the system from spawning too many.

A soft screen flash will accompany victory. The wide-paddle power-up lasts exactly 10 seconds. The multi-ball power-up gives the player 3 total balls. Bricks will show visible power-up icons before they break. Particle bursts will fire both when a brick cracks and when it fully breaks, with the break effect being larger.

Tougher bricks will show a double outline and diagonal stripe texture before they are hit. A small shield mark will also appear on bricks that need two hits.

The power-up icon will be drawn after the diagonal stripes so it remains visible. Tough bricks will show one or two small pips in their corner to indicate remaining hits, with the crack lines serving as the visible damage indicator.

The developer is refining the brick rendering to prevent the power-up icon from overlapping the corner pips. They are considering moving the pips to the top-left corner or slightly above the icon center so the two elements remain clearly visible.

The power-up icon will sit at the center of each brick, while the corner pips will move to the top-left corner. The developer confirmed the two elements fit without overlapping by adjusting the icon radius and pip positions. Both indicators will remain visible together for tougher bricks.

The developer finalized the playfield layout. Brick height is 26 pixels, with seven rows and ten columns. The ball will start at y=320 so it does not overlap bricks. The paddle will sit at y=556 with a height of 14 pixels. The canvas height is set to 600 pixels.

Constants will be defined at the top of the script. The paddle will follow the mouse directly with no smoothing, while arrow keys will drive velocity-based movement. A `lastInput` flag will track whether the player used the mouse or the keyboard, letting the paddle snap to the mouse cursor when switching from keys to mouse, or continue from its current position when switching back. In demo mode, this flag will be ignored. If the player has not yet moved the mouse or pressed keys, the paddle will start centered.

The developer is finalizing the transition from demo to player mode. When the player begins, the start overlay will hide, the end overlay will clear, and the mouse cursor will be locked to the paddle center so the paddle does not snap to the cursor. The game will reset to a fresh state with a centered paddle. All pieces are now in place for the final file.

A visual launch arrow will appear in the ready state. When a life is lost, a large "LIFE LOST" message will fade in the center of the screen. The developer decided not to include the remaining lives count in that message since the HUD already displays it. The final output will be a single clean code block.

The end overlay will only appear for human players, not during demo mode. A bonus of 200 points per remaining life will be added to the final score on victory. Game over screens will display the base score without this bonus.

The developer is adding phase guards to prevent duplicate victory or life-loss calls. The update loop will check for victory before checking for dead balls, ensuring a last brick break triggers a win even if the ball exits the playfield in the same frame.

The developer decided against adding Web Audio to avoid console warnings and errors. The script will run in strict mode and contain no console.log or debugger statements.

The developer is now writing the actual code. They defined strict mode and the base resolution constants. They will cache every DOM element at startup and null-check them to avoid runtime errors from typos.

The power-up HUD will use a gradient-filled progress bar that grows as the wide-paddle timer ticks down. The status text will display the active power-up name and remaining seconds, such as "WIDE 8.1s" or "WIDE 8.1s · ×3" when both effects are active.

Score will be integer-only. Brick power cells will be tracked as a Set of strings. The developer is now writing the final source code.

Fourteen power-up bricks are being placed across the seven rows so icons remain clearly visible in the screenshot.

The developer settled on 11 power-up bricks instead of 14, placing them across the wall so the icons remain clearly visible. They are now writing the final source file and will draw each brick with a dedicated `drawBrick` function, rendering the power-up icon above the crack lines so it stays readable.

A pulsing cyan and magenta border glow will animate the start banner for a polished look. The overlay text will read "Click or press a key to start" and include a brief help line about mouse and keyboard controls.

A footer legend with four brief text hints will sit below the canvas. It will wrap on narrow screens to save vertical space.

On narrow screens the footer will hide the last two hints so only the two most important remain visible. The header tagline will also shrink on mobile. The developer is now writing the final source and carefully checking that canvas transforms are applied before save/restore calls to avoid distortion. A shake effect is being added for dramatic moments.

The shake effect will be applied inside a canvas save/restore block so only the world layer moves. The HUD and HTML overlays will remain fixed. The life-lost flash will be drawn after the restore call so it stays in screen space. GlobalAlpha will be reset to 1 after any temporary alpha draws.

The ball speed constant is set to 440 pixels per second. The initial demo ball starts at x=190, y=300 with a velocity of 190 by -400.

The developer added a `setVelocity` helper that normalizes a desired direction vector to a fixed speed of 440 pixels per second. The demo ball will launch with a deterministic velocity of 190 by -397 instead of random noise.

The developer confirmed the demo ball will strike the brick wall rather than pass through gaps, since the 18-pixel ball is wider than the 5-pixel gaps. They are now implementing the main game loop using `performance.now()` for delta time, clamping the frame step to 33 milliseconds to keep movement smooth.

The developer will set `lastTime` to `performance.now()` immediately before the first `requestAnimationFrame` call so the initial delta time is small. They also plan to add a final overlap check at the end of each update: if the ball’s previous position crossed the paddle top, it will be bounced back to prevent the ball from ever passing through the paddle.

A swept-line collision check will be added inside each substep to catch tunneling through the paddle top. The developer is drafting a `resolvePaddleSwept` helper that tests the ball’s full movement segment against the paddle surface.

The `resolvePaddleSwept` helper now implements the exact collision logic. It first checks if the ball’s full movement segment crosses the paddle top surface. If the segment intersects the surface, it snaps the ball to that exact point and bounces it off the paddle. If the ball remains entirely below the surface, it falls back to the standard overlap check.

Side paddle hits are handled by the standard overlap resolver, which bounces the ball upward onto the paddle surface. The swept-line test only triggers when the ball crosses the top edge within the paddle’s horizontal bounds, preventing any tunneling through the top line. This ensures the ball never passes through the paddle.

The developer is now coding the `bounceFromPaddle` function. It calculates the ball's horizontal offset from the paddle center, clamps it between -1 and 1, and maps that offset to a bounce angle of up to 1.22 radians. The ball speed is clamped between 360 and 540 pixels per second. A hit flash is triggered on the paddle, and the offset is stored for visual feedback.

The paddle bounce angle is finalized at 1.18 radians, giving a sharp angle near 66 degrees from vertical while keeping a small upward component. Deterministic ball behavior is preserved for skill. The developer is now writing the complete source file.

Subtle left, center, and right tick marks will be drawn on the paddle to visually signal the bounce zones. No favicon or external assets will be included. Before finalizing the file, a quick pass will check for unmatched braces, undefined variables, and typos in element lookups.

A radial glow behind the brick field and a neon reflection under the paddle will add polish. The CSS page background will complement the canvas so letterboxing looks intentional. A watermark inside the canvas was considered but skipped to avoid clutter.

The start banner is capped at 460 pixels wide and sits semi-transparent at the vertical center of the canvas. The brick wall occupies the top 38 percent of the 600-pixel height, so the banner sits below it and the ball remains faintly visible through the glassy overlay.

The start banner will sit in the lower third of the canvas, above the paddle and below the brick field. This keeps the ball path and bricks fully visible. The overlay will use flex-end alignment with bottom padding.

The start banner now uses a bottom-heavy gradient that fades from dark blue to transparent, keeping the top of the canvas clear for the brick wall and power icons. The 64-pixel bottom padding places the banner near the paddle without hiding it, and the banner itself is roughly 90 pixels tall.

The banner will use a lighter bottom-heavy gradient so the paddle stays bright beneath it. On mobile, the banner and bottom padding risk covering most of the game area, so the developer will reduce those on small screens.

A media query for screens under 520 pixels will switch the start overlay to vertical centering and remove its background gradient. The banner shrinks to a compact size with minimal padding so it no longer covers the paddle on small viewports.

The mobile banner will shrink to 15 pixels for the heading and 11 pixels for the body, with the eyebrow label hidden. On desktop it stays at the bottom. The developer is now writing the final source. Power-up icons will be large and high-contrast: white or gold on a dark circle, 18 pixels inside a 26-pixel brick. A `drawPowerIcon` helper is being drafted to render them.

The `drawPowerIcon` helper is being implemented with a dark blue fill and a 1.4-pixel stroke. The wide-paddle icon uses a cyan outline and a white center, while the multi-ball icon uses a gold outline and a white center. The wide-paddle icon draws a horizontal bar with diagonal wing strokes.

The `drawPowerIcon` helper is now complete, with the multi-ball icon drawn as three small white circles. Canvas state is properly saved and restored so line caps do not leak between calls. Falling power-ups will bob and wobble horizontally as they descend, and their x-position will be clamped to keep them inside the play area.

A viewport meta tag will be added for mobile scaling. Buttons will use a custom gradient with dark text and uppercase styling to avoid native browser controls.

Button styling is finalized with pill-shaped corners, hover brightness, and a visible focus outline. The score and lives will remain in the HTML header rather than inside the canvas, since the sandboxed iframe screenshot captures the full page.

The HUD will be drawn immediately in `resetGame` so the first frame shows lives, score, and power status. Lives will render as heart icons. The developer is now writing the complete source, prioritizing correctness over keeping it under 800 lines. They also verified that the start overlay’s `pointer-events: none` allows clicks to pass through to the canvas and bubble up to the window handler.

All pointer logic will be consolidated into a single window-level `pointerdown` listener. This prevents double-handling clicks between the canvas overlay and the end overlay. The handler will check the event target to determine whether to start the game, restart from the end screen, or ignore the click.

The window-level pointer listener will also update the mouse position on every click so the paddle jumps to the cursor immediately. This prevents the paddle from lagging behind the first movement after the game starts.

The developer caught a bug in the demo-start sequence: `playerStart()` resets the paddle to the center, which would overwrite the mouse position set by the click handler. The fix is to call `updateMouse(e)` after `playerStart()` so the paddle jumps to the cursor instead of snapping back to the middle.

The developer is now drafting the keyboard `keydown` listener to handle game starts. If the player begins with arrow keys, the mouse position may be stale, so the paddle will remain centered until the user moves the mouse or presses keys.

The keyboard listener now blocks default scrolling for arrow keys and space. It starts the game from the demo screen, restarts after a win or loss, and sets the left or right key flags. Space, arrow up, or spacebar will launch the ball.

The developer verified the keyboard interaction flow. Pressing space during the demo starts the game and immediately launches the ball. Pressing an arrow key starts the game but leaves the ball frozen until the player presses space or clicks. If the player presses a key while the game is over, it restarts the match. They also noted that if the demo timer somehow triggers a game-over state, a key press should start a fresh player game instead of restarting the demo.

The developer confirmed the keyboard event flow is safe: after `playerStart` switches the mode to player, the phase becomes ready and the gameover check is skipped. They also noted that pressing space during the end screen restarts the match but leaves the ball frozen, so the player must press another key to launch it. They briefly considered falling through to launch on space to make that transition smoother.

The keyboard listener now falls through after restarting from the end screen. Pressing space or arrow up immediately launches the ball, while arrow keys also set the movement flags so the paddle starts moving right away.

The developer confirmed that holding space during the game-over or victory screen will not trigger repeated restarts. After the first key press restarts the match, the phase becomes ready and subsequent key repeats are ignored. If the player presses space again, the launch function does nothing because the game is already in progress.

The developer added a `keyup` listener to clear the arrow key flags when the player releases them. They also plan to listen for the `blur` event so that keys do not get stuck if the player alt-tabs away from the game.

A `blur` listener will clear the arrow key flags so they do not get stuck if the player alt-tabs away. The `updateMouse` helper now converts pointer coordinates to the canvas coordinate system and marks the cursor as inside the play area. Mouse clamping will be deferred until the paddle position is applied.

The pointer-move listener will only update `lastInput` to `'mouse'` when the cursor is inside the play area. The mouse position will be clamped to the paddle bounds before applying it. The developer will also store each brick's hue and CSS color so score floaters and particles can match the brick's original color.

A burst particle effect will fire from each brick break, mixing the brick’s color with white sparks. The ball speed will increase by six pixels per destroyed brick, capped at 520. A combo counter for consecutive hits was considered but skipped to avoid bugs. The canvas context will be configured once at startup with rounded line joins and image smoothing enabled.

The developer decided not to synchronize ball speeds across the multi-ball effect. They will add a bright jagged crack line and a dark chip polygon to make the first hit on a two-hit brick unmistakable. The first hit will trigger a smaller particle burst and a thud flash, while the second hit will produce the full break effect.

Particles will fire on both the crack and the final break so the first hit on a tough brick feels responsive. Destroyed bricks are marked dead and skipped in both rendering and collision. Power-ups only drop after a brick is fully destroyed. The ball collision logic uses the current paddle width, so the wide-paddle effect is active immediately.

The paddle width will lerp smoothly between 112 and 196 pixels. The center point is preserved during the transition so the paddle does not shift visually.

The developer refined the paddle update logic. The center point is computed from the current position and width before the width is lerped, then the paddle is repositioned so the center stays fixed. In demo mode the AI moves the paddle toward a target position, capped by a maximum step size. In player mode the left and right arrow keys control horizontal movement.

The developer noticed a potential double-width lerp bug in the paddle update logic. They are refining the exact sequence of width interpolation, center preservation, and velocity calculation to avoid it.

The developer decided to track the paddle's previous X position to compute velocity. This approach works for all input modes, including mouse flicks and keyboard movement. Velocity will be clamped to ±2000–±2200 so the ball cannot be launched by a single extreme movement.

The AI target function now returns a center x coordinate rather than a left edge. It predicts where the ball will be when it reaches the paddle, and if no ball is coming down it follows the lowest active ball. This makes the computer opponent look attentive and slightly ahead of the ball.

The AI will target the ball that reaches the paddle fastest, or fall back to the lowest active ball if none are coming down. A `brickBottom` variable will be set during brick construction to define the bottom boundary. The `predictX` helper will use a `WALL` constant of 4 so its wall collisions match the physics engine.

The `predictX` helper now simulates the ball’s trajectory up to 40 physics steps. It checks for wall collisions before reaching the target height and bounces the velocity when a wall is hit. The final position is clamped to the play area.

The demo mode will auto-launch the ball immediately when the game starts, bypassing the ready state. If the demo loses a life, it respawns and launches right away so the AI never stays idle. A safety net in the update loop will also trigger launch if the demo is stuck in the ready phase. The launch function will be guarded so it only runs when the phase is ready and the ball array is not empty.

The developer is ready to write the complete HTML file and will review it as a whole after drafting. They are also adding subtle CSS styling to the HUD stat cards so the heads-up display looks like a polished game interface rather than a plain webpage.

The developer corrected the stat card font weight to 700. They are also designing a CSS heart icon using rotated pseudo-elements so the lives display looks consistent across browsers.

The developer abandoned the CSS heart icon as too finicky. Unicode hearts are simpler and more reliable, and Segoe UI and system fonts include them.

The developer switched from Unicode hearts to inline SVG heart icons in the HUD. This avoids font-rendering issues across browsers and operating systems.

The developer is now writing the complete HTML file. They will keep functions reasonably sized to avoid syntax errors. The end screen will include instructional microcopy. They are also running a final check against the requirement list.

The developer confirmed every requirement is met. They will also add keyboard and click launch controls.

The developer will draw a thick bright crack with a dark outline on cracked bricks so the first hit is unmistakable. Collision rectangles will remain unchanged to avoid breaking physics. A "2×" badge will be added to the corner of tough bricks so the mechanic is readable at a glance.

The 2× badge will be drawn immediately so the screenshot shows tough bricks are already distinguishable before any cracks appear. Tough bricks will also use a more saturated border for extra visual contrast. The demo will keep the wall intact at first paint so the full level and power icons are visible, letting bricks begin breaking around the three-second mark so particles appear in the final screenshot.

Particle lifetime is set to 0.85 seconds so fragments linger longer. A continuous tiny ball trail will remain visible at all times. The enforced minimum vertical speed also guarantees the ball returns to the brick field before the three-second mark.

Impact flashes will last 0.2 seconds. Subtle background embers and twinkling stars will keep the scene alive before the first hit. The canvas will use `display: block` with CSS width and height set to 100% so no inline gap appears. Internal resolution is handled by canvas attributes while CSS stretches to the frame size.

The developer will place three heart SVGs directly in the HTML markup so the lives display is visible before the script runs. JavaScript will still update them dynamically, but the initial markup guarantees the header appears immediately on first paint.

The developer will call `renderLives()` immediately at the bottom of the script, before the first animation frame, so the lives display is already populated on the very first paint. The score will also be seeded in the HTML markup as "0" so it is visible before JavaScript runs. They are now composing the final answer. They also verified that `arcTo` with a zero radius behaves correctly as a straight line, and confirmed the brick corner radius is 5.

The developer finalized the visual geometry: the paddle is a capsule with half-height radius, and power-ups are drawn as 11-pixel-radius circles. They will enable image smoothing and clear the canvas each frame using `clearRect` after applying the device-pixel transform, ensuring the logical area is fully wiped before every redraw.

The developer will use a div for the power-fill element instead of an icon tag for clarity. The bar will remain visible as an empty track when no power-up is active. A different fill color may be used when multi-ball is active.

The power-fill bar will use a cyan gradient when the wide-paddle effect is active, and a pink-to-yellow gradient when multi-ball is active. A CSS transition will animate the width change smoothly. The developer is now ready to output the complete HTML file.

A dotted aim line will appear during the player ready state to show how paddle speed affects the ball angle. The line length and direction are calculated from the paddle's horizontal velocity. It is hidden during the demo so it does not appear in the three-second screenshot.

The aim line is drawn with a 2-pixel stroke and a dashed pattern. A launch hint is positioned just above the paddle so it does not overlap the stuck ball.

The start and end overlays will be absolutely positioned inside the game frame and hidden by default. They will appear as full-screen centered flex containers with a dark blurred background. The end overlay will sit above the canvas at z-index 3, the start overlay at z-index 2, and the canvas at z-index 1.

The developer is now writing the CSS. They will add a radial highlight to the page background. The layout will use a flex column so the footer never overflows the viewport. The banner pulse will remain even for users with reduced-motion preferences.

The developer is setting the HTML and body to full viewport height with hidden overflow. They are adding a fallback color to the heading gradient so the text remains readable if background-clip text fails. A clean font stack will be used throughout.

The developer abandoned the `background-clip: text` gradient for the heading because it could render invisible if unsupported. They will instead use a solid bright color with a soft cyan text-shadow glow. The word "NEON" will be wrapped in a `<span>` with its own cyan tint.

The developer will use compact but readable CSS. They will guard the score element against null before reading its offset width. No web fonts are used, so the initial resize will not shift the layout. A global `box-sizing: border-box` rule will be added. The end card will use a dark translucent background with a faint cyan border.

The end card will use a 18-pixel border radius, 22 pixels of top padding, and 28 pixels on the sides. Its width will be capped at 380 pixels or 90 percent of the frame, whichever is smaller, to prevent overflow on narrow viewports. The restart button will stretch full width on mobile.

A `separateFromSolids` helper will be added to `stepBall` to push the ball out of any bricks it still overlaps after the substeps finish. This final safety check prevents the ball from ever passing through bricks or the paddle.

The developer confirmed the final collision safety check will not double-damage bricks. The brick cooldown timer prevents duplicate hits even when the ball is pushed out of overlaps after the substeps finish.

The developer verified that the final collision safety check preserves ball speed and does not alter velocity. They also made the brick damage function idempotent by checking the alive flag first, preventing duplicate hits on already-broken bricks.

The developer finalized the brick break logic. Normal bricks award 100 points and tougher ones award 150. Each break spawns 18 colored fragments, 6 white ones, a floating score, and a shake. The ball speed increases by 6 on every break. If a power-up icon is present, it drops from the broken brick. The HUD flag is set to refresh the display.

The developer confirmed the collision cooldown prevents double-damage even if the ball overlaps a brick after the substeps finish. They also decided to call `updateHUD` at the end of every frame rather than using a dirty flag, since the function no-ops when the score is unchanged and still triggers the score-pop animation.

Floating score labels will be capped at 24 to prevent memory bloat. Falling power-ups will cast a visual drop shadow for visibility. Power-brick icons will pulse with an animated stroke so they stand out even in the screenshot.

The developer finalized the power-brick glow effect. Wide-paddle icons pulse cyan while multi-ball icons pulse gold. The demo loop will auto-restart without showing the end screen. Player input during the brief demo game-over state resets the game cleanly.

A `demoHold` timer will delay the demo reset by 1.2 seconds after the ball dies. During that hold the game stays in a game-over phase so the start overlay remains visible and the AI launch angle is ignored.

The AI paddle speed is set to 1100 so it almost never misses the ball in the first few seconds. The demo will show three lives because the AI recovers after a few misses. The developer is also checking whether the initial ball could hit the side wall and drop before the AI moves.

The AI has enough time to track the ball before it returns. The paddle will start aligned with the ball's x position so the AI does not need to chase it. During player-ready, the paddle and ball will be centered together.

A `clamp` helper and a `formatScore` utility will be added. The CSS will stay compact but polished. To guarantee the three-second screenshot shows broken bricks and cracks, a silent warmup run of 1.2 seconds will precede the first paint.

No silent warmup will run. The level will be fully visible on the first paint, and the three-second screenshot will show active gameplay with broken bricks, cracked two-hit bricks, and recent particles.

The developer calculated the initial ball's path to place a power brick in its trajectory. At the ball's starting speed and angle, it reaches y=200 around column 4. They mapped the brick grid coordinates to confirm the ball would intersect that column and row, ensuring an early power drop.

The developer placed a power-up brick at column 4, row 6 so the ball hits it first. They also added a tough brick nearby so a crack might appear. Row 6 is the bottom row closest to the ball, ensuring early action in the screenshot.

The developer finalized the brick layout to guarantee visible cracks in the screenshot. Rows 0 and 1 will be all-tough, row 2 will have even columns tough, and row 6 will have columns 3 through 5 tough. This ensures the first impact on row 6 cracks the brick, and the crack persists if the ball bounces away before the second hit.

The developer placed a wide-paddle power-up on brick (4,6) and a multi-ball power-up on brick (5,6). Both are tough bricks, so the crack persists until the ball hits them again. This guarantees the screenshot will show either a cracked brick or a falling power-up, satisfying both requirements.

The developer added an `aimAtBrick` helper to position the ball at the exact center of a target brick and fire it straight up. This guarantees the demo will hit column 4 on the first bounce.

The ball speed of 440 is sufficient to bounce back and hit multiple bricks before the three-second mark. The developer will also offset the initial angle slightly so the ball does not return to the same brick immediately after the crack bounce, preventing a second hit before the screenshot.

The developer is now writing the file. They will mark cracked two-hit bricks with visual stripes and a 2× label instead of text. A POWER_AT lookup table will assign wide-paddle and multi-ball icons to specific brick coordinates.

The developer finalized the power-up placement and tough-brick rules, then added a ball specular highlight and a trailing effect. The full source file is expected to be roughly 900 to 1100 lines.

The developer will write the code in a structured way and count braces mentally to avoid nesting errors. They will avoid deeply nested template strings. The restart button will use `type="button"` to prevent accidental form submission. They decided against `will-change` and `backdrop-filter` to avoid rendering issues. They also rejected wrapping the game loop in a try/catch that would swallow errors, preferring to write correct code instead.

The developer is double-checking the `circleRectSep` collision helper to ensure it correctly handles the case where the ball center sits inside a brick. This prevents the ball from clipping through bricks or the paddle.

The developer confirmed the collision math is sound: outward normals push the ball away from bricks, and reflection uses the negative dot product. They will also test the paddle with a swept check against the ball’s previous position before each discrete substep. The `stepBall` function will subdivide movement into steps based on ball speed and delta time.

The `stepBall` function splits each frame into substeps. It updates the ball position, clamps the walls, resolves the paddle and bricks, and checks if the ball fell below the play area. If the ball dies, it triggers a burst of red particles. After all substeps finish, it runs a final safety pass to catch any missed collisions.

The developer is writing the `resolvePaddle` function to prevent double bounces. It only triggers when the ball is moving downward and crosses the paddle top plane. This ensures the ball cannot hit the paddle twice in one frame.

The developer added a swept-line collision check to `resolvePaddle`. It interpolates the ball's exact crossing point against the paddle top plane and verifies the contact position falls within the paddle's horizontal bounds. This prevents the ball from clipping through the paddle or bouncing twice in one frame.

The `resolvePaddle` function now includes an overlap fallback using `circleRectSep` for side or embedded collisions. It rejects hits below the paddle's lower edge to prevent missed bounces. The horizontal contact range includes a small buffer so corner hits register. This prevents the ball from clipping through the paddle.

The developer added a particle spray effect for paddle hits. The `bounceFromPaddle` function now calculates the ball's horizontal offset from the paddle center to set the bounce angle and clamps the ball's speed between 380 and 530.

The developer finalized the paddle hit physics: the ball is launched at the calculated angle with speed clamped between 380 and 530, placed just above the paddle, and a blue particle burst fires at the contact point. They are now writing the complete HTML file, using standard arrow symbols in the instructions with a fallback label for compatibility.

The start banner will display "Mouse or arrow keys · Space launches" as its subtitle. The canvas will show a launch hint after the game begins, while the footer retains the movement instructions. The developer confirmed the start heading text fits the banner width and added responsive CSS to prevent wrapping on desktop screens.

The developer added a high-contrast focus style and disabled `overflow-wrap` to prevent awkward word breaks. They will draw the entire brick field with a single soft shadow rectangle behind it for a grounded look, and batch each brick’s glow by drawing a dark shadow offset rounded rect, the main fill, and a top highlight in sequence.

The developer finalized the brick rendering order: shadow, main fill, top highlight, stripes, crack, 2× label, power icon, and flash overlay. Each brick flash fades by 65% opacity per frame, and cooldowns decay at the same rate. They also began writing the particle update loop.

The particle update loop now applies gravity and age decay to each fragment. It filters dead particles every frame, which is acceptable for fewer than three hundred items. The developer will also rotate each particle square so they spin as they fall, using canvas save and restore for the transform.

The developer finalized the particle rotation and spin values, and will skip a ceiling spark to save time. They confirmed the file will contain only a self-contained code block with no external dependencies, no alerts, no localStorage, and no console calls. The page will include a meta charset and viewport tag, use the title "Neon Breakout", and display a "WAVE 01" level indicator in the header.

The developer added a `clampWalls` helper to keep the ball inside the play area by reversing its horizontal velocity when it hits the left or right walls. They also decided to include a lives count as accessible text for screen readers.

The developer set the wall offset to 5 pixels so the ball sits flush against the inner edge. They verified the brick field spans 16 to 944 pixels, which leaves no gap for the ball to slip beside the bricks.

The developer confirmed the wall-to-brick gap is 14 pixels, which is larger than the 18-pixel ball, so the ball cannot slip through. They will draw the walls after the background so they remain visible. They are now beginning to write the actual code, starting with the brick hue logic.

The developer finalized the seven row colors as a fixed hue array and added a small lightness variation by column for texture. They also wrote the victory and game over screen copy, using playful phrases like "WALL CLEARED" and "Every brick is stardust."

The developer added a lives bonus to the victory screen, awarding 250 points per remaining life. They implemented a double-guard against duplicate victory triggers by checking the phase both inside and outside the function. They then began writing the `showEnd` function to render the final score and game-over or victory message.

The `showEnd` function now toggles the victory class on the overlay and fills in the eyebrow, title, subtitle, and score. The `playerRestart` function switches back to player mode and hides both overlays.

The `playerRestart` function now resets the game state and clears the left and right key flags. A `demoHold` timer is added to delay the demo-to-player transition, and it decrements each frame until it expires and triggers a full reset. The developer also confirmed that resetting the game after a victory correctly returns the phase to playing.

The developer confirmed that particles still update during the demo hold because they are processed outside the phase check. They will keep the paddle AI moving during victory so the demo looks alive. They are now writing the complete HTML file and ensuring the layout fills the typical 1280×800 sandbox viewport.

The developer added soft, blurred blue and cyan decorative orbs to the page background. These fixed elements sit in the side margins around the centered canvas, giving the layout a polished, intentional appearance instead of empty space.

The developer decided against placing game instructions inside the canvas, and skipped a scanline overlay to preserve clarity. Instead, they will add a thin inner border and rounded corners to the frame via CSS.

The developer finalized the frame’s CSS box-shadow with a 6px dark inset, a soft cyan glow, and a subtle blur. They verified the shadow won’t be clipped by the stage’s overflow hidden because the frame is centered. They also added border-radius and overflow hidden to the overlays so they match the rounded frame edges.

The start overlay and banner will use `pointer-events: none` so they do not block mouse input, while the end overlay keeps normal pointer events. The developer is now ready to write the complete script with all functions.

The developer added a 132-pixel minimum width to the power-up stat to prevent layout jumps. They verified the stats container uses flex wrap so the three stats will stack vertically on narrow screens without causing horizontal page scroll.

The developer finalized the responsive stat styles. Stats flex to 90 pixels with `min-width: 0` so they shrink safely on narrow screens. A mobile breakpoint at 520 pixels tightens padding, increases the stat label size, hides the tag, and widens the heading letter spacing. The power fill bar is confirmed to stretch correctly within its 4-pixel parent container. They are now ready to write the final code and will verify brace counts as they compose.

The developer added a score-pop animation that scales the score label upward and fades it white for 0.28 seconds whenever the score increases. They also set the initial score and power text defaults in the HTML and called `updateHUD()` at startup to synchronize the display.

The score-pop animation will use `display: block` and `transform-origin: left center` so the scale effect anchors properly. The developer will keep the HUD and paddle color enough to signal active power-ups, and will add a large glowing power capsule on the canvas so dropped items are obvious. The first-tough brick will not drop until its second hit.

The power-up icon must be visible from the start so the mechanic is obvious. The ball may return to the brick before the 3-second window expires, so the icon on the cracked brick itself signals the feature. Footer text will also explain the drop behavior.

To satisfy both the immediate drop and the visible crack, the developer will pre-crack a tough brick at initialization so it appears cracked on first paint. A separate one-hit power brick will be aimed at so it breaks and drops right away.

The developer calculated the power capsule’s fall time: at 150 pixels per second, a drop from y=200 reaches the paddle around 2.7 seconds, so the player should catch it before the demo hold expires.

The developer recalculated the power capsule drop timing. It now arrives at the paddle around 2.4 seconds, so the screenshot will likely show the wide paddle active with a timer near 9.5 seconds. The AI must catch the drop to make this dramatic moment visible.

The AI will now prioritize catching falling power-ups. It tracks the lowest active capsule so the demo can catch the drop and show the wide-paddle effect.

The AI now compares falling power-ups and active balls by vertical urgency. It moves to the lower target, catching a capsule if it is closer to the paddle than the ball.

The developer is now leaning toward not pre-cracking any bricks. They feel a naturally cracked tough brick is more honest and will still appear in the 3-second screenshot.

The developer decided against pre-cracking any bricks. Showing a cracked brick before the ball arrives would look like a bug on first paint. The aimed tough brick will crack naturally after the first hit, so the crack appears by the three-second screenshot.

The developer chose to aim at a tough power brick at position (4, 6). The first hit will crack it visibly, and the second hit will drop the power-up. This ensures the cracked brick is present in the three-second screenshot, while the drop timing remains within the demo window.

The game will start with a single ball. Particle lifetime is extended to about one second so brick breaks remain visible through the three-second demo window. A new ring-flash particle type will draw a growing circle around each destroyed brick.

The ring-flash particle will draw a growing circle around each destroyed brick, fading from opaque to transparent as it ages. The developer will reset globalAlpha after drawing particles and wrap each particle in a canvas save/restore to keep the effects safe and isolated.

The developer will add a non-passive `touchmove` listener to prevent mobile browsers from scrolling the page while the player moves the paddle. They are now writing the complete HTML file and will review syntax as they type.

The developer switched to CSS `touch-action: none` for mobile scrolling instead of a JavaScript `preventDefault` listener. They also added `overscroll-behavior: none` to prevent page overscroll. They are now finalizing the paddle collision logic and writing the complete file.