Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
2 min 39 s
Cost
$0.013
Output tokens
22,335
Spent thinking
10,830 tokens
Speed
143.9 tok/s
Console errors
0
First token after
3.8 s
First visible text after
1 min 37 s
Prompt tokens
692
Code size
33.0 KB
Finish reason
stop
Served by
Parasail
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"high"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 955 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Neon Breakout</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0}
  body{
    min-height:100vh;
    background:
      radial-gradient(1200px 700px at 50% -15%, #1d2a63 0%, rgba(10,14,32,0) 62%),
      radial-gradient(900px 600px at 100% 110%, #3a1c5e 0%, rgba(10,14,32,0) 60%),
      #070b16;
    color:#e9edff;
    font-family:"Segoe UI",system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif;
    display:flex;flex-direction:column;align-items:center;
    padding:14px 12px 26px;
    -webkit-font-smoothing:antialiased;
  }
  .wrap{width:100%;max-width:980px}
  header{
    display:flex;align-items:center;justify-content:space-between;
    gap:12px;padding:2px 6px 12px;
  }
  .logo{display:flex;align-items:center;gap:10px;min-width:0}
  .logo .dot{width:13px;height:13px;border-radius:50%;background:linear-gradient(145deg,#7dd3fc,#a78bfa);
    box-shadow:0 0 14px #7dd3fc,0 0 26px rgba(167,139,250,.6);flex:none}
  .logo h1{
    font-size:clamp(15px,2.4vw,21px);margin:0;letter-spacing:4px;font-weight:800;
    background:linear-gradient(90deg,#7dd3fc,#a78bfa 45%,#f472b6);
    -webkit-background-clip:text;background-clip:text;color:transparent;white-space:nowrap
  }
  .tag{font-size:11px;letter-spacing:2px;color:#7f8bb8;text-transform:uppercase;white-space:nowrap}
  .stage{
    position:relative;width:100%;aspect-ratio:4/3;border-radius:16px;overflow:hidden;
    background:#0a0f22;
    box-shadow:0 26px 70px rgba(0,0,0,.65), 0 0 0 1px rgba(120,150,255,.16), inset 0 0 60px rgba(0,0,0,.6);
  }
  canvas{display:block;width:100%;height:100%;touch-action:none;cursor:none}
  .overlay{
    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    background:linear-gradient(180deg,rgba(6,10,24,.55),rgba(6,10,24,.72));
    backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
    padding:16px;transition:opacity .25s ease;
  }
  .overlay.hide{opacity:0;pointer-events:none}
  .panel{
    text-align:center;padding:clamp(18px,3.4vw,34px) clamp(20px,4vw,44px);
    border-radius:16px;max-width:92%;
    background:rgba(13,19,44,.78);
    border:1px solid rgba(130,160,255,.28);
    box-shadow:0 0 50px rgba(70,110,255,.22), inset 0 1px 0 rgba(255,255,255,.06);
  }
  .panel h2{
    margin:0 0 10px;font-size:clamp(22px,4.6vw,42px);letter-spacing:3px;font-weight:800;
    background:linear-gradient(90deg,#7dd3fc,#a78bfa 50%,#f472b6);
    -webkit-background-clip:text;background-clip:text;color:transparent;
    text-shadow:0 0 40px rgba(120,160,255,.25)
  }
  .panel p{margin:6px 0;font-size:clamp(12px,1.7vw,15px);color:#c3ccec;line-height:1.55}
  .panel .keys{font-size:12px;color:#8792bd;letter-spacing:.4px}
  .panel .stats{color:#ffd166;font-weight:700;letter-spacing:1px}
  button{
    margin-top:16px;cursor:pointer;border:0;border-radius:999px;
    padding:12px 30px;font-size:14px;font-weight:800;letter-spacing:2px;text-transform:uppercase;
    color:#081026;background:linear-gradient(90deg,#7dd3fc,#a78bfa);
    box-shadow:0 8px 26px rgba(120,160,255,.4);transition:transform .12s ease, box-shadow .2s ease;
    font-family:inherit;
  }
  button:hover{transform:translateY(-2px);box-shadow:0 12px 30px rgba(140,180,255,.55)}
  button:active{transform:translateY(0)}
  footer{margin-top:12px;font-size:11px;color:#6f7aa6;letter-spacing:1.2px;text-align:center;text-transform:uppercase}
  @media (max-width:480px){
    .tag{display:none}
    footer{font-size:9.5px;letter-spacing:.8px}
  }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="logo">
      <span class="dot"></span>
      <h1>NEON BREAKOUT</h1>
    </div>
    <div class="tag">Mouse / ← → &nbsp;·&nbsp; Space to launch</div>
  </header>

  <div class="stage">
    <canvas id="game"></canvas>
    <div class="overlay" id="overlay">
      <div class="panel">
        <h2 id="ovTitle">NEON BREAKOUT</h2>
        <p id="ovMsg">Click or press a key to start</p>
        <p class="keys">Move the paddle with the mouse or the ← → arrow keys</p>
        <p class="stats" id="ovStats"></p>
        <button id="ovBtn">Start Game</button>
      </div>
    </div>
  </div>
  <footer>Catch ⚡ power-ups · Crack the steel bricks · Clear the wall to win</footer>
</div>

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

  /* ============================ WORLD CONSTANTS ============================ */
  const W = 960, H = 720;
  const COLS = 10, ROWS = 8;
  const BRICK_LEFT = 44, BRICK_TOP = 104, GAP = 6;
  const BRICK_H = 26;
  const BRICK_W = (W - BRICK_LEFT*2 - GAP*(COLS-1)) / COLS;
  const PADDLE_H = 16, PADDLE_Y = H - 62;
  const PADDLE_W = 132, PADDLE_W_WIDE = 196;
  const BALL_R = 9;
  const MAX_ANGLE = 1.15;            // ~66 degrees
  const WIDE_TIME = 10;              // seconds
  const MULTI_BALLS = 3;

  const PALETTE = ['#ff4d6d','#ff7a45','#ffc93c','#5ce1a4','#38bdf8','#7aa2ff','#c084fc','#f472b6'];

  /* ============================ CANVAS ============================ */
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const ovTitle = document.getElementById('ovTitle');
  const ovMsg = document.getElementById('ovMsg');
  const ovStats = document.getElementById('ovStats');
  const ovBtn = document.getElementById('ovBtn');

  let dpr = 1;
  function resize(){
    const rect = canvas.getBoundingClientRect();
    dpr = Math.min(2, window.devicePixelRatio || 1);
    canvas.width  = Math.max(320, Math.round(rect.width * dpr));
    canvas.height = Math.max(240, Math.round(rect.height * dpr));
  }
  window.addEventListener('resize', resize);
  if (window.ResizeObserver) new ResizeObserver(resize).observe(canvas);
  resize();

  /* ============================ AUDIO (safe, optional) ============================ */
  let actx = null;
  function beep(freq, dur, type, vol){
    try{
      if(!actx) actx = new (window.AudioContext || window.webkitAudioContext)();
      if(actx.state === 'suspended') actx.resume();
      const o = actx.createOscillator(), g = actx.createGain();
      o.type = type || 'sine';
      o.frequency.value = freq;
      g.gain.setValueAtTime(vol || 0.05, actx.currentTime);
      g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime + dur);
      o.connect(g); g.connect(actx.destination);
      o.start(); o.stop(actx.currentTime + dur);
    }catch(e){}
  }
  const sfxPaddle = ()=> beep(430, 0.07, 'triangle', 0.05);
  const sfxTick   = ()=> beep(720, 0.045, 'square', 0.028);
  const sfxBreak  = ()=> beep(300, 0.09, 'sawtooth', 0.04);
  const sfxPower  = ()=> { beep(660,0.09,'sine',0.06); setTimeout(()=>beep(990,0.12,'sine',0.06),70); };
  const sfxLife   = ()=> beep(180, 0.32, 'sine', 0.07);
  const sfxWin    = () => { [523,659,784,1046].forEach((f,i)=>setTimeout(()=>beep(f,0.16,'triangle',0.06),i*110)); };

  /* ============================ STATE ============================ */
  let state = 'demo';           // demo | playing | gameover | victory
  let score = 0, lives = 3, level = 1;
  let bricks = [], balls = [], powerups = [], particles = [], floaters = [];
  let bricksRemaining = 0, totalBricks = 0;
  let paddle, ballSpeed = 415, widenTimer = 0, shake = 0;
  let controlMode = 'mouse';    // mouse | keys | ai
  let mouseTargetX = W/2 - PADDLE_W/2;
  let keyLeft = false, keyRight = false;
  let bgStars = [];
  for(let i=0;i<70;i++){
    bgStars.push({x:Math.random()*W, y:Math.random()*H, r:Math.random()*1.8+0.4, s:Math.random()*0.7+0.15, p:Math.random()*6.28});
  }

  /* ============================ HELPERS ============================ */
  const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
  function rnd(a,b){ return a + Math.random()*(b-a); }

  function roundRect(c,x,y,w,h,r){
    r = Math.min(r, w/2, h/2);
    c.beginPath();
    c.moveTo(x+r,y);
    c.arcTo(x+w,y,x+w,y+h,r);
    c.arcTo(x+w,y+h,x,y+h,r);
    c.arcTo(x,y+h,x,y,r);
    c.arcTo(x,y,x+w,y,r);
    c.closePath();
  }
  function shade(hex, amt){
    const n = parseInt(hex.slice(1),16);
    let r=(n>>16)&255, g=(n>>8)&255, b=n&255;
    r=clamp(Math.round(r+amt),0,255); g=clamp(Math.round(g+amt),0,255); b=clamp(Math.round(b+amt),0,255);
    return '#'+((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1);
  }

  /* ============================ LEVEL ============================ */
  function buildLevel(){
    bricks = [];
    bricksRemaining = 0; totalBricks = 0;
    const powerSlots = [];
    const slots = [];
    for(let r=0;r<ROWS;r++) for(let c=0;c<COLS;c++) slots.push({r,c});
    // choose 8 power-up bricks spread out
    const chosen = new Set();
    let guard = 0;
    while(chosen.size < 8 && guard++ < 400){
      const s = slots[Math.floor(Math.random()*slots.length)];
      if(s.r >= ROWS-1) continue;                 // keep bottom row plain
      if(chosen.has(s.r*COLS+s.c)) continue;
      // avoid clustering
      let ok = true;
      chosen.forEach(k=>{ const rr=Math.floor(k/COLS), cc=k%COLS;
        if(Math.abs(rr-s.r)<=1 && Math.abs(cc-s.c)<=1) ok=false; });
      if(!ok) continue;
      chosen.add(s.r*COLS+s.c);
    }
    chosen.forEach(k=> powerSlots.push(k));

    for(let r=0;r<ROWS;r++){
      for(let c=0;c<COLS;c++){
        const tough = (r >= ROWS-3) || (r === ROWS-4 && (c%3===1));
        const isPower = chosen.has(r*COLS+c);
        const b = {
          x: BRICK_LEFT + c*(BRICK_W+GAP),
          y: BRICK_TOP + r*(BRICK_H+GAP),
          w: BRICK_W, h: BRICK_H,
          row:r, col:c,
          maxHp: tough ? 2 : 1,
          hp: tough ? 2 : 1,
          tough: tough,
          type: isPower ? 'power' : (tough ? 'tough' : 'normal'),
          power: isPower ? (powerSlots.indexOf(r*COLS+c) % 2 === 0 ? 'wide' : 'multi') : null,
          flash: 0, cracked:false, dead:false,
          color: PALETTE[r % PALETTE.length]
        };
        bricks.push(b);
        bricksRemaining++; totalBricks++;
      }
    }
  }

  /* ============================ OBJECTS ============================ */
  function makePaddle(){
    paddle = { x: W/2 - PADDLE_W/2, prevX: W/2 - PADDLE_W/2, y: PADDLE_Y, w: PADDLE_W, h: PADDLE_H };
    mouseTargetX = paddle.x;
  }
  function newBall(x,y,angle){
    const s = ballSpeed;
    return {
      x, y, vx: Math.sin(angle)*s, vy: -Math.cos(angle)*s, r: BALL_R,
      attached:false, launchT:0, trail:[]
    };
  }
  function resetBall(){
    balls = [];
    const b = newBall(paddle.x+paddle.w/2, paddle.y - BALL_R - 1, rnd(-0.25,0.25));
    b.attached = true; b.launchT = 0.55;
    balls.push(b);
  }
  function spawnParticles(b){
    const count = b.tough ? 20 : 14;
    for(let i=0;i<count;i++){
      const a = Math.random()*Math.PI*2, sp = rnd(60,300);
      particles.push({
        x: b.x + b.w/2 + rnd(-b.w/2,b.w/2),
        y: b.y + b.h/2 + rnd(-b.h/2,b.h/2),
        vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - rnd(20,120),
        life: rnd(0.45,0.95), max: 0.95,
        size: rnd(2,5.5),
        color: b.type==='tough' ? '#c9d4f0' : b.color,
        sq: Math.random()<0.5
      });
    }
  }
  function floater(x,y,text,color){
    floaters.push({x,y,text,color:color||'#ffffff',life:0.9});
  }
  function spawnPowerup(b){
    powerups.push({
      x: b.x + b.w/2 - 13, y: b.y + b.h/2 - 13, w:26, h:26,
      type: b.power, vy: 145, vx: rnd(-25,25), rot:0, life: 12
    });
  }

  /* ============================ GAME FLOW ============================ */
  function resetGame(){
    score = 0; lives = 3; level = 1;
    ballSpeed = 415; widenTimer = 0; shake = 0;
    particles = []; powerups = []; floaters = [];
    makePaddle(); buildLevel(); resetBall();
    controlMode = 'mouse';
  }
  function startGame(){
    if(state === 'playing') return;
    resetGame();
    state = 'playing';
    overlay.classList.add('hide');
    beep(520,0.1,'triangle',0.05);
  }
  function gameOver(){
    state = 'gameover';
    ovTitle.textContent = 'GAME OVER';
    ovMsg.textContent = 'The wall got the better of you.';
    ovStats.textContent = 'FINAL SCORE  ' + score;
    ovBtn.textContent = 'Play Again';
    overlay.classList.remove('hide');
    beep(160,0.5,'sawtooth',0.06);
  }
  function victory(){
    state = 'victory';
    ovTitle.textContent = 'VICTORY!';
    ovMsg.textContent = 'Every brick destroyed. Impressive.';
    ovStats.textContent = 'FINAL SCORE  ' + score;
    ovBtn.textContent = 'Play Again';
    overlay.classList.remove('hide');
    sfxWin();
  }

  /* ============================ INPUT ============================ */
  function toWorldX(clientX){
    const r = canvas.getBoundingClientRect();
    return (clientX - r.left) * (W / r.width);
  }
  canvas.addEventListener('pointermove', e=>{
    if(state === 'demo') startGame();
    if(state !== 'playing') return;
    controlMode = 'mouse';
    mouseTargetX = toWorldX(e.clientX) - paddle.w/2;
  });
  canvas.addEventListener('pointerdown', e=>{
    if(state !== 'playing'){ startGame(); return; }
    controlMode = 'mouse';
    mouseTargetX = toWorldX(e.clientX) - paddle.w/2;
    launchAttached();
  });
  ovBtn.addEventListener('click', e=>{ e.stopPropagation(); startGame(); });
  overlay.addEventListener('pointerdown', ()=>{ if(state!=='playing') startGame(); });

  window.addEventListener('keydown', e=>{
    const k = e.key;
    if(k === 'ArrowLeft' || k === 'ArrowRight'){
      e.preventDefault();
      if(state !== 'playing'){ startGame(); }
      if(state === 'playing') controlMode = 'keys';
      if(k === 'ArrowLeft') keyLeft = true; else keyRight = true;
    } else if(k === ' ' || k === 'Spacebar' || k === 'Enter'){
      e.preventDefault();
      if(state !== 'playing') startGame();
      else launchAttached();
    } else {
      if(state !== 'playing') startGame();
    }
  });
  window.addEventListener('keyup', e=>{
    if(e.key === 'ArrowLeft') keyLeft = false;
    if(e.key === 'ArrowRight') keyRight = false;
  });
  window.addEventListener('blur', ()=>{ keyLeft = keyRight = false; });

  function launchAttached(){
    for(const b of balls) if(b.attached){ b.attached = false; }
  }

  /* ============================ UPDATE ============================ */
  function updatePaddle(dt){
    paddle.prevX = paddle.x;
    if(state === 'demo'){
      controlMode = 'ai';
      let target = paddle.x;
      let best = null;
      for(const b of balls){ if(b.attached) continue; if(!best || b.y > best.y) best = b; }
      if(best){
        if(best.vy > 0){
          const t = (paddle.y - best.y) / best.vy;
          target = best.x + best.vx * t - paddle.w/2;
        } else {
          target = best.x - paddle.w/2;
        }
      }
      target = clamp(target, 0, W - paddle.w);
      paddle.x += (target - paddle.x) * Math.min(1, dt*9);
    } else if(controlMode === 'mouse'){
      mouseTargetX = clamp(mouseTargetX, 0, W - paddle.w);
      paddle.x += (mouseTargetX - paddle.x) * Math.min(1, dt*22);
    } else {
      const sp = 640;
      if(keyLeft) paddle.x -= sp*dt;
      if(keyRight) paddle.x += sp*dt;
    }
    paddle.x = clamp(paddle.x, 0, W - paddle.w);
  }

  function normalizeSpeed(b){
    const m = Math.hypot(b.vx,b.vy) || 1;
    b.vx = b.vx/m*ballSpeed;
    b.vy = b.vy/m*ballSpeed;
  }

  function collidePaddle(b, speed){
    const prevBottom = b.py + b.r;
    const nowBottom  = b.y + b.r;
    const overX = (b.x + b.r > paddle.x) && (b.x - b.r < paddle.x + paddle.w);
    // top-face hit
    if(b.vy > 0 && nowBottom >= paddle.y && prevBottom <= paddle.y + 8 && overX){
      let off = (b.x - (paddle.x + paddle.w/2)) / (paddle.w/2);
      off = clamp(off, -1, 1);
      const ang = off * MAX_ANGLE;
      b.vx = Math.sin(ang) * speed;
      b.vy = -Math.cos(ang) * speed;
      b.y = paddle.y - b.r - 0.5;
      sfxPaddle();
      return true;
    }
    // side / corner hit -> generic push-out
    const cx = clamp(b.x, paddle.x, paddle.x+paddle.w);
    const cy = clamp(b.y, paddle.y, paddle.y+paddle.h);
    const dx = b.x-cx, dy = b.y-cy;
    const d = Math.hypot(dx,dy);
    if(d < b.r){
      let nx, ny;
      if(d > 0.0001){ nx = dx/d; ny = dy/d; } else { nx = 0; ny = -1; }
      b.x += nx*(b.r-d+0.4);
      b.y += ny*(b.r-d+0.4);
      const dot = b.vx*nx + b.vy*ny;
      b.vx -= 2*dot*nx; b.vy -= 2*dot*ny;
      if(b.vy > 0){ // still going down: give it the classic angled bounce
        let off = clamp((b.x-(paddle.x+paddle.w/2))/(paddle.w/2), -1, 1);
        const ang = off*MAX_ANGLE;
        b.vx = Math.sin(ang)*speed; b.vy = -Math.cos(ang)*speed;
      }
      sfxPaddle();
      return true;
    }
    return false;
  }

  function hitBrick(b){
    b.flash = 0.16;
    if(b.hp > 1){
      b.hp--; b.cracked = true;
      score += 25;
      floater(b.x+b.w/2, b.y, '25', '#ffd166');
      for(let i=0;i<5;i++){
        const a = Math.random()*Math.PI*2;
        particles.push({x:b.x+b.w/2,y:b.y+b.h/2,vx:Math.cos(a)*rnd(40,140),vy:Math.sin(a)*rnd(40,140),
          life:0.4,max:0.4,size:rnd(1.5,3),color:'#c9d4f0',sq:true});
      }
      sfxTick();
    }else{
      b.hp = 0; b.dead = true;
      bricksRemaining--;
      const pts = (b.type === 'power') ? 150 : (b.tough ? 100 : 50);
      score += pts;
      floater(b.x+b.w/2, b.y, '+' + pts, b.type==='power' ? '#7dd3fc' : '#ffffff');
      spawnParticles(b);
      if(b.type === 'power') spawnPowerup(b);
      shake = Math.min(11, shake + 3.2);
      ballSpeed = Math.min(690, ballSpeed + 2.6);
      sfxBreak();
    }
  }

  function collideBricks(b){
    for(let i=0;i<bricks.length;i++){
      const k = bricks[i];
      if(k.dead) continue;
      const cx = clamp(b.x, k.x, k.x+k.w);
      const cy = clamp(b.y, k.y, k.y+k.h);
      const dx = b.x-cx, dy = b.y-cy;
      const d = Math.hypot(dx,dy);
      if(d >= b.r) continue;
      let nx, ny, pen;
      if(d > 0.0001){ nx = dx/d; ny = dy/d; pen = b.r - d; }
      else{
        const left = b.x-k.x, right = k.x+k.w-b.x, top = b.y-k.y, bot = k.y+k.h-b.y;
        const m = Math.min(left,right,top,bot);
        if(m===left){ nx=-1; ny=0; } else if(m===right){ nx=1; ny=0; }
        else if(m===top){ nx=0; ny=-1; } else { nx=0; ny=1; }
        pen = b.r;
      }
      b.x += nx*(pen+0.6);
      b.y += ny*(pen+0.6);
      const dot = b.vx*nx + b.vy*ny;
      b.vx -= 2*dot*nx; b.vy -= 2*dot*ny;
      normalizeSpeed(b);
      hitBrick(k);
      return true;
    }
    return false;
  }

  function updateBall(b, dt){
    if(b.attached){
      b.x = paddle.x + paddle.w/2;
      b.y = paddle.y - b.r - 1;
      b.trail.length = 0;
      b.launchT -= dt;
      if(b.launchT <= 0){
        b.attached = false;
        const a = rnd(-0.28,0.28);
        b.vx = Math.sin(a)*ballSpeed; b.vy = -Math.cos(a)*ballSpeed;
      }
      return;
    }
    const speed = ballSpeed;
    const dist = speed*dt;
    const steps = Math.max(1, Math.ceil(dist/3));
    const sdt = dt/steps;
    for(let s=0;s<steps;s++){
      b.px = b.x; b.py = b.y;
      b.x += b.vx*sdt;
      b.y += b.vy*sdt;

      // walls
      if(b.x - b.r < 0){ b.x = b.r; b.vx = Math.abs(b.vx); }
      else if(b.x + b.r > W){ b.x = W-b.r; b.vx = -Math.abs(b.vx); }
      if(b.y - b.r < 0){ b.y = b.r; b.vy = Math.abs(b.vy); }

      collidePaddle(b, speed);
      collideBricks(b);
    }
    // trail
    b.trail.push({x:b.x,y:b.y});
    if(b.trail.length > 12) b.trail.shift();
  }

  function updatePowerups(dt){
    for(let i=powerups.length-1;i>=0;i--){
      const p = powerups[i];
      p.y += p.vy*dt;
      p.x += p.vx*dt;
      p.rot += dt*1.6;
      p.life -= dt;
      // catch
      if(p.y + p.h/2 > paddle.y && p.y - p.h/2 < paddle.y + paddle.h &&
         p.x + p.w/2 > paddle.x && p.x - p.w/2 < paddle.x + paddle.w){
        applyPower(p);
        powerups.splice(i,1);
        continue;
      }
      if(p.y > H + 30 || p.life <= 0){ powerups.splice(i,1); }
    }
  }

  function applyPower(p){
    sfxPower();
    if(p.type === 'wide'){
      const center = paddle.x + paddle.w/2;
      widenTimer = WIDE_TIME;
      paddle.w = PADDLE_W_WIDE;
      paddle.x = center - paddle.w/2;
      floater(p.x, p.y, 'WIDE PADDLE!', '#7dd3fc');
    }else{
      // multi ball: add 2 extra balls
      const src = balls.find(b=>!b.attached) || balls[0];
      let baseA = Math.atan2(src.vy, src.vx);
      for(let i=0;i<MULTI_BALLS-1;i++){
        const a = baseA + (i===0 ? 0.42 : -0.42);
        const nb = newBall(src.x, src.y, 0);
        nb.vx = Math.cos(a)*ballSpeed; nb.vy = Math.sin(a)*ballSpeed;
        if(nb.vy > 0) { nb.vy = -Math.abs(nb.vy); } // keep them mostly upward-biased
        balls.push(nb);
      }
      floater(p.x, p.y, 'MULTI BALL!', '#c084fc');
    }
  }

  function update(dt){
    // particles & floaters always
    for(let i=particles.length-1;i>=0;i--){
      const p = particles[i];
      p.life -= dt;
      if(p.life <= 0){ particles.splice(i,1); continue; }
      p.vy += 780*dt;
      p.x += p.vx*dt; p.y += p.vy*dt;
      p.vx *= 0.995;
    }
    for(let i=floaters.length-1;i>=0;i--){
      const f = floaters[i];
      f.life -= dt; f.y -= 34*dt;
      if(f.life <= 0) floaters.splice(i,1);
    }
    shake *= Math.pow(0.0015, dt);

    if(state === 'gameover' || state === 'victory') return;

    updatePaddle(dt);

    if(widenTimer > 0){
      widenTimer -= dt;
      if(widenTimer <= 0){
        widenTimer = 0;
        const center = paddle.x + paddle.w/2;
        paddle.w = PADDLE_W;
        paddle.x = center - paddle.w/2;
      }
    }

    // balls
    for(let i=balls.length-1;i>=0;i--){
      const b = balls[i];
      updateBall(b, dt);
      if(b.y - b.r > H + 4){
        balls.splice(i,1);
        continue;
      }
    }
    if(balls.length === 0){
      if(state === 'demo'){
        resetBall();
      }else{
        lives--;
        sfxLife();
        if(lives <= 0){ gameOver(); return; }
        resetBall();
      }
    }

    updatePowerups(dt);

    // victory
    if(bricksRemaining <= 0 && state === 'playing'){ victory(); return; }
    if(bricksRemaining <= 0 && state === 'demo'){
      // attract mode: rebuild the wall and keep going
      buildLevel();
      resetBall();
    }
  }

  /* ============================ DRAWING ============================ */
  function drawBackground(){
    const g = ctx.createLinearGradient(0,0,0,H);
    g.addColorStop(0,'#0c1230'); g.addColorStop(0.5,'#0a1026'); g.addColorStop(1,'#070b18');
    ctx.fillStyle = g; ctx.fillRect(0,0,W,H);

    // subtle grid
    ctx.strokeStyle = 'rgba(120,160,255,0.05)';
    ctx.lineWidth = 1;
    for(let x=0;x<W;x+=48){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
    for(let y=0;y<H;y+=48){ ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }

    // drifting stars
    const t = performance.now()/1000;
    for(const s of bgStars){
      const a = 0.25 + 0.25*Math.sin(t*s.s + s.p);
      ctx.fillStyle = 'rgba(190,215,255,'+a.toFixed(3)+')';
      ctx.beginPath(); ctx.arc(s.x, s.y + Math.sin(t*0.25+s.p)*6, s.r, 0, Math.PI*2); ctx.fill();
    }
    // vignette
    const vg = ctx.createRadialGradient(W/2,H/2,H*0.32,W/2,H/2,H*0.95);
    vg.addColorStop(0,'rgba(0,0,0,0)'); vg.addColorStop(1,'rgba(0,0,0,0.55)');
    ctx.fillStyle = vg; ctx.fillRect(0,0,W,H);
  }

  function drawHUD(){
    // top strip
    const g = ctx.createLinearGradient(0,0,0,92);
    g.addColorStop(0,'rgba(10,16,38,0.92)'); g.addColorStop(1,'rgba(10,16,38,0)');
    ctx.fillStyle = g; ctx.fillRect(0,0,W,92);
    ctx.strokeStyle = 'rgba(130,160,255,0.18)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(24,88); ctx.lineTo(W-24,88); ctx.stroke();

    ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
    ctx.fillStyle = '#7f8bb8'; ctx.font = '700 13px system-ui,sans-serif';
    ctx.fillText('SCORE', 30, 34);
    ctx.fillStyle = '#eaf2ff'; ctx.font = '800 38px system-ui,sans-serif';
    ctx.fillText(String(score), 30, 72);

    ctx.textAlign = 'right';
    ctx.fillStyle = '#7f8bb8'; ctx.font = '700 13px system-ui,sans-serif';
    ctx.fillText('LIVES', W-30, 34);
    for(let i=0;i<Math.max(lives,0);i++){
      drawHeart(W-46-i*30, 52, 24, i===0 ? '#ff5f7a' : '#ff7d95');
    }

    ctx.textAlign = 'center';
    ctx.fillStyle = '#7f8bb8'; ctx.font = '700 12px system-ui,sans-serif';
    ctx.fillText('LEVEL ' + level, W/2, 30);
    const pct = totalBricks ? (totalBricks-bricksRemaining)/totalBricks : 0;
    const bw = 190, bx = W/2-bw/2;
    ctx.fillStyle = 'rgba(255,255,255,0.09)';
    roundRect(ctx,bx,44,bw,9,5); ctx.fill();
    const gg = ctx.createLinearGradient(bx,0,bx+bw,0);
    gg.addColorStop(0,'#7dd3fc'); gg.addColorStop(1,'#a78bfa');
    ctx.fillStyle = gg;
    roundRect(ctx,bx,44,Math.max(6,bw*pct),9,5); ctx.fill();
    ctx.fillStyle = '#a8b3da'; ctx.font = '600 11px system-ui,sans-serif';
    ctx.fillText(bricksRemaining + ' BRICKS LEFT', W/2, 72);
  }

  function drawHeart(x,y,s,color){
    ctx.save(); ctx.translate(x,y); ctx.scale(s/20,s/20);
    ctx.beginPath();
    ctx.moveTo(0,5);
    ctx.bezierCurveTo(-9,-5,-15,1.5,-15,7.5);
    ctx.bezierCurveTo(-15,14.5,-6.5,19,0,23);
    ctx.bezierCurveTo(6.5,19,15,14.5,15,7.5);
    ctx.bezierCurveTo(15,1.5,9,-5,0,5);
    ctx.closePath();
    ctx.fillStyle = color; ctx.shadowColor = 'rgba(255,95,122,.6)'; ctx.shadowBlur = 12;
    ctx.fill(); ctx.shadowBlur = 0; ctx.restore();
  }

  function drawCracks(b){
    const seed = (b.row*7 + b.col*13);
    const rndv = i => { const x = Math.sin(seed*12.9898 + i*78.233)*43758.5453; return x - Math.floor(x); };
    ctx.save();
    ctx.strokeStyle = 'rgba(12,16,30,0.9)';
    ctx.lineWidth = 1.8; ctx.lineCap = 'round';
    for(let k=0;k<3;k++){
      ctx.beginPath();
      let x = b.x + b.w*(0.2 + 0.6*rndv(k));
      let y = b.y + b.h*0.5;
      ctx.moveTo(x,y);
      for(let s=0;s<3;s++){
        x += (rndv(k*11+s+1)-0.5)*b.w*0.36;
        y += (rndv(k*23+s+1)-0.5)*b.h*1.1;
        ctx.lineTo(x,y);
      }
      ctx.stroke();
    }
    // chipped corner
    ctx.fillStyle = 'rgba(12,16,30,0.55)';
    ctx.beginPath();
    ctx.moveTo(b.x+b.w, b.y); ctx.lineTo(b.x+b.w-11, b.y);
    ctx.lineTo(b.x+b.w, b.y+11); ctx.closePath(); ctx.fill();
    ctx.restore();
  }

  function drawPowerIcon(cx,cy,type,size,alpha){
    ctx.save();
    ctx.translate(cx,cy);
    ctx.globalAlpha = alpha === undefined ? 1 : alpha;
    ctx.fillStyle = '#0b1020';
    if(type === 'wide'){
      // wide paddle + double arrows
      ctx.fillStyle = '#08101f';
      roundRect(ctx,-size*0.52,-size*0.16,size*1.04,size*0.32,size*0.12); ctx.fill();
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.moveTo(-size*0.46,0); ctx.lineTo(-size*0.18,-size*0.26); ctx.lineTo(-size*0.18,-size*0.1);
      ctx.lineTo(-size*0.34,-size*0.1); ctx.lineTo(-size*0.34,size*0.1); ctx.lineTo(-size*0.18,size*0.1);
      ctx.lineTo(-size*0.18,size*0.26); ctx.closePath(); ctx.fill();
      ctx.beginPath();
      ctx.moveTo(size*0.46,0); ctx.lineTo(size*0.18,size*0.26); ctx.lineTo(size*0.18,size*0.1);
      ctx.lineTo(size*0.34,size*0.1); ctx.lineTo(size*0.34,-size*0.1); ctx.lineTo(size*0.18,-size*0.1);
      ctx.lineTo(size*0.18,-size*0.26); ctx.closePath(); ctx.fill();
      ctx.fillStyle = '#ffffff';
      roundRect(ctx,-size*0.2,-size*0.13,size*0.4,size*0.26,size*0.08); ctx.fill();
    }else{
      ctx.fillStyle = '#ffffff';
      for(let i=-1;i<=1;i++){
        ctx.beginPath(); ctx.arc(i*size*0.33, 0, size*0.19, 0, Math.PI*2); ctx.fill();
      }
    }
    ctx.restore();
  }

  function drawBricks(){
    for(const b of bricks){
      if(b.dead) continue;
      const x=b.x,y=b.y,w=b.w,h=b.h;
      ctx.save();
      // shadow
      ctx.fillStyle = 'rgba(0,0,0,0.35)';
      roundRect(ctx, x+2, y+4, w-2, h, 5); ctx.fill();

      let base = b.color;
      if(b.tough) base = '#8f9ec4';
      if(b.type === 'power') base = b.power === 'wide' ? '#38bdf8' : '#c084fc';

      const g = ctx.createLinearGradient(0,y,0,y+h);
      g.addColorStop(0, shade(base, 42));
      g.addColorStop(0.55, base);
      g.addColorStop(1, shade(base, -42));
      ctx.fillStyle = g;
      if(b.type === 'power'){
        ctx.shadowColor = base; ctx.shadowBlur = 16;
      }
      roundRect(ctx,x,y,w,h,5); ctx.fill();
      ctx.shadowBlur = 0;

      // top sheen
      ctx.fillStyle = 'rgba(255,255,255,0.22)';
      roundRect(ctx, x+3, y+2.5, w-6, h*0.28, 3); ctx.fill();

      // border
      ctx.strokeStyle = b.tough ? 'rgba(226,236,255,0.55)' : 'rgba(255,255,255,0.22)';
      ctx.lineWidth = b.tough ? 1.6 : 1;
      roundRect(ctx, x+0.5, y+0.5, w-1, h-1, 5); ctx.stroke();

      if(b.tough){
        // rivets / steel texture
        ctx.fillStyle = 'rgba(255,255,255,0.28)';
        ctx.beginPath(); ctx.arc(x+8,y+8,1.7,0,Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(x+w-8,y+8,1.7,0,Math.PI*2); ctx.fill();
      }
      if(b.type === 'power'){
        drawPowerIcon(x+w/2, y+h/2, b.power, Math.min(w,h)*0.9);
      }
      if(b.cracked) drawCracks(b);
      if(b.flash > 0){
        ctx.fillStyle = 'rgba(255,255,255,'+(b.flash/0.16*0.65)+')';
        roundRect(ctx,x,y,w,h,5); ctx.fill();
        b.flash -= 0.016;
      }
      ctx.restore();
    }
  }

  function drawPowerups(){
    for(const p of powerups){
      ctx.save();
      ctx.translate(p.x+p.w/2, p.y+p.h/2);
      const pulse = 1 + 0.08*Math.sin(performance.now()/160 + p.x);
      ctx.scale(pulse,pulse);
      const col = p.type === 'wide' ? '#38bdf8' : '#c084fc';
      ctx.shadowColor = col; ctx.shadowBlur = 18;
      const g = ctx.createRadialGradient(-4,-4,2,0,0,16);
      g.addColorStop(0,'#ffffff'); g.addColorStop(0.35,col); g.addColorStop(1,shade(col,-45));
      ctx.fillStyle = g;
      roundRect(ctx,-13,-13,26,26,7); ctx.fill();
      ctx.shadowBlur = 0;
      ctx.strokeStyle = 'rgba(255,255,255,0.75)'; ctx.lineWidth = 1.4;
      roundRect(ctx,-12.5,-12.5,25,25,6.5); ctx.stroke();
      drawPowerIcon(0,0,p.type,17);
      ctx.restore();
    }
  }

  function drawPaddle(){
    const x = paddle.x, y = paddle.y, w = paddle.w, h = paddle.h;
    ctx.save();
    // glow
    ctx.shadowColor = widenTimer > 0 ? 'rgba(56,189,248,0.9)' : 'rgba(150,180,255,0.55)';
    ctx.shadowBlur = widenTimer > 0 ? 26 : 16;
    const g = ctx.createLinearGradient(0,y,0,y+h);
    if(widenTimer > 0){ g.addColorStop(0,'#bae6fd'); g.addColorStop(0.5,'#38bdf8'); g.addColorStop(1,'#0369a1'); }
    else { g.addColorStop(0,'#e8f0ff'); g.addColorStop(0.5,'#9fb6ef'); g.addColorStop(1,'#4a5b96'); }
    ctx.fillStyle = g;
    roundRect(ctx,x,y,w,h,8); ctx.fill();
    ctx.shadowBlur = 0;
    // centre marker
    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    roundRect(ctx, x+w/2-9, y+3, 18, 3, 1.5); ctx.fill();
    // end caps
    ctx.fillStyle = widenTimer>0 ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.35)';
    roundRect(ctx, x+4, y+4, 7, h-8, 3); ctx.fill();
    roundRect(ctx, x+w-11, y+4, 7, h-8, 3); ctx.fill();

    // wide timer bar
    if(widenTimer > 0){
      const frac = widenTimer / WIDE_TIME;
      const bw = w, by = y - 13;
      ctx.fillStyle = 'rgba(255,255,255,0.12)';
      roundRect(ctx, x, by, bw, 6, 3); ctx.fill();
      ctx.fillStyle = '#7dd3fc';
      roundRect(ctx, x, by, bw*frac, 6, 3); ctx.fill();
      ctx.fillStyle = '#cfe8ff'; ctx.font = '700 10px system-ui,sans-serif'; ctx.textAlign='center';
      ctx.fillText('WIDE ' + widenTimer.toFixed(1) + 's', x+bw/2, by-3);
    }
    ctx.restore();
  }

  function drawBalls(){
    for(const b of balls){
      // trail
      for(let i=0;i<b.trail.length;i++){
        const t = b.trail[i];
        const a = (i/b.trail.length)*0.42;
        ctx.fillStyle = 'rgba(150,200,255,'+a.toFixed(3)+')';
        ctx.beginPath(); ctx.arc(t.x, t.y, b.r*(i/b.trail.length)*0.85, 0, Math.PI*2); ctx.fill();
      }
      ctx.save();
      ctx.shadowColor = 'rgba(140,200,255,0.9)'; ctx.shadowBlur = 20;
      const g = ctx.createRadialGradient(b.x-b.r*0.35, b.y-b.r*0.4, 1, b.x, b.y, b.r);
      g.addColorStop(0,'#ffffff'); g.addColorStop(0.55,'#cfe6ff'); g.addColorStop(1,'#5aa9ff');
      ctx.fillStyle = g;
      ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,Math.PI*2); ctx.fill();
      ctx.restore();
    }
  }

  function drawParticles(){
    for(const p of particles){
      const a = clamp(p.life/p.max,0,1);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      if(p.sq) ctx.fillRect(p.x-p.size/2, p.y-p.size/2, p.size, p.size);
      else { ctx.beginPath(); ctx.arc(p.x,p.y,p.size*a+0.5,0,Math.PI*2); ctx.fill(); }
    }
    ctx.globalAlpha = 1;
  }

  function drawFloaters(){
    ctx.textAlign = 'center';
    for(const f of floaters){
      const a = clamp(f.life/0.9,0,1);
      ctx.globalAlpha = a;
      ctx.fillStyle = f.color;
      ctx.font = '800 18px system-ui,sans-serif';
      ctx.fillText(f.text, f.x, f.y);
    }
    ctx.globalAlpha = 1;
  }

  function draw(){
    ctx.setTransform(canvas.width/W, 0, 0, canvas.height/H, 0, 0);
    ctx.clearRect(0,0,W,H);
    ctx.save();
    if(shake > 0.2){
      ctx.translate(rnd(-shake,shake)*0.5, rnd(-shake,shake)*0.5);
    }
    drawBackground();
    drawBricks();
    drawPowerups();
    drawPaddle();
    drawBalls();
    drawParticles();
    drawFloaters();
    drawHUD();
    ctx.restore();
  }

  /* ============================ LOOP ============================ */
  let last = performance.now();
  function frame(now){
    let dt = (now-last)/1000;
    last = now;
    if(dt > 0.05) dt = 0.05;
    update(dt);
    draw();
    requestAnimationFrame(frame);
  }

  /* ============================ BOOT ============================ */
  makePaddle();
  buildLevel();
  resetBall();
  state = 'demo';
  ovTitle.textContent = 'NEON BREAKOUT';
  ovMsg.textContent = 'Click or press a key to start';
  ovStats.textContent = '';
  ovBtn.textContent = 'Start Game';
  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (10,830 tokens)
Let me build a polished Breakout game in a single HTML file.

Key features:
- Canvas, paddle controlled by mouse + arrow keys
- Bounce angle depends on hit position on paddle
- 8 rows of bricks, some tough (2 hits) that crack after first hit
- Power-up bricks with visible icons (widen = 10s, multiball = 3 balls)
- Collision robust: use swept collision / substepping to prevent tunneling
- Particles on brick break
- 3 lives, score, game over & victory screens, restart

Let me design carefully.

Canvas size: internal 900x700 maybe. Responsive scaling via CSS with aspect ratio, and devicePixelRatio handling.

Physics: ball speed increases slightly over time / per hit. Use sub-stepping: move ball in small steps (e.g., max 4px per step) and check collisions each step.

Collision detection: ball is a circle radius r. Brick is an AABB. For each substep, compute new position, then check overlap with bricks. Resolve by determining which axis caused overlap using previous position, push out.

Standard robust approach:
- For each brick, find closest point on brick rect to ball center. If dist < r → collision.
- Determine penetration normal: if ball center inside rect, push out along minimal axis. Otherwise normal = (center - closest)/dist.
- Move ball out along normal by penetration, reflect velocity: v = v - 2(v·n)n.
- Also mark brick hit, count damage.

Paddle collision: circle vs paddle rect. If ball moving down and overlaps paddle top, reflect based on hit offset: angle = offset * maxAngle where offset = (ballX - paddleX)/(paddleW/2) clamped [-1,1]. maxAngle ~ 65deg. Set velocity direction with that angle from vertical.

Also side collisions with walls: left/right/top bounce.

Lives: lose a ball when it falls below bottom. Multi-ball: when one falls, others remain? Common: lives decrement when all balls are gone. I'll decrement a life when a ball falls but keep going if other balls remain? Simpler: lives decrement only when the last ball falls. Actually classic: each ball falling costs a life in single ball; with multiball, usually lives count down only when all balls are lost. I'll do: when ball falls and it was the last remaining ball → lose life.

Power-ups:
- "W" wide paddle: 10 seconds, visual timer bar above paddle.
- "M" multi-ball: 3 balls.
- Icon drawn on brick (a small symbol). Bricks "visibly carry a power-up icon" — draw the icon in the brick face.

Power-up drops: fall slowly, caught by paddle.

Screens: title/start overlay ("Click or press a key to start"), game over, victory. Restart button.

Since the sandbox takes a screenshot after 3 seconds without interaction, I should make the demo self-playing? The instruction says "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". Hmm — that means an AI plays behind the overlay message so something meaningful is visible. But requirement says paddle controlled by mouse/keys. I could implement an AI auto-pilot that moves the paddle to track the ball, so the game plays itself in demo mode. Then clicking/pressing key starts actual play. That satisfies both.

But careful: "The iframe only receives keyboard events after the user clicks it". So mouse events also probably only after click? Actually mouse moves might not be delivered either until click? Typically mouse events are delivered. To be safe, demo mode with AI autopilot active until user interacts.

So: on load, game is in "attract/demo" state: overlay shows "Click or press a key to start", and AI paddle tracks the ball. When user clicks or presses key, switch to player control and hide overlay.

Also arrow keys should work after click — add keydown listener on window; it only receives keys after click which is fine.

Let me write the code.

Structure:

HTML:
- Header: title "BRICK BREAKER", score, lives, level info.
- Canvas with glow effects.
- Overlay div positioned absolutely over canvas for start/gameover/victory messages.

Actually I'll draw overlays on canvas for crispness? Using DOM overlay is easier for buttons and text. But DOM overlay text in canvas area — fine, styled nicely. I'll do DOM overlay with a button.

Let me set canvas internal resolution 960 x 720? With DPR scaling. Container max-width.

Let's define constants:
- W = 960, H = 720 world units.
- Canvas CSS: width 100%, aspect-ratio 4/3.

Paddle: width 130 (normal), 190 (wide), height 16, y = H - 60.
Ball radius 9, speed base 380 px/s, increases a bit per paddle hit up to 620.

Bricks: 8 rows × 12 columns? That's 96 bricks, maybe too many. Let's do 10 columns × 8 rows = 80 bricks. Bricks width: (960 - 2*margin - gaps)/cols. margin 40, gap 6 → usable 880 - 9*6=54 → 826/10 = 82.6 wide. Height 26, gap 6, rows 8 → 8*32=256 + top 90 → bottom ~346. Good, leaves space.

Tough bricks: some rows (e.g., rows 5-6 pattern) are tough (2 HP). Normal bricks 1 HP.

Crack visual: after first hit on tough brick, draw crack lines and darker color, plus a small damage indicator.

Score: normal brick 50, tough 100 (maybe 50 for first hit 100 for break). Power-up brick 150.

Victory: all bricks destroyed.

Particles: on break, spawn ~14 particles with color of brick, gravity, fade. Also small shake maybe.

Also ball trail effect.

Power-up icons: draw on the brick a small icon: for widen, draw arrows "«»" or a wide paddle symbol; for multi, three small circles. I'll draw distinct shapes.

Dropping power-ups: when power-up brick breaks, spawn a falling capsule with icon, speed ~150 px/s. Caught by paddle (overlap) → apply effect. If falls off bottom → lost.

Wider paddle for 10 seconds: track widenTimer, decrement, show timer bar.

Multi-ball: 3 balls — spawn 2 extra balls at slight angles.

Lives: 3 hearts display.

Also a "Level 1" label.

Let's implement collision robustly with substeps.

function update(dt):
  - if state !== 'playing' → still run demo mode? Demo mode: state = 'demo'. Let's have state variable 'demo' | 'playing' | 'gameover' | 'victory'. In demo, we still simulate the whole game with AI paddle. Actually simpler: always simulate; control mode determines who moves paddle.

In demo mode, the game can progress and possibly reach game over... That would be confusing if the demo reaches game over before user interacts (3 seconds screenshot - unlikely to lose 3 lives in 3s though). But over time the demo may reach victory which is fine/fun. But if demo loses all lives, show game over overlay. Hmm. Better: in demo mode, lives don't decrease (infinite), so demo continues forever, restarting the level if all bricks cleared? Actually in demo mode let's reset bricks when cleared and continue - "attract mode". Simpler: in demo mode, if all bricks destroyed, rebuild level (new level) and keep going; lives don't deplete.

When user starts (click/key), reset the game fresh: full lives 3, reset bricks, and switch to player control. That's clean.

Let's implement:

- resetGame() : builds bricks, score=0, lives=3, balls = [one ball], paddle normal, state='playing', hide overlay.
- demo mode: state='demo', builds level, lives=3 but don't decrement; AI paddle tracks ball.

At start, call initDemo().

Overlay content per state:
- demo: "BRICK BREAKER" title, "Click or press a key to start" and small hint about controls, plus a "Start" button.
- gameover: "GAME OVER", score, "Click or press R / button to restart".
- victory: "VICTORY!", score, restart.

Any click on canvas starts/restarts. Key press: space/enter starts; also arrow keys start? "press a key to start" – any key starts. But arrow keys also move paddle; start on any key is fine.

Careful: after game over, pressing arrow key restarts — fine.

Implementation of AI paddle in demo: target x = predicted ball x when it comes down; simple approach: target = ball.x + ball.vx * timeToReachPaddle, clamp. That works decently.

Let me now write the drawing code nicely:
- Background: dark gradient, subtle grid or starfield, vignette.
- Bricks: rounded rect with gradient, inner highlight, colored by row palette. Tough bricks: metallic gray-blue with border showing HP; cracked overlay with crack lines.
- Power-up bricks: have a glowing icon and a distinct sheen.
- Paddle: rounded rect with gradient and glow; wide state shows extra glow and a timer bar.
- Balls: white glowing circle with trail.
- Particles.

Let me write it.

Colors palette per row:
row0: #ff5f6d? Let's use a neon palette:
['#ff4d6d','#ff8a3d','#ffd93d','#4ddb8f','#38bdf8','#a78bfa','#f472b6','#facc15'] etc. 8 rows.

Ball physics details:

Ball object: {x,y,vx,vy,r,trail:[]}

Update ball movement with substeps:
const steps = Math.ceil(speed*dt / 3); each step moves dt/steps.

Per step:
  newX = x + vx*stepDt, newY = y + vy*stepDt
  // wall collisions
  if newX - r < 0 → x = r, vx = abs(vx)
  if newX + r > W → x = W-r, vx = -abs(vx)
  if newY - r < 0 → y = r, vy = abs(vy)
  // paddle collision: if vy>0 and ball bottom >= paddle.top and ball top <= paddle.bottom and x within paddle range expanded
  Then compute offset and set angle.
  // brick collisions: iterate bricks alive, check circle-rect overlap; resolve each (maybe break after first resolution to avoid multiple). I'll handle up to few.

Circle-rect collision resolution:
function collideBricks(ball):
  for each alive brick:
    const cx = clamp(ball.x, brick.x, brick.x+brick.w);
    const cy = clamp(ball.y, brick.y, brick.y+brick.h);
    let dx = ball.x-cx, dy = ball.y-cy;
    let dist = hypot;
    if (dist < ball.r) {
      if (dist > 0) { n = {dx/dist, dy/dist}; }
      else { // center inside brick: push out minimal axis
        const left = ball.x - brick.x, right = brick.x+brick.w - ball.x;
        const top = ball.y - brick.y, bottom = brick.y+brick.h - ball.y;
        const m = Math.min(left,right,top,bottom);
        if (m===left) n={-1,0} else if(m===right) n={1,0} else if(m===top) n={0,-1} else n={0,1};
      }
      // push out
      ball.x += n.x*(ball.r - dist + 0.01) ... careful when dist===0: penetration = ball.r.
      Actually penetration = ball.r - dist (for inside case dist=0 → penetration=r, push out along n).
      ball.x += n.x*penetration; ball.y += n.y*penetration;
      const dot = ball.vx*n.x + ball.vy*n.y;
      ball.vx -= 2*dot*n.x; ball.vy -= 2*dot*n.y;
      // small nudge to avoid sticking
      hit brick: brick.hp--; if hp<=0 break → particles, score, maybe drop powerup; else crack.
      return true (handled) — but we might want to continue checking other bricks in same step. I'll loop and handle all overlapping bricks but with a cap of e.g. 4 to avoid weirdness; after each resolution recompute ball position. Simpler: handle one brick per substep, and since substeps are ~3px, that's fine. But an overlap with two adjacent bricks could cause the ball to bounce back and forth. Standard: resolve the first collision and continue next substep. Fine.

Ball speed: maintain constant speed magnitude. After any reflection, renormalize to current speed.

speed = baseSpeed + brickBreaks*2, capped at 700. Also clamp ball's vy to avoid too horizontal: if |vy| < 0.25*speed → adjust.

Paddle bounce: offset = (ball.x - (paddle.x + paddle.w/2)) / (paddle.w/2); clamp [-1,1]; angle = offset * (60° in rad) → direction: vx = sin(angle)*speed, vy = -cos(angle)*speed. Ensure vy negative (upward).

Also paddle side collision: if ball hits the side of the paddle (coming from the side), reflect horizontally. Handle: if ball overlaps paddle region and vy>0 and ball.y+r > paddle.y and ball.y - r < paddle.y+paddle.h → top bounce. But if ball hits the left/right edge while moving sideways... For simplicity: treat any overlap with the top half region as a top bounce; edges get sharp angles. If the ball approaches from the side (|vx| dominant) and hits below paddle top, we could reflect vx. I'll add: if ball center is below paddle top and vy>0 but ball.x is outside paddle horizontal span (within a few px) → reflect vx only.

Actually a neat approach: check the ball's previous position. If previous bottom <= paddle.y (i.e., ball was above paddle) → top bounce. Else → side bounce.

Let me store ball.px, ball.py before moving.

Paddle moving fast could cause ball to pass through? Since we use continuous-ish steps of 3px and paddle is 16px tall, should be fine. But paddle movement itself could tunnel a ball into the paddle. To handle, when checking paddle collision, also test overlap with the swept paddle region (union of prev and current paddle x). I'll use paddlePrevX.

Power-up catching: capsule {x,y,w:26,h:26,type,vy:130}. Catch if overlaps paddle rect. Also if it reaches bottom, remove.

Now the UI/HUD: I'd rather draw score/lives on canvas (top bar) for a cohesive look, plus DOM header? Let's draw HUD inside canvas top area (score left, lives hearts right, level). But the bricks start at y=90, so top 0-80 is HUD area. Nice.

I'll draw HUD on canvas:
- Left: "SCORE" label + number.
- Center: title "BRICK BREAKER" small? maybe.
- Right: lives as heart shapes.

Also wide-paddle timer bar drawn above paddle when active.

Let me write the code now.

Canvas sizing: use ResizeObserver / window resize to set canvas.width = rect.width * dpr, and scale context by (rect.width/W). Set CSS width 100% with aspect-ratio 4/3 inside a container max-width 960px.

Simpler: canvas style width:100%; height:auto; aspect-ratio: 4/3. In JS, compute rect = canvas.getBoundingClientRect(); canvas.width = Math.round(rect.width*dpr); canvas.height = Math.round(rect.height*dpr); ctx.setTransform(dpr*rect.width/W, 0,0, dpr*rect.width/W, 0,0)? Since aspect ratio 4/3 matches W/H = 960/720 = 4/3, scale = rect.width/W and height scale = rect.height/H equal. I'll compute sx = rect.width/W, sy = rect.height/H, and use ctx.setTransform(sx*dpr,0,0,sy*dpr,0,0).

Handle resize by re-setting transform each frame (cheap) — just set at start of draw.

Let me write the full code.

```js
const W=960,H=720;
```

Brick layout:
const COLS=10, ROWS=8;
const TOP=96, LEFT=44, GAP=6;
const avail = W - 2*LEFT - GAP*(COLS-1); // 960-88-54=818
const bw = avail/COLS = 81.8
const bh = 26;

Brick generation: for each row r, col c: skip some cells for variety? Keep full grid for "at least 6 rows". Maybe leave a couple gaps in the middle row for a pattern. I'll keep all 80 bricks to guarantee victory achievable.

Types: 
- tough: rows 6 and 7 (bottom two) mostly tough, plus some in row 5.
Let me define: tough if r >= 5 (rows 5,6,7) → 30 tough bricks. Plus maybe some in row 4 random. Fine.

Power-up bricks: pick e.g. 6 bricks randomly (not on bottom row? bottom row fine). Types alternate widen/multiball.

Brick object: {x,y,w,h,hp,maxHp,color,row,col,type:'normal'|'tough'|'power', powerType:'wide'|'multi', cracked:false, flash:0}

Hit effect: flash timer to make brick flash white briefly.

Score: normal 50, tough first hit 25, tough break 100, power 150.

Let's now write the drawing of a brick:

function drawBrick(b):
  ctx.save();
  rounded rect path.
  fill: base color with vertical gradient (lighten top).
  if tough: use metallic palette color based on row: e.g. '#5b6b8c' fill with silver border. Let's give tough bricks their own color: '#8b93b0' gradient to '#4d5570' with a light border.
  if power: gradient of gold/green plus glow shadow.
  flash: overlay white alpha.
  cracked: draw crack lines (deterministic per brick using seed from col/row) in dark color, plus a couple of chips.
  if power: draw icon in center.
  restore.

Crack drawing: use a deterministic pseudo-random path based on (row*31+col*17).

Let me implement drawCracks(ctx, x,y,w,h, seed): draw 3 polylines from center outward.

Icon drawing:
- wide: draw a small wide paddle shape (rounded rect wide) with left/right arrows.
- multi: three circles.

Drop power-up: capsule with color matching, icon inside, glow.

Now particles: {x,y,vx,vy,life,maxLife,size,color}. Update: vy += 900*dt; x+=vx*dt; life-=dt; draw as square/circle with alpha.

Also screen shake on brick break (small). ctx.translate with random offsets; keep track of shake magnitude decaying.

Ball trail: store last N positions, draw with decreasing alpha and radius.

Now let's write the main loop:

let last = performance.now();
function frame(now){ dt = Math.min(0.033, (now-last)/1000); last=now; update(dt); draw(); requestAnimationFrame(frame);}

update:
 - if state==='demo' or 'playing': update paddle, balls, bricks, powerups, particles, ball timers.
 - In demo: if lives<3? keep lives at 3 (don't decrement) and if all balls lost → reset ball to paddle and continue.
 - In playing: ball loss → lives--, if lives<=0 → gameover overlay; else reset ball attached to paddle.

Ball attached state: ball.state = 'attached' when waiting for launch; position follows paddle; when playing, launched.

Simplify: when a life is lost, create attached ball at paddle center; it launches when player clicks or moves? Auto-launch after a short delay (0.8s) or on click/space. I'll auto-launch after 0.6s in playing mode, and immediately in demo mode.

Ball attached: if ball.ready true: ball.x = paddle.x + paddle.w/2; ball.y = paddle.y - r - 2.

Also implement "launch" with slight random angle.

Now input:
- mousemove on canvas → target paddle x = mouse.x - paddle.w/2 (in world coords). set mouseActive = true, and if state==='demo' startGame().
- keydown ArrowLeft/ArrowRight → control via keys; startGame on any key in demo.
- In playing, arrow keys move paddle with acceleration.

Paddle control: 
if mouseMoved recently (within 2s) use mouse target; else keyboard. Or combine: keyboard adds velocity, mouse sets position directly. I'll do: keys set paddle velocity; mouse sets paddle.x directly (and sets lastMouseTime). Update: if keyLeft/keyRight → paddle.x += dir * speed * dt, and ignore mouse until mouse moves again. Use flag mouseHasFocus set true on mousemove; arrow key sets it false.

Actually simpler and standard: use both — arrow keys move paddle, mouse moves paddle; whichever was last used wins.

Implementation:
let controlMode = 'mouse'; // or 'keys'
on mousemove: controlMode='mouse'; mouseTargetX = worldX - paddle.w/2
on keydown of arrows: controlMode='keys'
update: if controlMode==='mouse' → paddle.x += (mouseTargetX - paddle.x) * min(1, dt*18) (smooth) ; else keyboard.

Clamp paddle.x in [0, W-paddle.w].

Wide paddle changes width → keep center stable: when widening, paddle.x -= (newW-oldW)/2.

Now the demo AI: controlMode='ai'; target = predict. In demo we set paddle.x toward target.

Ball prediction: time to reach paddle y: t = (paddle.y - ball.y)/ball.vy if vy>0; predictedX = ball.x + ball.vx*t. target = predictedX - paddle.w/2.

If ball.vy <= 0 or attached → target = ball.x.

Now the overlays (DOM):
```html
<div id="overlay" class="overlay">
  <div class="panel">
    <h1 id="ovTitle">BRICK BREAKER</h1>
    <p id="ovMsg">Click or press a key to start</p>
    <button id="ovBtn">Start</button>
    <div class="hint">Move: Mouse or ← → · Space to launch</div>
  </div>
</div>
```
Overlay absolutely positioned over canvas within .stage wrapper.

Style: backdrop blur, dark panel, neon border.

Handle states: showOverlay(title,msg,btnLabel).

Also HUD drawn on canvas.

Let me also add sound? Could add simple WebAudio blips — optional but "polished". WebAudio might be blocked without user gesture; but demo mode runs before gesture → audio context suspended; after click it resumes. I'll add simple sounds with a lazily created AudioContext, guarded by try/catch. Keep it subtle. Actually risk of errors; I'll include carefully with try/catch and only resume on user gesture.

Let me include it—small function beep(freq, dur, type, gain).

Now write everything.

Also mobile: touch events to move paddle. Add touchstart/touchmove → controlMode='mouse' style. Nice for narrow screens.

Responsive: container max-width 960px, padding. At 375px width, canvas is ~343px wide, height 257px — small but fine. Maybe adjust: on narrow screens, use W smaller? Keep world coords; just scale.

Let me write the code.

Detailed code:

```js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = 960, H = 720;
```

resize function:
```js
function resize(){
  const rect = canvas.getBoundingClientRect();
  const dpr = Math.min(2, window.devicePixelRatio||1);
  canvas.width = Math.round(rect.width*dpr);
  canvas.height = Math.round(rect.height*dpr);
}
window.addEventListener('resize', resize);
```
And in draw: const sx = canvas.width/W, sy=canvas.height/H; ctx.setTransform(sx,0,0,sy,0,0).

Since canvas.width/dpr uses CSS pixels * dpr, canvas.width/W = (rect.width*dpr)/W which scales properly.

Initial: call resize after layout; also use ResizeObserver.

Now write the brick creation:

```js
function buildLevel(){
  bricks = [];
  const powerCells = new Set();
  // choose some power bricks deterministically-random
  ...
}
```
Use Math.random for variety but deterministic enough.

Let me pick power-up bricks: choose 7 random positions among rows 0..5 (avoid too many). Alternate wide/multi.

Brick colors by row: palette array of 8.

Let's write drawing helpers:

```js
function roundRect(ctx,x,y,w,h,r){ ... ctx.beginPath(); ... }
```

Let me write the final code now. I'll be careful about correctness.

Full script outline:

```js
// ---------- state ----------
let state='demo'; // demo | playing | gameover | victory
let score=0, lives=3, level=1;
let bricks=[], balls=[], powerups=[], particles=[], floaters=[];
let paddle;
let widenTimer=0;
let shake=0;
let bricksBroken=0;
let ballSpeed=400;
```

floaters: score popups "+50" floating text.

update(dt):
```
if(state==='gameover'||state==='victory'){ updateParticles(dt); return; }
// paddle
updatePaddle(dt);
// balls
for each ball: updateBall(ball,dt)
// powerups
// particles
// widen timer
```

updateBall:
```
if(ball.attached){ ball.x = paddle.x+paddle.w/2; ball.y=paddle.y-ball.r-1; ball.launchT-=dt; if(ball.launchT<=0){ attach=false; set direction up with slight random } return; }
```
Hmm attached ball needs launching. In playing mode, launch on space or automatically after 1s. Let's auto after 0.9s, and space launches immediately.

Ball movement with substeps:
```
const speed = Math.hypot(ball.vx, ball.vy) || ballSpeed;
const stepDist = Math.min(speed*dt, 8);
const steps = Math.max(1, Math.ceil(stepDist/3));
const sdt = dt/steps;
for(let s=0;s<steps;s++){
  ball.px=ball.x; ball.py=ball.y;
  ball.x+=ball.vx*sdt; ball.y+=ball.vy*sdt;
  // walls
  if(ball.x-ball.r<0){ball.x=ball.r; ball.vx=Math.abs(ball.vx);}
  if(ball.x+ball.r>W){ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx);}
  if(ball.y-ball.r<0){ball.y=ball.r; ball.vy=Math.abs(ball.vy);}
  // paddle
  if(ball.vy>0 && checkPaddle(ball)) { /* handled inside */ }
  // bricks
  if(checkBricks(ball)) { /* bounce */ }
}
```
Careful: after reflecting, ball.vx/ball.vy magnitudes stay same.

checkPaddle: 
```
const py = paddle.y, ph = paddle.h;
const prevBottom = ball.py + ball.r;
const curBottom = ball.y + ball.r;
if(curBottom >= py && prevBottom <= py + ph + 2 && ball.vy>0){
   // check horizontal overlap with swept paddle
   const px1 = Math.min(paddle.prevX, paddle.x), px2 = Math.max(paddle.prevX+paddle.w, paddle.x+paddle.w);
   if(ball.x+ball.r >= px1 && ball.x-ball.r <= px2){
       // top bounce
       const off = clamp((ball.x - (paddle.x+paddle.w/2))/(paddle.w/2), -1, 1);
       const ang = off * MAX_ANGLE; // MAX_ANGLE = 1.05 rad (~60°)
       ball.vx = Math.sin(ang)*speed;
       ball.vy = -Math.cos(ang)*speed;
       ball.y = py - ball.r - 0.5;
       ball.hits++;
       maybe increase speed slightly
       sfx
   }
}
```
Wait: if ball comes from the side below paddle top, prevBottom <= py+ph+2 may be true while ball is below paddle. Let's refine: require ball.y < py + ph (ball center above paddle bottom) i.e. ball center is in the upper region. Combine: curBottom >= py && ball.y < py + ph && horizontal overlap. Then top bounce. Else if ball overlaps paddle rect entirely (side hit) → reflect vx.

I'll implement generic circle-vs-paddle-rect resolution:
```
function collidePaddle(ball){
  const rect = {x:paddle.x,y:paddle.y,w:paddle.w,h:paddle.h};
  const cx = clamp(ball.x, rect.x, rect.x+rect.w);
  const cy = clamp(ball.y, rect.y, rect.y+rect.h);
  let dx=ball.x-cx, dy=ball.y-cy;
  const dist=Math.hypot(dx,dy);
  if(dist>=ball.r) return false;
  // resolve
  if(dist>0.0001){ nx=dx/dist; ny=dy/dist; } else { inside → push up }
  ...
}
```
But that generic approach loses the "angle by offset" requirement. So: do the offset-based bounce when the hit is on the top surface (ball center above paddle center or prevBottom <= paddle.y+ small). Else generic side reflect.

Plan:
```
const prevBottom = ball.py + ball.r;
// top-face test
if(ball.vy>0 && prevBottom <= paddle.y + 2 && ball.y + ball.r >= paddle.y){
   const cx = clamp(ball.x, paddle.x, paddle.x+paddle.w);
   if(ball.x + ball.r >= paddle.x && ball.x - ball.r <= paddle.x + paddle.w){
      // could be corner hit: if ball.x < paddle.x or > paddle.x+paddle.w → side
   }
}
```
Hmm, corner case: ball hits the very edge of the paddle from above but x slightly outside → in classic breakout it deflects sideways. Let's handle: if the ball center is within [paddle.x - something, paddle.x+paddle.w + something] treat as top hit; the offset formula naturally gives a sharp angle near edges. If ball center is outside the paddle horizontally by more than radius → side collision handled by generic.

I'll do:
```
// determine collision using swept top test
const topPrev = ball.py + ball.r, topNow = ball.y + ball.r;
const overX = ball.x + ball.r > paddle.x && ball.x - ball.r < paddle.x + paddle.w;
const hitTop = ball.vy > 0 && topNow >= paddle.y && topPrev <= paddle.y + 6 && overX;
```
If hitTop: bounce with offset angle using ball.x clamped relative to paddle center. Also ensure ball doesn't get stuck: ball.y = paddle.y - ball.r - 0.5.
Else: generic circle-rect resolution for side/bottom hits (rare). For side hit, reflect vx and push out.

That should be robust enough given small substeps.

Edge case: paddle moves up? Paddle doesn't move vertically. Good.

Also ball could tunnel through the paddle top if it's fast and the paddle is thin (16px) but substeps of 3px handle it.

Now bricks collision: circle vs AABB as described. After resolving, the ball's speed magnitude preserved.

One more consideration: to prevent the ball from getting stuck between two adjacent bricks (bouncing repeatedly), we resolve only one brick per substep and add a tiny separation.

After resolving a brick hit with hp>0 (tough brick first hit), the ball bounces off; fine.

Score/floater and particles on break.

Check win: bricks.every(b=>b.hp<=0) → victory. Use bricksRemaining counter.

Let me also add brick flash and small screen shake on break.

Power-up drop: when power brick broken, spawn powerup at brick center.

Now, AI demo and player control.

Let me code the demo AI target:
```
function aiTarget(){
  // find the lowest descending ball
  let best=null;
  for(const b of balls){ if(b.attached) continue; if(!best || b.y>best.y) best=b; }
  if(!best) return paddle.x+paddle.w/2 - paddle.w/2;
  if(best.vy<=0) return best.x - paddle.w/2;
  const t = (paddle.y - best.y)/best.vy;
  let px = best.x + best.vx*t;
  return px - paddle.w/2;
}
```
Smooth follow.

Now the DOM overlay updates. Let me code:

```js
function showOverlay(title, sub, btn){
  overlay.style.display='flex';
  ovTitle.textContent=title; ovMsg.innerHTML=sub; ovBtn.textContent=btn;
}
function hideOverlay(){ overlay.style.display='none'; }
```

State transitions:
- init: state='demo'; reset level; showOverlay('BRICK BREAKER', 'Click or press a key to start', 'Start Game');
- startGame(): if state==='demo' → full reset (score=0,lives=3,level=1, rebuild level, ball attached). state='playing'; hideOverlay().
  if state==='gameover' or 'victory' → same reset, state='playing'.
- on click on canvas or button → startGame().
- on keydown: if state!=='playing' → startGame(); else if space → launch.

Careful: keydown of arrow keys while in gameover restarts — acceptable.

Also in 'demo' state, if the AI clears the level → rebuild the level (attract loop) and keep score? Reset score maybe. I'll just rebuild bricks and reset balls, keep state demo.

Also demo: lives never decrease; if all balls lost → respawn attached ball.

Now writing the actual code. Let me be thorough with visuals.

Background: 
- gradient from #0b1020 to #131a33.
- subtle animated starfield / floating particles? Keep light: draw a grid of faint dots, plus a few slowly drifting bokeh circles.
- Vignette overlay.

HUD (top of canvas, y 0..88):
- Left block: "SCORE" small caps gray, number big bold neon.
- Right: "LIVES" + hearts.
- Center: level chip "LEVEL 1" and maybe bricks remaining bar.
Also a thin separator line at y=88.

Let me draw hearts as small heart shapes with path.

Wide paddle indicator: a bar above paddle showing remaining time with label "WIDE".

Let me write the heart path:
```js
function drawHeart(x,y,s,color){
 ctx.save(); ctx.translate(x,y); ctx.scale(s/10,s/10);
 ctx.beginPath();
 ctx.moveTo(0,3);
 ctx.bezierCurveTo(-6,-3,-10,1,-10,5);
 ctx.bezierCurveTo(-10,10,-4,13,0,16);
 ctx.bezierCurveTo(4,13,10,10,10,5);
 ctx.bezierCurveTo(10,1,6,-3,0,3);
 ctx.closePath(); ctx.fillStyle=color; ctx.fill(); ctx.restore();
}
```
Heart points down (y down) → ok.

Now let me write the code fully.

Also add a subtle "ball glow" using shadowBlur.

I'll write everything now, then sanity check.

One more: "The ball must never pass through bricks or the paddle." Our substep approach with max 3px steps plus AABB checks ensures this as long as ball radius ≥ step size relative to brick size — brick height 26, width 81. Fine. But there's a corner case: ball tunneling exactly through a brick's corner if the movement per substep is 3px — the collision check uses the new center position only; if the ball passes through a thin edge in one substep... Ball radius 9, so a 3px step can't jump over the brick's 26px height. Safe.

Let me also handle the case where a brick hit occurs but the ball is inside the brick (dist==0) — push out along the minimal axis.

Code for hitBrick:
```js
function hitBrick(b, ball){
  b.flash = 0.15;
  if(b.hp>1){ b.hp--; b.cracked=true; score+=25; floater(b.x+b.w/2,b.y,'25'); sfx('tick'); }
  else {
    b.hp=0; bricksRemaining--;
    const pts = b.type==='power'?150: (b.type==='tough'?100:50);
    score+=pts; floater(...);
    spawnParticles(b);
    if(b.type==='power') spawnPowerup(b);
    shake=Math.min(12, shake+4);
    sfx('break');
    // speed up slightly
    ballSpeed = Math.min(700, ballSpeed+2);
  }
}
```

Note: after hp-- on tough brick, second hit gives full points.

Speed increase: base 400 → after 80 bricks maybe +160 → cap 700. But the ball speed is per ball; I'll keep a global ballSpeed and apply to all balls after each break: for each ball, scale velocity to ballSpeed.

Simpler: keep constant speed per ball set at creation; on break, multiply all balls' speeds by 1.004 and cap. I'll maintain `speedScale`.

Let's keep: let speed = 400; each break: speed = Math.min(700, speed+3). Then after any reflection, renormalize ball velocity to `speed`. Implement in a helper `normalizeSpeed(ball)`.

Multi-ball: when power-up caught, spawn 2 extra balls from the current ball's position with angles ±25° from current direction, and the current ball keeps going. Total 3 balls.

Track total balls: when a ball falls, remove it; if no balls remain → lose life.

Widen: paddle.w = 190 for 10s, then back to 130. Draw timer bar.

Now particles colors: use brick color; for tough brick break use its color.

Let me write the final HTML.

Also add: instructions row under canvas (small text) for desktop; and the HUD in canvas.

I'll write it now in full.

```html
<!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:#070b16; }
 *{box-sizing:border-box}
 body{margin:0;min-height:100vh;background:radial-gradient(1200px 800px at 50% -10%,#1b2450 0%,#070b16 60%); color:#e8ecff; font-family:...; display:flex; flex-direction:column; align-items:center; padding:16px; }
 .wrap{width:100%;max-width:960px;}
 h1{...}
 .stage{position:relative; width:100%; aspect-ratio:4/3; border-radius:18px; overflow:hidden; box-shadow:0 20px 60px rgba(0,0,0,.6), 0 0 0 1px rgba(120,150,255,.15);}
 canvas{display:block;width:100%;height:100%; touch-action:none; cursor:none;}
 .overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; background:rgba(5,8,20,.62); backdrop-filter:blur(6px);}
 .panel{text-align:center;padding:28px 34px;border-radius:16px;background:rgba(12,18,40,.75);border:1px solid rgba(130,160,255,.25); box-shadow:0 0 40px rgba(60,90,255,.25); max-width:90%}
 .panel h2{font-size:clamp(24px,5vw,44px); margin:0 0 8px; letter-spacing:2px; background:linear-gradient(90deg,#7dd3fc,#a78bfa,#f472b6); -webkit-background-clip:text; color:transparent;}
 ...
</style>
```

Use system fonts to avoid external fonts: font-family: "Segoe UI", system-ui, -apple-system, sans-serif.

Now the canvas cursor:none so mouse control feels like the paddle follows. Maybe keep cursor crosshair. I'll use cursor:none but only when overlay hidden... keep simple: cursor:none.

Let me now produce the final code.

I need to double check the paddle control with mouse: mousemove on canvas gives clientX → world x = (clientX - rect.left) * W/rect.width.

Write it.

Also handle pointerdown on canvas → startGame() (and if playing, launch attached ball).

Let's write final code carefully.

```js
const overlay=document.getElementById('overlay');
const ovTitle=document.getElementById('ovTitle');
const ovMsg=document.getElementById('ovMsg');
const ovBtn=document.getElementById('ovBtn');
const ovStats=document.getElementById('ovStats');
```

showOverlay with optional stats text (final score).

Now write update functions.

Also add a "combo"? Not needed.

Let me write out the code.

```js
// ---------- Audio ----------
let actx=null;
function beep(freq,dur,type='sine',vol=0.06){
  try{
    if(!actx) actx = new (window.AudioContext||window.webkitAudioContext)();
    if(actx.state==='suspended') actx.resume();
    const o=actx.createOscillator(), g=actx.createGain();
    o.type=type; o.frequency.value=freq;
    g.gain.setValueAtTime(vol, actx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+dur);
    o.connect(g); g.connect(actx.destination);
    o.start(); o.stop(actx.currentTime+dur);
  }catch(e){}
}
```
This may produce many oscillators but fine. Maybe limit beep rate. OK.

Now full code writing.

For the brick "crack" visual, I'll draw jagged lines:

```js
function drawCracks(b){
  const seed=(b.row*7+b.col*13)%97;
  const rnd=(i)=> ((Math.sin(seed*12.9898+i*78.233)*43758.5453)%1+1)%1;
  ctx.save();
  ctx.strokeStyle='rgba(15,18,30,.85)'; ctx.lineWidth=1.6; ctx.lineCap='round';
  for(let k=0;k<3;k++){
    ctx.beginPath();
    let x=b.x+b.w*(0.2+0.6*rnd(k)), y=b.y+b.h*0.5;
    ctx.moveTo(x,y);
    for(let s=0;s<3;s++){
      x += (rnd(k*10+s)-0.5)*b.w*0.35;
      y += (rnd(k*20+s)-0.5)*b.h*0.9;
      ctx.lineTo(x,y);
    }
    ctx.stroke();
  }
  ctx.restore();
}
```

Now, the tough brick must "visibly crack after the first one" — done, plus change color slightly darker and maybe show a small "chip" corner.

Power-up brick icon drawing:
```js
function drawPowerIcon(x,y,type,size){
  ctx.save(); ctx.translate(x,y);
  if(type==='wide'){
    // wide paddle with arrows
    ctx.fillStyle='rgba(255,255,255,.95)';
    roundRect(ctx,-size*0.45,-size*0.16,size*0.9,size*0.32,size*0.12); ctx.fill();
    // arrows
    ctx.strokeStyle='#0b1020'; ctx.lineWidth=2; ...
  } else {
    // three balls
    ctx.fillStyle='rgba(255,255,255,.95)';
    for(let i=-1;i<=1;i++){ ctx.beginPath(); ctx.arc(i*size*0.32,0,size*0.18,0,Math.PI*2); ctx.fill(); }
  }
  ctx.restore();
}
```

Now write draw() fully.

Time to produce the answer. I'll write it carefully and completely.

For the wide-paddle timer bar: draw above paddle: rect width proportional to remaining/10 with gradient, label "WIDE".

Also for multi-ball active indicator: show a badge "×3" in HUD? Show small text near lives: "BALLS 3". I'll compute balls count and display.

Let me finalize the HUD layout:
- y=18: labels.
- Left at x=28: "SCORE" (12px, letter-spacing, color #7f8bb5) at y=30; value at y=64 font 34px bold.
- Right at x=W-28 (right aligned): "LIVES" label; hearts row under it.
- Center: "LEVEL 1" chip at x=W/2, y=30; bricks remaining bar: text "BRICKS 72" at y=56? Let's do center chip with level and a progress bar of bricks destroyed.

Fine.

Writing the final answer now.

For the demo, ensure a ball is present: initDemo creates a ball attached to the paddle with launchT=0.5, and the AI moves the paddle. When the ball is attached in demo mode, the AI target = paddle center → it stays. Then launches upward.

Also the demo needs a visible message: the overlay panel shows "Click or press a key to start" — yes, plus the game is visible behind the blurred overlay (backdrop-filter blur). The screenshot will show a blurred game with the message. Good enough — but the requirement says something meaningful visible. The overlay is semi-transparent so the game is visible. Maybe make the overlay less opaque (rgba(5,8,20,.5)) so the game is visible behind. Good.

Now, the demo AI paddle should actually play well enough to break bricks within 3 seconds — the ball takes ~1s to travel from paddle to bricks, then bounces. Within 3 seconds, a few bricks break. Good.

Let me write the code.

Also add slight randomness in AI to avoid perfect play? It's fine.

Final: write the code.

One more detail: `balls` array and `attached` ball. When the ball falls (y - r > H), remove it. If balls.length === 0 → life lost.

For multi-ball the lives decrement only when all balls are gone. Good.

Writing now.

Also: pointer events for mobile: 'pointermove' works for mouse and touch (touch requires touch-action:none, set). Use pointermove on the canvas.

I'll use canvas.addEventListener('pointermove', ...) and 'pointerdown'.

Let me write out everything.

const MAX_ANGLE = 1.15 rad (~66°).

Also ensure ball doesn't go too horizontal: after paddle bounce, |off| ≤1 → angle ≤66°, so vy = -cos(66°)*speed = -0.4*speed, fine.

Speed: base 420. min vertical component ensures not stuck.

Also cap the ball speed relative to canvas.

Let me write the code now, final.

I also want to ensure that after a brick collision, the ball doesn't get stuck in a repeating collision with the same brick: after resolving, add a small epsilon along the normal and the brick's hp decreased; for a tough brick with hp remaining, the next frame the ball may overlap again if it was pushed exactly to the surface... we push out by penetration+0.5 so it should be clear.

Hmm, one subtlety: if the ball's center is inside the brick (dist≈0 case) we push it out but the normal chosen may push it into the neighboring brick → ping-pong. With 26px tall bricks and the ball moving 3px per substep, it should resolve.

Write it.

For particles: also spawn a few on tough-brick first hit (small dust).

Let me write the final code text.

Also the floating score texts: array of {x,y,text,life,color}.

I'll now compose the answer.

Regarding `state` and startGame: also need to reset `controlMode='mouse'` maybe to 'keys'? Set to 'mouse' default so mouse works; if the user only uses keys, the first arrow keydown sets 'keys'. But initially the mouse target may be unset (0). If the user moves the mouse, fine. Set mouseTargetX = paddle.x initially.

Let me write.

Final code:

```js
// ============ INIT ============
buildLevel();
state='demo';
showOverlay('NEON BREAKOUT','Click or press a key to start<br><span class="sub">Move the paddle with your mouse or the ← → arrow keys</span>','Start Game');
```
Hmm the panel has separate elements; I'll set ovMsg.innerHTML with the hint and ovBtn.

Layout of panel:
```
<h2 id="ovTitle">NEON BREAKOUT</h2>
<p id="ovMsg">Click or press a key to start</p>
<p class="keys">Mouse or ← → to move · Space to launch</p>
<button id="ovBtn">Start Game</button>
<p id="ovStats" class="stats"></p>
```

OK writing the answer now. I'll be complete and careful.