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
3 min 19 s
Cost
$0.019
Output tokens
31,699
Spent thinking
20,774 tokens
Speed
159.3 tok/s
Console errors
0
First token after
435 ms
First visible text after
2 min 21 s
Prompt tokens
692
Code size
31.0 KB
Finish reason
stop
Served by
Parasail
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"max"}
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 · 881 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout — Neon Brick Breaker</title>
<style>
  :root{
    --bg:#05070f;
    --text:#e8eefc;
    --muted:#8b9ac0;
    --cyan:#22d3ee;
    --violet:#a855f7;
  }
  *{box-sizing:border-box;}
  html,body{height:100%;}
  body{
    margin:0;
    background:
      radial-gradient(1100px 620px at 12% -12%, #172352 0%, transparent 58%),
      radial-gradient(900px 620px at 102% 2%, #37145a 0%, transparent 55%),
      var(--bg);
    color:var(--text);
    font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    display:flex;flex-direction:column;align-items:center;
    padding:12px 12px 20px;
    -webkit-font-smoothing:antialiased;
  }
  .wrap{width:100%;max-width:960px;display:flex;flex-direction:column;gap:9px;}
  header{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;}
  .brand{
    font-weight:800;letter-spacing:.3em;font-size:clamp(13px,2.6vw,19px);
    background:linear-gradient(90deg,#22d3ee,#a855f7);
    -webkit-background-clip:text;background-clip:text;color:transparent;
  }
  .stats{display:flex;gap:7px;flex-wrap:wrap;}
  .stat{
    background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.09);
    border-radius:12px;padding:5px 12px;min-width:74px;text-align:center;
  }
  .label{display:block;font-size:9px;letter-spacing:.2em;color:var(--muted);}
  .value{display:block;font-size:17px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.25;}
  .lives{display:flex;gap:5px;justify-content:center;align-items:center;height:23px;}
  .dot{width:11px;height:11px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#fff,#22d3ee);box-shadow:0 0 9px rgba(34,211,238,.85);transition:.25s;}
  .dot.off{background:#2a3350;box-shadow:none;}
  .stage{
    position:relative;width:100%;border-radius:16px;overflow:hidden;
    border:1px solid rgba(255,255,255,.10);
    box-shadow:0 22px 60px rgba(0,0,0,.62), inset 0 0 0 1px rgba(34,211,238,.06);
    background:#04060e;
  }
  canvas{display:block;width:100%;height:auto;touch-action:none;cursor:none;}
  .overlay{
    position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
    background:radial-gradient(ellipse at center, rgba(4,7,16,.12), rgba(4,7,16,.55));
    transition:opacity .4s ease;padding:10px;
  }
  .overlay.hidden{opacity:0;pointer-events:none;}
  .panel{
    text-align:center;padding:16px 24px;border-radius:16px;max-width:min(94%,470px);
    background:linear-gradient(160deg,rgba(23,32,66,.90),rgba(9,13,28,.92));
    border:1px solid rgba(255,255,255,.14);
    box-shadow:0 18px 50px rgba(0,0,0,.6);
    backdrop-filter:blur(6px);
  }
  .badge{
    display:inline-block;font-size:9.5px;letter-spacing:.24em;padding:4px 11px;border-radius:999px;
    background:linear-gradient(90deg,#22d3ee,#a855f7);color:#04121d;font-weight:800;margin-bottom:8px;
  }
  .panel h1{margin:2px 0 4px;font-size:clamp(23px,5.4vw,40px);letter-spacing:.06em;}
  .panel p{margin:4px 0;color:#b8c6e4;font-size:clamp(11.5px,2.3vw,14.5px);line-height:1.5;}
  .panel .cta{margin-top:11px;font-weight:700;color:#fff;}
  .panel .cta span{
    display:inline-block;border:1px solid rgba(255,255,255,.34);border-radius:8px;
    padding:3px 9px;margin:0 2px;background:rgba(255,255,255,.07);
  }
  .panel b{color:#7ef0ff;font-variant-numeric:tabular-nums;}
  .toast{
    position:absolute;left:50%;top:12px;transform:translateX(-50%);padding:7px 15px;border-radius:999px;
    font-weight:800;font-size:12.5px;letter-spacing:.09em;background:rgba(34,211,238,.16);
    border:1px solid rgba(34,211,238,.5);color:#c9f8ff;opacity:0;pointer-events:none;
    transition:opacity .25s,transform .25s;white-space:nowrap;
  }
  .toast.show{opacity:1;transform:translateX(-50%) translateY(4px);}
  .hintbar{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;color:#7f8db0;font-size:11px;letter-spacing:.03em;}
  .hintbar #modeTag{color:#67e8f9;font-weight:700;}
  kbd{
    background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-bottom-width:2px;
    border-radius:5px;padding:1px 5px;font-size:10px;font-family:inherit;color:#cbd6f0;
  }
  @media(max-width:560px){
    .stat{min-width:62px;padding:4px 8px;}
    .value{font-size:15px;}
    .label{font-size:8px;}
    .hintbar{font-size:10px;}
  }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="brand">BREAKOUT</div>
    <div class="stats">
      <div class="stat"><span class="label">SCORE</span><span class="value" id="score">0</span></div>
      <div class="stat"><span class="label">LIVES</span><span class="value lives" id="lives"></span></div>
      <div class="stat"><span class="label">BRICKS</span><span class="value" id="left">0</span></div>
      <div class="stat"><span class="label">COMBO</span><span class="value" id="combo">x1</span></div>
    </div>
  </header>

  <div class="stage" id="stage">
    <canvas id="game"></canvas>
    <div class="toast" id="toast"></div>
    <div class="overlay" id="overlay"><div class="panel" id="panel"></div></div>
  </div>

  <div class="hintbar">
    <div>Move: <kbd>&larr;</kbd> <kbd>&rarr;</kbd> or mouse &nbsp;•&nbsp; Serve: <kbd>Space</kbd>/click &nbsp;•&nbsp; Restart: <kbd>R</kbd></div>
    <div id="modeTag">DEMO AI — press any key to take control</div>
  </div>
</div>

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

/* ---------------------------------------------------------------- setup */
const W = 900, H = 640;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d', { alpha:false });
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const panel = document.getElementById('panel');
const elScore = document.getElementById('score');
const elLives = document.getElementById('lives');
const elLeft  = document.getElementById('left');
const elCombo = document.getElementById('combo');
const elToast = document.getElementById('toast');
const elMode  = document.getElementById('modeTag');

const ROW_COLORS = ['#ff4d6d','#ff7b54','#ffc247','#7ee787','#38bdf8','#5b8bff','#a855f7','#f472b6'];
const BALL_R = 8;
const PADDLE_W = 118, PADDLE_H = 15;
const PADDLE_Y = H - 58;
const BASE_SPEED = 335, MAX_SPEED = 585;
const WIDE_SECONDS = 10;

/* ------------------------------------------------------------- helpers */
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
function rr(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 hex2rgb(h){
  h=h.replace('#','');
  if(h.length===3) h=h.split('').map(c=>c+c).join('');
  const n=parseInt(h,16);
  return {r:(n>>16)&255,g:(n>>8)&255,b:n&255};
}
function shade(hex,amt){
  const c=hex2rgb(hex), t = amt<0?0:255, p=Math.abs(amt);
  return `rgb(${Math.round(c.r+(t-c.r)*p)},${Math.round(c.g+(t-c.g)*p)},${Math.round(c.b+(t-c.b)*p)})`;
}
function mulberry32(a){
  return function(){
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a>>>15, 1 | a);
    t = t + Math.imul(t ^ t>>>7, 61 | t) ^ t;
    return ((t ^ t>>>14) >>> 0) / 4294967296;
  };
}
function fitCanvas(){
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.round(W*dpr);
  canvas.height = Math.round(H*dpr);
  ctx.setTransform(dpr,0,0,dpr,0,0);
}
window.addEventListener('resize', fitCanvas);
fitCanvas();

/* --------------------------------------------------------------- state */
let mode = 'demo';          // 'demo' (AI) | 'player'
let state = 'serve';        // 'serve' | 'play' | 'over' | 'win'
let score = 0, lives = 3, combo = 1, comboTimer = 0, remaining = 0;
let balls = [], bricks = [], particles = [], drops = [], floaters = [];
let wideTimer = 0, serveTimer = 0, shake = 0, time = 0, flash = 0;
let paddle = { x: W/2 - PADDLE_W/2, w: PADDLE_W };
let keys = { left:false, right:false };
let mouseX = W/2, mouseActive = false;
let hudCache = {};

/* -------------------------------------------------------------- level */
function buildLevel(){
  bricks = [];
  const rows = 8, cols = 13, margin = 42, gap = 6, top = 56, vgap = 9, bh = 26;
  const bw = (W - margin*2 - gap*(cols-1)) / cols;
  const rng = mulberry32(913377);
  for(let r=0;r<rows;r++){
    for(let c=0;c<cols;c++){
      let hp = 1;
      if(r===0 || r===1) hp = 2;
      else if(r===3 && c%5===3) hp = 2;
      else if(r===5 && c%7===2) hp = 2;
      else if(r===6 && c%6===5) hp = 2;
      bricks.push({
        x: margin + c*(bw+gap), y: top + r*(bh+vgap), w: bw, h: bh,
        hp, maxHp: hp, color: ROW_COLORS[r % ROW_COLORS.length],
        power: null, cracks: null, alive: true, flash: 0
      });
    }
  }
  // sprinkle power-up bricks in the lower half
  const cands = bricks.filter(b => b.maxHp===1 && b.y > 120);
  for(let i=cands.length-1;i>0;i--){ const j=Math.floor(rng()*(i+1)); const t=cands[i]; cands[i]=cands[j]; cands[j]=t; }
  const types = ['wide','multi','wide','multi','wide','multi','wide'];
  for(let i=0;i<types.length;i++){ if(cands[i]) cands[i].power = types[i]; }
  remaining = bricks.length;
}

/* -------------------------------------------------------------- reset */
function resetGame(asDemo){
  score = 0; lives = 3; combo = 1; comboTimer = 0;
  wideTimer = 0; shake = 0; flash = 0;
  balls = []; particles = []; drops = []; floaters = [];
  buildLevel();
  paddle.w = PADDLE_W;
  paddle.x = W/2 - PADDLE_W/2;
  mode = asDemo ? 'demo' : 'player';
  state = 'serve';
  serveTimer = asDemo ? 0.7 : 1.1;
  ballOnPaddle();
  elMode.textContent = asDemo ? 'DEMO AI — press any key to take control' : 'You are in control';
  elMode.style.color = asDemo ? '#67e8f9' : '#f0abfc';
  updateHud(true);
  if(asDemo) showStartPanel(); else hidePanel();
}
function ballOnPaddle(){
  balls = [{ x: paddle.x + paddle.w/2, y: PADDLE_Y - BALL_R - 1, vx:0, vy:0,
             r: BALL_R, speed: BASE_SPEED, trail: [] }];
}
function launch(){
  const b = balls[0]; if(!b) return;
  const a = (Math.random()*0.85 - 0.42);
  b.speed = Math.min(b.speed, MAX_SPEED);
  b.vx = Math.sin(a) * b.speed;
  b.vy = -Math.cos(a) * b.speed;
  state = 'play';
}
function takeControl(){
  if(mode === 'player') return;
  mode = 'player';
  mouseActive = true;
  hidePanel();
  elMode.textContent = 'You are in control';
  elMode.style.color = '#f0abfc';
  toast('YOU HAVE CONTROL');
}
function loseLife(){
  lives--; combo = 1; wideTimer = 0; paddle.w = PADDLE_W;
  updateHud(true);
  if(lives <= 0){ gameOver(); return; }
  ballOnPaddle(); state = 'serve'; serveTimer = 1.0;
  toast(lives + (lives===1?' LIFE LEFT':' LIVES LEFT'));
}
function gameOver(){
  state = 'over';
  showPanel(`
    <div class="badge">GAME OVER</div>
    <h1>OUT OF BALLS</h1>
    <p>Final score <b>${score.toLocaleString()}</b> &nbsp;•&nbsp; bricks cleared <b>${bricks.length-remaining}/${bricks.length}</b></p>
    <p class="cta"><span>Click</span> or press <span>R</span> to play again</p>`);
}
function win(){
  state = 'win';
  for(let i=0;i<70;i++){
    particles.push({x:Math.random()*W, y:-Math.random()*H*0.6, vx:(Math.random()-.5)*180,
      vy:60+Math.random()*220, size:2+Math.random()*3.5, life:2.2, max:2.2,
      color:ROW_COLORS[(Math.random()*ROW_COLORS.length)|0]});
  }
  showPanel(`
    <div class="badge">VICTORY</div>
    <h1>ALL BRICKS CLEARED</h1>
    <p>Final score <b>${score.toLocaleString()}</b> &nbsp;•&nbsp; lives left <b>${lives}</b></p>
    <p class="cta"><span>Click</span> or press <span>R</span> to play again</p>`);
}

/* --------------------------------------------------------------- HUD */
function updateHud(force){
  const s = score.toLocaleString();
  if(force || hudCache.s !== s){ elScore.textContent = s; hudCache.s = s; }
  if(force || hudCache.l !== lives){
    if(elLives.children.length !== 3){
      elLives.innerHTML = '';
      for(let i=0;i<3;i++){ const d=document.createElement('span'); d.className='dot'; elLives.appendChild(d); }
    }
    for(let i=0;i<3;i++) elLives.children[i].classList.toggle('off', i>=lives);
    hudCache.l = lives;
  }
  if(force || hudCache.b !== remaining){ elLeft.textContent = remaining; hudCache.b = remaining; }
  if(force || hudCache.c !== combo){ elCombo.textContent = 'x'+combo; hudCache.c = combo; }
}
let toastTimer = null;
function toast(msg){
  elToast.textContent = msg;
  elToast.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(()=>elToast.classList.remove('show'), 1700);
}
function showStartPanel(){
  showPanel(`
    <div class="badge">DEMO MODE</div>
    <h1>BREAKOUT</h1>
    <p>The AI is playing right now — watch the ball bounce off the paddle edges for sharp angles.</p>
    <p class="cta"><span>Click</span> or press a key to start playing</p>`);
}
function showPanel(html){ panel.innerHTML = html; overlay.classList.remove('hidden'); }
function hidePanel(){ overlay.classList.add('hidden'); }

/* ------------------------------------------------------------ paddle */
function setPaddleWidth(w){ paddle.w = w; paddle.x = clamp(paddle.x, 4, W - paddle.w - 4); }

function aiTarget(){
  if(!balls.length) return W/2;
  let best = null, bestT = Infinity;
  for(const b of balls){
    if(b.vy <= 0) continue;
    const t = (PADDLE_Y - b.y) / b.vy;
    if(t > 0 && t < bestT){ bestT = t; best = b; }
  }
  if(best){
    const period = 2*W;
    let nx = ((best.x + best.vx*bestT) % period + period) % period;
    if(nx > W) nx = period - nx;
    return nx;
  }
  let low = balls[0];
  for(const b of balls) if(b.y > low.y) low = b;
  return low.x;
}
function updatePaddle(dt){
  if(mode === 'demo'){
    const target = aiTarget() - paddle.w/2;
    const diff = target - paddle.x;
    const step = 640*dt;
    paddle.x += clamp(diff, -step, step);
  } else {
    const dir = (keys.right?1:0) - (keys.left?1:0);
    if(dir !== 0){ paddle.x += dir*760*dt; mouseActive = false; }
    else if(mouseActive){
      const target = mouseX - paddle.w/2;
      paddle.x += (target - paddle.x) * Math.min(1, dt*26);
    }
  }
  paddle.x = clamp(paddle.x, 4, W - paddle.w - 4);
}
function bounceOffPaddle(b){
  const rel = clamp((b.x - (paddle.x + paddle.w/2)) / (paddle.w/2), -1, 1);
  const MAXA = 1.05;                            // 60 degrees from vertical
  const angle = rel * MAXA;
  const sp = b.speed;
  b.vx = Math.sin(angle) * sp;
  b.vy = -Math.cos(angle) * sp;
  const minVy = sp*0.30;                       // never travel perfectly flat
  if(Math.abs(b.vy) < minVy){
    const s = b.vy >= 0 ? 1 : -1;
    b.vy = s*minVy;
    const vx = Math.sqrt(Math.max(0, sp*sp - b.vy*b.vy));
    b.vx = (b.vx >= 0 ? 1 : -1) * vx;
  }
  combo = 1; comboTimer = 0;
  if(wideTimer > 0) flash = Math.max(flash, 0.12);
}

/* ------------------------------------------------------ brick impact */
function makeCracks(br){
  const out = [];
  for(let i=0;i<3;i++){
    const pts = [];
    let px = 0.12 + Math.random()*0.76, py = Math.random();
    pts.push([px,py]);
    for(let s=0;s<3;s++){
      px = clamp(px + (Math.random()-0.5)*0.55, 0.02, 0.98);
      py = clamp(py + (Math.random()-0.5)*0.55, 0.02, 0.98);
      pts.push([px,py]);
    }
    out.push(pts);
  }
  return out;
}
function spawnParticles(br, n, white){
  n = n || 13;
  for(let i=0;i<n;i++){
    const a = Math.random()*Math.PI*2, sp = 60 + Math.random()*240;
    particles.push({
      x: br.x + br.w/2 + (Math.random()-0.5)*br.w,
      y: br.y + br.h/2 + (Math.random()-0.5)*br.h,
      vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - 70,
      size: 1.8 + Math.random()*3.4, life: 0.45 + Math.random()*0.55, max: 1,
      color: (white && Math.random()<0.45) ? '#ffffff' : br.color
    });
  }
}
function floatText(x,y,text,color){
  floaters.push({x,y,text,color,life:0.85,max:0.85});
}
function hitBrick(br){
  br.hp--;
  br.flash = 0.18;
  combo = Math.min(5, combo+1); comboTimer = 2.4;
  if(br.hp <= 0){
    br.alive = false;
    const pts = (br.maxHp === 2 ? 120 : 50) * combo;
    score += pts;
    spawnParticles(br, br.maxHp === 2 ? 18 : 12);
    shake = Math.min(8, shake + (br.maxHp === 2 ? 4.2 : 2.4));
    floatText(br.x + br.w/2, br.y, '+' + pts, br.color);
    if(br.power) spawnDrop(br);
    remaining--;
    if(remaining <= 0){ win(); }
  } else {
    score += 20;
    if(!br.cracks) br.cracks = makeCracks(br);
    spawnParticles(br, 6, true);
    floatText(br.x + br.w/2, br.y, 'CRACK!', '#ffd166');
    shake = Math.min(8, shake + 1.2);
  }
  updateHud();
}
function spawnDrop(br){
  drops.push({ x: br.x + br.w/2, y: br.y + br.h/2, vy: 175, type: br.power, rot: 0, t: 0 });
}
function collideBrick(b, br){
  const cx = clamp(b.x, br.x, br.x + br.w);
  const cy = clamp(b.y, br.y, br.y + br.h);
  let dx = b.x - cx, dy = b.y - cy;
  const d2 = dx*dx + dy*dy;
  if(d2 > b.r*b.r) return false;
  let nx, ny, depth;
  if(d2 > 1e-8){
    const d = Math.sqrt(d2); nx = dx/d; ny = dy/d; depth = b.r - d;
  } else {
    const ol = b.x - br.x, orr = br.x + br.w - b.x, ot = b.y - br.y, ob = br.y + br.h - b.y;
    const m = Math.min(ol, orr, ot, ob);
    if(m === ot){ nx = 0; ny = -1; depth = ot + b.r; }
    else if(m === ob){ nx = 0; ny = 1; depth = ob + b.r; }
    else if(m === ol){ nx = -1; ny = 0; depth = ol + b.r; }
    else { nx = 1; ny = 0; depth = orr + b.r; }
  }
  b.x += nx*(depth + 0.6);
  b.y += ny*(depth + 0.6);
  const dot = b.vx*nx + b.vy*ny;
  b.vx -= 2*dot*nx; b.vy -= 2*dot*ny;
  const sp = Math.hypot(b.vx, b.vy) || b.speed;
  b.speed = sp;
  const minVy = sp*0.28;
  if(Math.abs(b.vy) < minVy){
    const s = b.vy >= 0 ? 1 : -1;
    b.vy = s*minVy;
    const vx = Math.sqrt(Math.max(0, sp*sp - b.vy*b.vy));
    b.vx = (b.vx >= 0 ? 1 : -1)*vx;
  }
  hitBrick(br);
  return true;
}

/* ---------------------------------------------------------- ball step */
function stepBall(b, dt){
  // keep the speed normalised and slowly ramp it up
  b.speed = Math.min(MAX_SPEED, b.speed + 1.4*dt);
  const m = Math.hypot(b.vx, b.vy);
  if(m > 0.001){ b.vx = b.vx/m*b.speed; b.vy = b.vy/m*b.speed; }
  const dist = b.speed*dt;
  const steps = Math.max(1, Math.ceil(dist / (b.r*0.7)));
  const sdt = dt/steps;
  for(let i=0;i<steps;i++){
    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); }
    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); }
    // paddle
    if(b.vy > 0 && b.y + b.r >= PADDLE_Y && b.y - b.r <= PADDLE_Y + PADDLE_H &&
       b.x + b.r >= paddle.x && b.x - b.r <= paddle.x + paddle.w){
      if(b.y > PADDLE_Y + PADDLE_H*0.55){        // hit the side of the paddle
        b.x = b.x > paddle.x + paddle.w/2 ? paddle.x + paddle.w + b.r : paddle.x - b.r;
        b.vx = b.x > W/2 ? Math.abs(b.vx) : -Math.abs(b.vx);
        combo = 1; comboTimer = 0;
      } else {
        b.y = PADDLE_Y - b.r - 0.5;
        b.x = clamp(b.x, paddle.x + b.r, paddle.x + paddle.w - b.r);
        bounceOffPaddle(b);
      }
    }
    // bricks
    for(let k=0;k<bricks.length;k++){
      const br = bricks[k];
      if(!br.alive) continue;
      if(b.x + b.r < br.x || b.x - b.r > br.x + br.w) continue;
      if(b.y + b.r < br.y || b.y - b.r > br.y + br.h) continue;
      if(collideBrick(b, br)) break;
    }
  }
  b.trail.push({x:b.x, y:b.y});
  if(b.trail.length > 11) b.trail.shift();
}

/* ------------------------------------------------------------ effects */
function applyDrop(d){
  if(d.type === 'wide'){
    wideTimer = WIDE_SECONDS;
    setPaddleWidth(Math.round(PADDLE_W*1.7));
    flash = 0.35;
    toast('WIDE PADDLE — 10s');
  } else {
    const target = Math.max(3, Math.min(6, balls.length + 1));
    while(balls.length < target){
      const src = balls[0];
      const ang = (Math.random()-0.5)*1.3;
      const sp = src ? src.speed : BASE_SPEED;
      balls.push({
        x: src ? src.x : paddle.x + paddle.w/2,
        y: src ? src.y : PADDLE_Y - BALL_R - 2,
        vx: Math.sin(ang)*sp, vy: -Math.cos(ang)*sp,
        r: BALL_R, speed: sp, trail: []
      });
    }
    flash = 0.35;
    toast('MULTI-BALL ×' + balls.length);
  }
}
function updateDrops(dt){
  for(let i=drops.length-1;i>=0;i--){
    const d = drops[i];
    d.t += dt;
    d.vy = Math.min(260, d.vy + 90*dt);
    d.y += d.vy*dt;
    d.rot = Math.sin(d.t*4)*0.35;
    if(d.y + 13 >= PADDLE_Y && d.y - 13 <= PADDLE_Y + PADDLE_H &&
       d.x + 13 >= paddle.x && d.x - 13 <= paddle.x + paddle.w){
      applyDrop(d);
      drops.splice(i,1);
      continue;
    }
    if(d.y > H + 40) drops.splice(i,1);
  }
}
function updateParticles(dt){
  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 += 520*dt;
    p.x += p.vx*dt;
    p.y += p.vy*dt;
    p.vx *= (1 - 0.8*dt);
  }
}
function updateFloaters(dt){
  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);
  }
}

/* -------------------------------------------------------------- update */
function update(dt){
  time += dt;
  if(wideTimer > 0){
    wideTimer -= dt;
    if(wideTimer <= 0){ wideTimer = 0; setPaddleWidth(PADDLE_W); }
  }
  if(comboTimer > 0){ comboTimer -= dt; if(comboTimer <= 0) combo = 1; }
  shake = Math.max(0, shake - 22*dt);
  flash = Math.max(0, flash - dt*1.6);
  updatePaddle(dt);

  if(state === 'serve'){
    if(balls[0]){
      const b = balls[0];
      b.x = paddle.x + paddle.w/2;
      b.y = PADDLE_Y - BALL_R - 1;
      b.trail.length = 0;
    }
    serveTimer -= dt;
    if(serveTimer <= 0) launch();
  } else if(state === 'play'){
    for(let i=balls.length-1;i>=0;i--){
      const b = balls[i];
      stepBall(b, dt);
      if(b.y - b.r > H){ balls.splice(i,1); }
    }
    if(balls.length === 0) loseLife();
  }
  updateDrops(dt);
  updateParticles(dt);
  updateFloaters(dt);
  updateHud();
}

/* -------------------------------------------------------------- render */
function drawBackground(){
  const g = ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#0a1024'); g.addColorStop(.5,'#070b18'); g.addColorStop(1,'#04060e');
  ctx.fillStyle = g; ctx.fillRect(0,0,W,H);

  ctx.strokeStyle = 'rgba(110,150,255,.05)'; ctx.lineWidth = 1;
  ctx.beginPath();
  for(let x=30;x<W;x+=45){ ctx.moveTo(x+.5,0); ctx.lineTo(x+.5,H); }
  for(let y=30;y<H;y+=45){ ctx.moveTo(0,y+.5); ctx.lineTo(W,y+.5); }
  ctx.stroke();

  const lg = ctx.createLinearGradient(0,0,28,0);
  lg.addColorStop(0,'rgba(56,189,248,.20)'); lg.addColorStop(1,'rgba(56,189,248,0)');
  ctx.fillStyle = lg; ctx.fillRect(0,0,28,H);
  const lg2 = ctx.createLinearGradient(W,0,W-28,0);
  lg2.addColorStop(0,'rgba(168,85,247,.20)'); lg2.addColorStop(1,'rgba(168,85,247,0)');
  ctx.fillStyle = lg2; ctx.fillRect(W-28,0,28,H);

  ctx.fillStyle = 'rgba(125,211,252,.28)'; ctx.fillRect(0,0,W,2);
}
function drawIcon(c, type, cx, cy, s){
  c.save(); c.translate(cx,cy); c.scale(s,s);
  c.strokeStyle = '#fff'; c.fillStyle = '#fff';
  c.lineWidth = 1.9; c.lineCap = 'round'; c.lineJoin = 'round';
  if(type === 'wide'){
    c.beginPath(); c.moveTo(-7.5,0); c.lineTo(7.5,0); c.stroke();
    c.beginPath(); c.moveTo(-3,-4.2); c.lineTo(-7.6,0); c.lineTo(-3,4.2); c.closePath(); c.fill();
    c.beginPath(); c.moveTo(3,-4.2); c.lineTo(7.6,0); c.lineTo(3,4.2); c.closePath(); c.fill();
  } else {
    c.beginPath(); c.arc(-6.2,0,2.7,0,Math.PI*2); c.fill();
    c.beginPath(); c.arc(0,0,2.7,0,Math.PI*2); c.fill();
    c.beginPath(); c.arc(6.2,0,2.7,0,Math.PI*2); c.fill();
  }
  c.restore();
}
function drawBricks(){
  for(const br of bricks){
    if(!br.alive) continue;
    const {x,y,w,h} = br;
    ctx.save();
    if(br.power){
      ctx.shadowColor = 'rgba(253,224,71,.55)'; ctx.shadowBlur = 14;
    }
    const g = ctx.createLinearGradient(0,y,0,y+h);
    g.addColorStop(0, shade(br.color, .30));
    g.addColorStop(1, shade(br.color, -.18));
    ctx.fillStyle = g;
    rr(ctx,x,y,w,h,5); ctx.fill();
    ctx.shadowBlur = 0;

    ctx.fillStyle = 'rgba(255,255,255,.20)';
    rr(ctx,x+3,y+2.5,w-6,3,1.5); ctx.fill();

    if(br.maxHp === 2){
      ctx.strokeStyle = 'rgba(255,255,255,.34)'; ctx.lineWidth = 1.6;
      rr(ctx,x+1.2,y+1.2,w-2.4,h-2.4,4.5); ctx.stroke();
      ctx.fillStyle = 'rgba(0,0,0,.10)';
      rr(ctx,x+3,y+h-6,w-6,2.5,1.2); ctx.fill();
    }
    if(br.cracks){
      ctx.fillStyle = 'rgba(0,0,0,.20)';
      rr(ctx,x,y,w,h,5); ctx.fill();
      for(const poly of br.cracks){
        ctx.beginPath();
        for(let i=0;i<poly.length;i++){
          const px = x + poly[i][0]*w, py = y + poly[i][1]*h;
          if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
        }
        ctx.strokeStyle = 'rgba(0,0,0,.55)'; ctx.lineWidth = 1.7; ctx.stroke();
        ctx.strokeStyle = 'rgba(255,255,255,.16)'; ctx.lineWidth = 0.9; ctx.stroke();
      }
    }
    if(br.power){
      ctx.fillStyle = 'rgba(8,12,26,.55)';
      rr(ctx,x+w/2-11,y+h/2-11,22,22,7); ctx.fill();
      ctx.strokeStyle = 'rgba(253,224,71,.95)'; ctx.lineWidth = 1.5;
      rr(ctx,x+w/2-11,y+h/2-11,22,22,7); ctx.stroke();
      drawIcon(ctx, br.power, x+w/2, y+h/2, .78);
    }
    if(br.flash > 0){
      ctx.fillStyle = `rgba(255,255,255,${Math.min(.85, br.flash*4)})`;
      rr(ctx,x,y,w,h,5); ctx.fill();
      br.flash = Math.max(0, br.flash - 0.016);
    }
    ctx.restore();
  }
}
function drawDrops(){
  for(const d of drops){
    ctx.save();
    ctx.translate(d.x, d.y);
    ctx.rotate(d.rot);
    const wide = d.type === 'wide';
    ctx.shadowColor = wide ? 'rgba(34,211,238,.9)' : 'rgba(244,114,182,.9)';
    ctx.shadowBlur = 16;
    ctx.fillStyle = wide ? 'rgba(8,110,145,.95)' : 'rgba(178,25,90,.95)';
    rr(ctx,-13,-13,26,26,8); ctx.fill();
    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,.8)'; ctx.lineWidth = 1.6;
    rr(ctx,-13,-13,26,26,8); ctx.stroke();
    drawIcon(ctx, d.type, 0, 0, .92);
    ctx.restore();
  }
}
function drawPaddle(){
  const p = paddle;
  ctx.save();
  const wide = wideTimer > 0;
  const g = ctx.createLinearGradient(p.x,0,p.x+p.w,0);
  if(wide){ g.addColorStop(0,'#fde68a'); g.addColorStop(.5,'#fbbf24'); g.addColorStop(1,'#f59e0b'); }
  else    { g.addColorStop(0,'#22d3ee'); g.addColorStop(.5,'#60a5fa'); g.addColorStop(1,'#a855f7'); }
  ctx.shadowColor = wide ? 'rgba(251,191,36,.85)' : 'rgba(56,189,248,.8)';
  ctx.shadowBlur = 20;
  ctx.fillStyle = g;
  rr(ctx,p.x,PADDLE_Y,p.w,PADDLE_H,PADDLE_H/2); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,.4)';
  rr(ctx,p.x+5,PADDLE_Y+2.5,p.w-10,3.5,1.75); ctx.fill();
  if(wide){
    const by = PADDLE_Y - 11;
    ctx.fillStyle = 'rgba(0,0,0,.45)'; rr(ctx,p.x,by,p.w,5,2.5); ctx.fill();
    ctx.fillStyle = '#fbbf24'; rr(ctx,p.x,by,p.w*clamp(wideTimer/WIDE_SECONDS,0,1),5,2.5); ctx.fill();
  }
  ctx.restore();
}
function drawBalls(){
  for(const b of balls){
    for(let i=0;i<b.trail.length;i++){
      const t = b.trail[i], k = (i+1)/b.trail.length;
      ctx.beginPath();
      ctx.fillStyle = `rgba(190,235,255,${0.30*k})`;
      ctx.arc(t.x, t.y, b.r*(0.28+0.62*k), 0, Math.PI*2);
      ctx.fill();
    }
    ctx.save();
    ctx.shadowColor = 'rgba(125,211,252,.95)'; ctx.shadowBlur = 18;
    const g = ctx.createRadialGradient(b.x-2.5,b.y-3,1,b.x,b.y,b.r);
    g.addColorStop(0,'#ffffff'); g.addColorStop(1,'#38bdf8');
    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;
    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
  }
  ctx.globalAlpha = 1;
}
function drawFloaters(){
  ctx.textAlign = 'center';
  ctx.font = '700 13px ui-sans-serif,system-ui,sans-serif';
  for(const f of floaters){
    ctx.globalAlpha = clamp(f.life/f.max, 0, 1);
    ctx.fillStyle = f.color;
    ctx.fillText(f.text, f.x, f.y);
  }
  ctx.globalAlpha = 1;
  ctx.textAlign = 'left';
}
function drawCanvasHud(){
  // demo badge
  if(mode === 'demo'){
    ctx.save();
    ctx.font = '700 10px ui-sans-serif,system-ui,sans-serif';
    const txt = 'DEMO · AI PLAYING';
    const tw = ctx.measureText(txt).width;
    ctx.fillStyle = 'rgba(8,12,26,.65)';
    rr(ctx, W - tw - 30, 12, tw + 20, 22, 11); ctx.fill();
    ctx.strokeStyle = 'rgba(34,211,238,.5)'; ctx.lineWidth = 1;
    rr(ctx, W - tw - 30, 12, tw + 20, 22, 11); ctx.stroke();
    ctx.fillStyle = '#7ef0ff';
    ctx.textAlign = 'center';
    ctx.fillText(txt, W - tw - 20 + (tw)/2, 27);
    ctx.restore();
  }
  // combo
  if(combo > 1){
    ctx.save();
    ctx.textAlign = 'center';
    ctx.font = '800 20px ui-sans-serif,system-ui,sans-serif';
    ctx.fillStyle = 'rgba(255,209,102,.95)';
    ctx.shadowColor = 'rgba(255,180,60,.7)'; ctx.shadowBlur = 12;
    ctx.fillText('COMBO ×' + combo, W/2, 30);
    ctx.restore();
  }
  // power-up status
  if(wideTimer > 0){
    ctx.save();
    ctx.font = '700 11px ui-sans-serif,system-ui,sans-serif';
    ctx.fillStyle = '#fde68a';
    ctx.fillText('WIDE PADDLE  ' + wideTimer.toFixed(1) + 's', 16, H - 14);
    ctx.restore();
  }
  // serve hint
  if(state === 'serve' && mode === 'player'){
    ctx.save();
    ctx.textAlign = 'center';
    ctx.font = '700 15px ui-sans-serif,system-ui,sans-serif';
    ctx.fillStyle = 'rgba(226,238,255,.9)';
    ctx.fillText('Click or press SPACE to serve', W/2, PADDLE_Y - 42);
    ctx.restore();
  }
  if(flash > 0){
    ctx.fillStyle = `rgba(255,255,255,${flash*.28})`;
    ctx.fillRect(0,0,W,H);
  }
}
function render(){
  ctx.save();
  if(shake > 0.05) ctx.translate((Math.random()-.5)*shake, (Math.random()-.5)*shake);
  drawBackground();
  drawBricks();
  drawDrops();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawFloaters();
  drawCanvasHud();
  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);
  render();
  requestAnimationFrame(frame);
}

/* --------------------------------------------------------------- input */
function pointerToCanvas(e){
  const rect = canvas.getBoundingClientRect();
  mouseX = clamp((e.clientX - rect.left) * (W/rect.width), 0, W);
}
stage.addEventListener('pointermove', e => {
  pointerToCanvas(e);
  if(mode === 'demo' && e.movementX !== 0 && e.pointerType !== 'touch') takeControl();
});
stage.addEventListener('pointerdown', e => {
  pointerToCanvas(e);
  if(state === 'over' || state === 'win'){ resetGame(false); return; }
  if(mode === 'demo'){ takeControl(); }
  else if(state === 'serve'){ serveTimer = 0; launch(); }
});
window.addEventListener('keydown', e => {
  if(e.code === 'ArrowLeft'){ keys.left = true; e.preventDefault(); }
  if(e.code === 'ArrowRight'){ keys.right = true; e.preventDefault(); }
  if(e.code === 'Space') e.preventDefault();
  if(e.key === 'r' || e.key === 'R'){ resetGame(false); return; }
  if(state === 'over' || state === 'win'){ resetGame(false); return; }
  if(mode === 'demo'){ takeControl(); }
  if(e.code === 'Space' && state === 'serve'){ serveTimer = 0; launch(); }
});
window.addEventListener('keyup', e => {
  if(e.code === 'ArrowLeft') keys.left = false;
  if(e.code === 'ArrowRight') keys.right = false;
});
window.addEventListener('blur', () => { keys.left = keys.right = false; });

/* ---------------------------------------------------------------- boot */
resetGame(true);
requestAnimationFrame(frame);
setTimeout(() => { if(mode === 'demo') hidePanel(); }, 4200);
})();
</script>
</body>
</html>
Reasoning trace (20,774 tokens)
Let me build a polished Breakout game in a single HTML file.

Key requirements:
- Paddle controlled with mouse + left/right arrow keys
- Bounce angle depends on hit position (center = straight up, edges = sharp)
- One level, at least 6 rows of bricks, mixing normal bricks and tougher bricks (2 hits, visible cracks after first hit)
- Some bricks carry power-up icons; when broken drop them, catching with paddle gives wider paddle (10s) or multi-ball (3 balls)
- Ball never passes through bricks or paddle
- Particles when brick breaks
- 3 lives, score displayed
- Game over and victory screens with restart

Let me design this carefully.

Canvas size: let's do 900x600 for the play field, responsive scaling.

I'll use a fixed logical resolution (e.g., 900x620) and scale with CSS to fit viewport width. Canvas width=900 height=620, styled with max-width:100%, aspect-ratio maintained.

Let me plan the game loop:

State: 'ready' (click to start), 'playing', 'gameover', 'victory'.

Elements:
- Ball: {x,y,vx,vy,r, speed}
- Paddle: {x,y,w,h}
- Bricks: grid, cols=13, rows=8. Each brick: {x,y,w,h,hp,maxhp,color,type,icon, cracked}

Collision: since ball speed can be high, use sub-stepping: move ball in small steps (e.g., max 4px per step), checking collisions each step. That guarantees no tunneling.

Collision detection with bricks: axis-aligned rectangle vs circle. For each brick, compute closest point on rect to ball center; if distance < r, collision. Then determine which side to reflect: compute overlap in x and y, choose the smaller penetration.

Actually a cleaner approach: for a circle vs AABB collision, find the normal. Approach:
- dx = ball.x - clamp(ball.x, brick.x, brick.x+brick.w)
- dy = ball.y - clamp(ball.y, brick.y, brick.y+brick.h)
- dist = hypot(dx,dy)
If dist < r → collision. The normal direction: if dist > 0, normal = (dx/dist, dy/dist), penetration = r - dist. Else (center inside brick) → push out along the smaller overlap axis.

For a brick hit, the ball usually hits the bottom face → ball should bounce up. Using the closest-point normal generally works fine.

Let's implement:

function collideBricks(ball) {
  for each brick alive:
    compute closest point
    if collision:
      determine response
      apply damage (hp--), particles, score
      if hp==0 → destroy, maybe drop powerup
      else → crack visual
      return true (one collision per step)
}

For the crack visual: draw crack lines over the brick — precompute random crack polylines per brick at creation? Better: on first hit, generate crack lines (random seeded paths) stored on the brick. Draw them in a darker color.

Power-up: bricks marked with an icon (e.g., "W" for wide paddle, "M" for multi-ball, drawn as a small icon like a widen-arrows symbol or a letter). Let's use actual small vector icons: wide = horizontal double arrow ↔ (arrows pointing left/right), multi = three dots / three small circles. That looks nicer than letters.

Falling powerup: {x, y, vy, type, size}. Falls down at speed ~180px/s. When it reaches paddle region and overlaps paddle → apply effect. Also could bounce off walls? Keep simple: fall and collect. If it goes off bottom, it's lost.

Power-up effects:
- Wide paddle: paddle.w = baseW * 1.6 for 10 seconds; show a timer bar above paddle or a visual glow.
- Multi-ball: spawn 2 extra balls with slight angle variation.

Lives: 3. Lose a life when all balls are gone. Game over screen. Victory when all bricks destroyed.

Score: normal brick 50, tough brick 100? Let's do: normal 50, tough 75 per hit? Let's keep: normal brick = 50 points, tough brick = 100 points (destroyed). Plus combo multiplier maybe. Add a combo system: consecutive brick hits without paddle touch increase multiplier up to x5. That's a nice polish.

HUD: score, lives (balls icons), level, power-up timers. Draw in canvas or as HTML overlay? Let's do an HTML header above canvas for crisp text and easy responsive layout. Actually, mixing: I could draw everything on canvas for the game, plus HTML overlay for start/gameover screens. Let me do:

- Page: dark gradient background, a container with header (title + stats), canvas, and an overlay div for messages (start, game over, victory).

Stats in header: Score, Lives (displayed as ● ● ● or heart icons), plus in-canvas indicator for active power-ups.

Let me handle the canvas scaling: set canvas width/height attributes to logical size (e.g., 900 x 640) and CSS width to fit container with `width:100%; height:auto; max-width: 900px;`. Use devicePixelRatio for crispness? If I set canvas.width = 900*dpr and scale ctx, then CSS width:100% might scale weirdly. Simpler: set canvas.width=900*scale where scale = min(dpr,2) and ctx.setTransform(scale,0,0,scale,0,0), then style width:100% with aspect ratio. Actually with the transform approach, the drawing coordinates stay logical 900x640 and the backing store is larger → crisp. CSS: canvas { width:100%; height:auto; display:block; } — the intrinsic aspect ratio from width/height attributes is 900:640 scaled equally by dpr, so it works out.

Hmm but if width attribute = 900*2 = 1800 and height = 1280, the CSS `width:100%` scales the whole thing; aspect ratio preserved (1800/1280 = 900/640). Good.

Let me pick logical size 900 x 640. Play area: top wall at y=0, sides at x=0 and x=900, bottom is open (lose ball below 640). Paddle at y = 640 - 60 = 580? Let's define paddle y = 590, height 16, ball radius 8.

Brick layout: 8 rows x 14 cols? Let's do 8 rows, 13 columns. Area from x=40 to x=860 → width 820; 13 cols with gap 6 → brick width = (820 - 12*6)/13 = (820-72)/13 = 748/13 = 57.5. Good. Rows: start y=70, brick height 26, gap 8 → 8 rows = 8*34 = 272, ends at y=342. Plenty of space.

Brick types: normal (1 hp) and tough (2 hp) — mix. Maybe 3 tough rows. Colors by row gradient.

Let's define row types:
row 0: tough, tough, tough, ... hmm. Let's make a pattern:

```
const ROWS = 8, COLS = 13;
for r in rows:
  for c in cols:
    let hp = 1;
    if (r === 0 || r === 1) hp = 2;  // top two rows tougher
    else if ((r + c) % 5 === 0) hp = 2; // scattered
```
Plus some bricks with power-ups: choose a handful of random bricks (say 6) to have icons, type wide or multi.

Let's do: powerup bricks selected deterministically with a pseudo-random generator so the layout is interesting but stable. Use a seeded RNG (mulberry32) for reproducibility.

Power-up bricks should probably be normal hp (1 hit) for easier collection, or could be tough. Let's make them 1-hit normal bricks highlighted with a glowing border and a distinct color (gold).

Colors: rows go from a hue gradient: e.g., row colors: ['#ff5f6d', '#ff8a3d', '#ffd166', '#7bd88f', '#4cc9f0', '#4895ef', '#9b5de5', '#f15bb5'] — nice rainbow.

Power-up brick color: '#f9c74f' with icon.

Draw the icon inside the brick: 
- Wide: two chevrons pointing outward with a horizontal line: draw ‹—› shape. I'll draw arrows: line from x-8 to x+8 at center, arrowheads at both ends.
- Multi: three small circles arranged horizontally.

Ball trail: keep last N positions with fading alpha.

Particles: on brick break, spawn ~10 particles with random velocities, gravity, fade out. Also a small screen shake maybe (subtle).

Ball speed: starts at 320 px/s, increases slightly over time (cap ~560). Paddle bounce: set angle based on hit position.

Paddle bounce physics:
- offset = (ball.x - paddle.centerX) / (paddle.w/2) ∈ [-1, 1]
- angle = offset * MAX_ANGLE (e.g., 60° = 1.047 rad) from vertical.
- vx = speed * sin(angle), vy = -speed * cos(angle).
- Also ensure vy is never ~0; with max 60° it's fine. Also add minimum vertical component.

For the ball direction when it hits a brick, reflect and keep speed constant.

Prevent the ball from getting stuck in a horizontal loop: ensure |vy| >= some minimum (e.g., 0.25 * speed). After each bounce, if |vy| < min, adjust.

Also a "serve" state: ball starts resting on paddle; press space/click to launch. But the requirement says games must start in a self-playing demo mode behind a "Click or press a key to start" message. So: the game shows an overlay "Click or press a key to start", and behind it the game is... hmm, "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."

So I should have an AI/demo mode that plays automatically. Approach: Start the game immediately with an AI controlling the paddle (auto-tracking the lowest descending ball) and the ball auto-launching. Show a banner "Click or press a key to start" — but if the AI plays, then clicking would take over control. Hmm. Let's think about what the grader wants: The screenshot at 3 seconds should show the game in action (bricks, ball moving, particles), with a clear message like "Click or press a key to start" or "Demo mode — press any key to play".

Design: The game starts in DEMO mode: AI moves the paddle toward the ball, ball launches automatically, and a message overlays "DEMO — Click or press a key to play yourself" that fades after a few seconds. Then on any key/click, control switches to the player (and the message changes to "You have control" or fades). The game keeps running (lives etc.) — but should the demo consume lives? Yes, demo just plays; if demo loses a ball, the AI respawns and continues. Actually to keep it simple and fair: In demo mode, the AI is nearly perfect so it rarely loses.

But the overlay message must be visible at first paint (screenshot at 3s). So make the banner persistent for the first ~5 seconds or until interaction. I'll display a prominent centered panel: "BREAKOUT" title, subtitle "Click or press any key to take control — Demo AI is playing now". Then it fades out over 1s after interaction or after 6 seconds.

Let me have the overlay show "DEMO MODE" badge at the top of canvas area persistently while demo is active, plus the big start panel initially.

Plan:
- On load: state = 'demo'. Overlay panel visible with title "BREAKOUT", text "Demo AI is playing. Click or press any key to take control." Panel fades away ~ after 5.5s automatically or on interaction. A small "DEMO" pill remains in the top-right corner of the canvas while in demo mode.
- On click/keydown: state = 'playing', control = player. Small "DEMO" pill disappears. Show brief toast "You have control!".
- Arrow keys move paddle; mouse moves paddle (mouse control enabled only after click? mouse move over canvas moves paddle anyway — but in demo mode mouse movement shouldn't steal control... Actually let's allow mouse to take control on mousemove too? The requirement says the overlay "Click or press a key to start". Let's make mousemove over canvas also switch to player control, but then any accidental mouse move... in a sandbox iframe the mouse might be at 0,0 and not move. I'll enable mouse control on mousemove (that's natural). Hmm but the grader might move the mouse to test. Fine.

Actually careful: if mouse control takes over on mousemove, then during demo a stray mousemove would break demo. That's acceptable — it's what "click or press a key to start" implies. I'll switch on mousemove only if it's a real movement (movementX != 0) — fine.

Lives: In demo mode, when a ball is lost, the AI respawns and continues without losing lives? Hmm — but then score/lives never decrease. For the demo, better to keep lives decrementing realistically but the AI is good so it rarely happens. I'll decrement lives normally in both modes. Actually if the demo AI loses all 3 lives quickly, we'd hit game over screen at 3s which is bad. The AI should be good: track the target ball with high precision and a bit of predictive lead. With ball speed 320 and paddle width 110, it should be nearly perfect. I'll implement the AI to track the ball with the predicted landing x, moving at high speed (600 px/s) toward the target — it will basically never miss.

Also handle the case where the ball is stuck horizontally: AI should handle. Fine.

Game over screen: overlay with "GAME OVER", final score, "Press R or click to restart". Victory: "VICTORY!" with score and "Play again".

Now, the requirement "3 lives, score displayed. Game over and victory screens with a restart option." Fine.

Let's write the code.

Detailed structure:

HTML:
```html
<div class="wrap">
  <header>
    <div class="brand">BREAKOUT</div>
    <div class="stats">
      <div class="stat"><span class="label">SCORE</span><span class="value" id="score">0</span></div>
      <div class="stat"><span class="label">LIVES</span><span class="value lives" id="lives"></span></div>
      <div class="stat"><span class="label">BRICKS</span><span class="value" id="bricksLeft">0</span></div>
    </div>
  </header>
  <div class="stage">
    <canvas id="game" width="900" height="640"></canvas>
    <div class="overlay" id="overlay"> ... </div>
  </div>
  <footer>hint text: Move with mouse or ← → keys · Click/tap to serve</footer>
</div>
```

Overlay content will be updated by JS (innerHTML) for start/gameover/victory.

CSS: dark theme with neon accents, glow, rounded corners, subtle grid background. Responsive: at 375px, the header wraps, canvas scales down.

Canvas styling: border-radius, box-shadow, border.

Now the game code.

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

DPR scaling:
```js
function resize(){
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = W * dpr; canvas.height = H * dpr;
  ctx.setTransform(dpr,0,0,dpr,0,0);
}
```
But then CSS width:100% of container up to max 900px. Since the backing store is 900*dpr and CSS width might be e.g. 860px, that's fine.

Hmm, careful: ctx.setTransform with dpr then drawing in logical coords. But I also do ctx.save/restore around drawing — fine.

Alternatively, don't use dpr; just canvas width=900 height=640 and CSS scale — slightly blurry on hi-dpi. Use dpr approach.

Game state variables:
```js
let state = 'demo'; // 'demo' | 'play' | 'over' | 'win'
let score, lives, bricks, balls, particles, powerups, paddle, keys, shake
```

Brick creation:
```js
function buildLevel(){
  bricks = [];
  const rows = 8, cols = 13;
  const margin = 40, gap = 6;
  const bw = (W - margin*2 - gap*(cols-1)) / cols;
  const bh = 26, top = 64, vgap = 8;
  ...
}
```
Width: (900-80-72)/13 = 748/13 = 57.538... fine, use float.

Brick object: {x,y,w,h,hp,maxHp,row,col,color,power:null|'wide'|'multi',cracks:null,alive:true,flash:0}

Power-up assignment: use rng to pick ~7 bricks among rows 3..6 (not too close to top) and set power.

Let me write the seeded RNG:
```js
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return ((t^t>>>14)>>>0)/4294967296;}}
```

Level pattern: make top 2 rows all tough (2 hp), and some tough bricks in lower rows.

Let's define per-row hp:
```
row 0: all tough
row 1: all tough
row 2: tough at c%4===0? 
row 3: normal, with some tough at (c%5===2)
row 4: normal
row 5: some tough
row 6: normal
row 7: normal
```

Colors per row: array of 8 colors.

Power-ups: choose bricks in rows 3-6 that are normal hp, pick 6 of them: 3 wide, 3 multi. Mark with power and gold-ish color? Keep row color but add glow border and icon. Maybe give power bricks a distinct look: brighter fill with a gold border.

Collision and physics per frame:

```js
function update(dt){
  // paddle
  if (mode === 'demo') aiPaddle(dt); else playerPaddle(dt);
  // balls
  for each ball: move with substeps
  // powerups fall
  // particles
}
```

Ball movement with substeps:
```js
function stepBall(b, dt){
  const speed = Math.hypot(b.vx,b.vy);
  const dist = speed*dt;
  const steps = Math.max(1, Math.ceil(dist/ (b.r*0.8)));
  const sdt = dt/steps;
  for(let i=0;i<steps;i++){
    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); }
    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); }
    // paddle
    if (b.vy>0 && b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h && b.x >= paddle.x - b.r && b.x <= paddle.x + paddle.w + b.r) {
       // resolve
       b.y = paddle.y - b.r;
       bounceOffPaddle(b);
       // also nudge x inside paddle
    }
    // bricks
    for(const br of bricks){ if(!br.alive) continue; if (collide(b,br)) break; }
  }
}
```

Careful: paddle collision check should only trigger when ball is above/at paddle top. Using the condition `b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h` plus x overlap. Since the ball moves downward, and I resolve by setting b.y = paddle.y - b.r and flipping vy. But the ball might come from below (vy<0) after passing the paddle... then it wouldn't trigger since vy>0 required. But if the ball enters the paddle region from the side while moving down, we handle it.

Potential issue: ball moving fast downward could pass entirely below the paddle within one substep. Substepping with step size <= r*0.8 = 6.4px prevents tunneling through the 16px paddle? Ball r=8, paddle h=16. Step 6.4 < 16 so ok, the ball's y will land within the paddle band in some substep (the band is 16 + 8 + 8 = effectively from paddle.y-8 to paddle.y+24; ball center crossing from above to below takes at least... The condition triggers when b.y + r >= paddle.y, i.e., b.y >= paddle.y - 8 = 582. And requires b.y - r <= paddle.y + h → b.y <= 604. So the "capture band" for the center is [582, 604], which is 22px tall; with steps of 6.4 px we always land inside. Good.

But also x overlap must hold. If the ball is moving diagonally and passes the paddle's x range during the step... With 6.4px steps it's fine.

Edge case: the ball hits the side of the paddle (approaching from the side). Our check would trigger if x within [paddle.x - r, paddle.x + w + r]. Then we set b.y = paddle.y - b.r which might teleport the ball up weirdly. To be more robust, handle: if ball's center y > paddle.y + paddle.h/2 → treat as side hit (reflect vx). I'll add that.

Bounce off paddle:
```js
function bounceOffPaddle(b){
  const rel = (b.x - (paddle.x + paddle.w/2)) / (paddle.w/2);
  const clamped = Math.max(-1, Math.min(1, rel));
  const maxAngle = 1.05; // 60deg
  const angle = clamped * maxAngle;
  const speed = Math.max(b.speed, baseSpeed) ; // maintain
  b.vx = speed * Math.sin(angle);
  b.vy = -speed * Math.cos(angle);
  b.speed = speed;
  // minimum vertical component
  if (Math.abs(b.vy) < speed*0.28) { b.vy = -Math.sign(b.vy||1)*speed*0.28; b.vx = Math.sign(b.vx)*Math.sqrt(Math.max(0,speed*speed - b.vy*b.vy)); }
  // combo reset
  combo = 1; comboTimer = 0;
}
```
Note: at the exact edges rel = ±1 → 60° from vertical → sharp angle. Center → straight up. 

Brick collision:
```js
function circleRect(b, r){
  const cx = Math.max(r.x, Math.min(b.x, r.x + r.w));
  const cy = Math.max(r.y, Math.min(b.y, r.y + r.h));
  let dx = b.x - cx, dy = b.y - cy;
  let dist = Math.hypot(dx,dy);
  if (dist > b.r) return null;
  if (dist === 0) {
    // center inside: push out along smallest overlap
    const overlapL = b.x - r.x, overlapR = r.x + r.w - b.x, overlapT = b.y - r.y, overlapB = r.y + r.h - b.y;
    const m = Math.min(overlapL, overlapR, overlapT, overlapB);
    if (m === overlapT) return {nx:0, ny:-1, depth: overlapT + b.r};
    if (m === overlapB) return {nx:0, ny:1, depth: overlapB + b.r};
    if (m === overlapL) return {nx:-1, ny:0, depth: overlapL + b.r};
    return {nx:1, ny:0, depth: overlapR + b.r};
  }
  return {nx: dx/dist, ny: dy/dist, depth: b.r - dist};
}
```
Response: reflect velocity about the normal: 
v' = v - 2(v·n)n. But for a brick face, we want to make sure the ball doesn't get a weird angle. Using the proper reflection is standard and fine.

Then: b.x += nx*depth; b.y += ny*depth; reflect.

Also ensure the reflected vy isn't ~0 (horizontal bounce leading to endless horizontal travel). Add: if |vy| < speed*0.2 after reflection, adjust. Actually a ball bouncing horizontally between walls forever is a classic issue. Let's enforce: after any reflection, if |b.vy| < b.speed*0.25, then set b.vy = ±b.speed*0.25 and recompute b.vx = ±sqrt(speed² - vy²) keeping the sign of vx. Hmm, but for a brick hit on the side, we want it to keep moving mostly horizontally? Standard breakout adds an angle limit. I'll enforce the minimum vy of 0.3*speed to avoid stuck horizontals.

Ball speed management: b.speed increases over time: base 330, +2 per brick broken maybe, capped at 560. Also in multi-ball, all balls share the same speed value (each ball has its own speed but keep them equal-ish).

Brick damage:
```js
br.hp--;
br.flash = 0.12;
if (br.hp <= 0) { br.alive = false; score += points*combo; spawn particles; maybe drop powerup; if (br.power) spawn falling powerup; }
else { br.cracked = true; generate cracks; score += 10; }
```

Cracks: generate an array of line segments on first hit: e.g., 3 polylines from random points. Store as array of [x1,y1,x2,y2] in local normalized coordinates (0..1) so they scale. Draw with dark stroke.

Particles: {x,y,vx,vy,life,maxLife,size,color}. Update with gravity 400, drag.

Power-up falling items: {x,y,type,w:26,h:26,vy:200,rot}. Draw as a rounded capsule with an icon; glow. When y > paddle.y and overlapping paddle x-range → collect. Also collect if it hits the paddle's top region: check `pu.y + pu.h/2 >= paddle.y && pu.y - pu.h/2 <= paddle.y + paddle.h && pu.x >= paddle.x && pu.x <= paddle.x+paddle.w`.

Effects:
- 'wide': paddle.w = baseW*1.65, wideTimer = 10 (seconds). Show countdown bar.
- 'multi': spawn 2 more balls from the current ball positions with angles ±25° from the current velocity; cap balls at, say, 6. If already 3+ balls, maybe just add score instead. The requirement says "multi-ball (3 balls)" — so catching it gives 3 balls. I'll implement: ensure at least 3 balls: if balls.length < 3, add until 3; else add up to max 6. And show a toast "MULTI-BALL!".

Paddle width base 120, wide 200.

Timers: wideTimer decrements; when it reaches 0, paddle shrinks back. Use performance.now based dt.

Lives: lives = 3. When all balls lost → lives--; if lives <= 0 → game over; else respawn ball on paddle (state 'serve' with auto-launch after a short delay in demo, or on click in play mode). In play mode, waiting for a click to serve is standard but might make the game look stalled. Let's auto-launch after 1.2 s in play mode too, or on click/space immediately. I'll auto-launch after 1s and also allow click/space to launch instantly.

In demo mode, the AI serves automatically.

AI paddle: target = the ball that is descending toward the paddle, with a predicted landing x. Compute prediction: simulate the ball's path with wall reflections until y >= paddle.y. Simple approach: for each descending ball, predict x at paddle line by iterating the ray with reflections (ignoring bricks).

```js
function predictLanding(b){
  let x = b.x, y = b.y, vx = b.vx, vy = b.vy;
  const targetY = paddle.y;
  if (vy <= 0) return null;
  // step
  let t = (targetY - y)/vy;
  let dist = vx*t;
  // reflect horizontally
  // x position = x + dist, folded into [0,W]
  let nx = x + dist;
  // fold
  if (nx < 0) nx = -nx;
  if (nx > W) nx = 2*W - nx;
  if (nx < 0 || nx > W) nx = ((x + dist) % (2*W) + 2*W) % (2*W); if (nx > W) nx = 2*W - nx;
  return nx;
}
```
Since only vertical walls reflect, folding is correct.

AI moves paddle center toward target with max speed 520 px/s. Add slight imperfection? No — keep it good.

Also the AI should move the paddle under the ball when the ball is stuck bouncing horizontally. If no descending ball, drift toward the center or toward the ball's x.

Since the AI is in demo mode, the demo banner persists. Also when the player takes over, the AI stops.

Now the visuals:

Canvas background: dark navy gradient with subtle stars/grid, drawn once to an offscreen canvas for performance? Let's just draw a gradient + a faint grid each frame — cheap enough at 900x640. Actually a gradient fill each frame is fine.

Draw order: background, bricks (with cracks, icons, glow), powerups falling, paddle (glow, gradient), balls (with trail), particles, HUD overlays in canvas (power-up timer, combo indicator), then the HTML overlay panel.

Screen shake: on brick break, shake = 3; decay.

Let's also add a subtle vignette.

Combo: consecutive brick hits without touching the paddle → combo increments up to 5, shown as "x2" near the score, decays after 2s of no brick hits.

Now the HTML overlay:

```html
<div class="overlay" id="overlay">
  <div class="panel" id="panel">
    <h1>BREAKOUT</h1>
    <p class="sub">...</p>
    <p class="hint">Click or press a key to start</p>
  </div>
</div>
```

I'll set the overlay content dynamically:
- Initial (demo): title "BREAKOUT", badge "DEMO MODE", text "The AI is playing. Click or press any key to take control.", hint.
- Game over: "GAME OVER", score, "Press R or click to restart".
- Victory: "VICTORY!", score, "Press R or click to restart".

The overlay is clickable to restart in over/win states (add a click handler on the stage).

Restart: R key or click on the overlay in the game-over/win state.

Also "Press Space to serve" — in play mode when the ball is on the paddle.

Let me also handle the touch: pointermove for mouse control (works for touch drag too).

Implementation of the input:
```js
window.addEventListener('keydown', e => {
  if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { keys[e.code]=true; e.preventDefault(); }
  if (e.key === 'r' || e.key === 'R') restart();
  if (state === 'demo') takeControl();
  if (state==='over'||state==='win') restart();
  if (e.code==='Space') serve();
});
window.addEventListener('keyup', ...);
canvas.addEventListener('pointermove', e => { mouseX = ...; if(mode==='demo') takeControl(); });
```
Mouse position mapping: use getBoundingClientRect and scale: mx = (e.clientX - rect.left) * (W / rect.width).

Since the demo message says "Click or press a key to start", pressing any key takes control.

Now, one issue: in the demo, the ball should launch automatically after ~0.8 s from load so the screenshot at 3s shows action. Yes: initial serve with a 0.9 s delay.

Let's write the render of the bricks:

```js
function drawBrick(br){
  const {x,y,w,h} = br;
  ctx.save();
  // shadow / base
  roundRect(x, y, w, h, 5);
  const g = ctx.createLinearGradient(0,y,0,y+h);
  g.addColorStop(0, lighten(br.color, 0.25));
  g.addColorStop(1, br.color);
  ctx.fillStyle = g; ctx.fill();
  // top highlight
  ctx.fillStyle = 'rgba(255,255,255,0.18)';
  roundRect(x+3, y+3, w-6, 3, 2); ctx.fill();
  if (br.maxHp===2) {
     // tough brick: metallic border
     ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth=2; roundRect(x+1,y+1,w-2,h-2,5); ctx.stroke();
  }
  if (br.power) { gold border + glow + icon }
  if (br.cracked) { draw crack lines }
  if (br.flash>0) { white overlay alpha }
  ctx.restore();
}
```

Helper color manipulation: implement lighten/darken via hex parsing.

roundRect: use ctx.roundRect if available (Chrome supports it since v99). Safer to implement manually:
```js
function roundRect(ctx,x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }
```
I'll implement my own to be safe.

Crack drawing: normalized segments; draw with rgba(0,0,0,0.45) lineWidth 1.5 and a light highlight line offset.

Tough bricks also should show a "2-hit" indicator: maybe small dots or a border. After the first hit, cracks appear and the color darkens. Let me also darken the fill when cracked (draw a dark overlay with 'multiply' or just a semi-transparent black fill).

Power-up brick icons: draw a small rounded capsule in the middle with the icon.

Falling powerup rendering: a glowing rounded square (26x26) with the icon and a soft shadow, rotating slightly.

Paddle rendering: rounded rect with gradient (cyan→blue), glow shadow, plus when wide, a golden tint and a timer bar below/above.

Ball: white circle with a glow (shadowBlur) and a trail of fading circles.

Particles: small squares/circles with color from the brick.

HUD in canvas: top-left? The HTML header already has score/lives. In-canvas: show active power-up timers near the bottom-left, e.g. a "WIDE 7.3s" bar. And a combo indicator near the top center.

Let me also draw the walls as neon lines.

OK, let me now write out the full code.

Additional details: the header stats should update via textContent.

Lives display: use three small ball dots: "●●●" styled with spans; lost lives dimmed.

Bricks left counter.

Let me write the code now.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breakout</title>
<style>
  :root{
    --bg:#070b16;
    --panel:#0f1629;
    --accent:#22d3ee;
    --accent2:#a855f7;
    --text:#e6edf7;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;
    background:
      radial-gradient(1200px 700px at 20% -10%, #17204a 0%, transparent 60%),
      radial-gradient(900px 600px at 100% 0%, #2a1042 0%, transparent 55%),
      var(--bg);
    color:var(--text);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    display:flex; flex-direction:column; align-items:center;
    padding: 14px 12px 22px;
    min-height:100%;
  }
  .wrap{width:100%; max-width:960px; display:flex; flex-direction:column; gap:10px;}
  header{display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;}
  .brand{font-weight:800; letter-spacing:.28em; font-size:clamp(14px,2.6vw,20px); background:linear-gradient(90deg,#22d3ee,#a855f7); -webkit-background-clip:text; background-clip:text; color:transparent;}
  .stats{display:flex; gap:8px; flex-wrap:wrap;}
  .stat{background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08); border-radius:12px; padding:6px 12px; min-width:76px; text-align:center; backdrop-filter: blur(6px);}
  .label{display:block; font-size:9px; letter-spacing:.18em; color:#8ea0c4;}
  .value{display:block; font-size:18px; font-weight:700; font-variant-numeric: tabular-nums;}
  .lives{display:flex; gap:5px; justify-content:center; align-items:center; height:24px}
  .dot{width:11px;height:11px;border-radius:50%;background:radial-gradient(circle at 30% 30%, #fff, #22d3ee); box-shadow:0 0 8px rgba(34,211,238,.8)}
  .dot.off{background:#2a3350; box-shadow:none;}
  .stage{position:relative; width:100%; border-radius:16px; overflow:hidden; border:1px solid rgba(255,255,255,.1); box-shadow: 0 20px 60px rgba(0,0,0,.6), 0 0 0 1px rgba(34,211,238,.06) inset; background:#05070f;}
  canvas{display:block; width:100%; height:auto; touch-action:none; cursor:none;}
  .overlay{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(5,8,18,.72); backdrop-filter: blur(3px); transition: opacity .35s ease; }
  .overlay.hidden{opacity:0; pointer-events:none;}
  .panel{text-align:center; padding:26px 30px; border-radius:18px; background:linear-gradient(160deg, rgba(23,32,66,.92), rgba(10,14,30,.92)); border:1px solid rgba(255,255,255,.12); box-shadow:0 18px 50px rgba(0,0,0,.55); max-width:min(90%,460px);}
  .badge{display:inline-block; font-size:10px; letter-spacing:.22em; padding:5px 10px; border-radius:999px; background:linear-gradient(90deg,#22d3ee,#a855f7); color:#05101c; font-weight:800; margin-bottom:10px;}
  .panel h1{margin:6px 0 6px; font-size:clamp(26px,6vw,44px); letter-spacing:.06em;}
  .panel p{margin:6px 0; color:#b9c6e2; font-size:clamp(12px,2.4vw,15px); line-height:1.5}
  .panel .cta{margin-top:14px; font-weight:700; color:#fff; font-size:clamp(12px,2.6vw,15px)}
  .panel .cta span{display:inline-block; border:1px solid rgba(255,255,255,.35); border-radius:8px; padding:4px 10px; margin:0 3px; background:rgba(255,255,255,.06)}
  .hintbar{display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; color:#7f8db0; font-size:11.5px; letter-spacing:.04em;}
  kbd{background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.14); border-bottom-width:2px; border-radius:5px; padding:1px 6px; font-size:10.5px; font-family:inherit;}
  .toast{position:absolute; left:50%; top:14px; transform:translateX(-50%); padding:8px 16px; border-radius:999px; font-weight:700; font-size:13px; letter-spacing:.08em; background:rgba(34,211,238,.14); border:1px solid rgba(34,211,238,.45); color:#c8f7ff; opacity:0; transition:opacity .25s, transform .25s; pointer-events:none;}
  .toast.show{opacity:1; transform:translateX(-50%) translateY(4px);}
  @media (max-width:520px){ .stat{min-width:64px; padding:5px 9px} .value{font-size:16px} }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="brand">BREAKOUT</div>
    <div class="stats">
      <div class="stat"><span class="label">SCORE</span><span class="value" id="score">0</span></div>
      <div class="stat"><span class="label">LIVES</span><span class="value lives" id="lives"></span></div>
      <div class="stat"><span class="label">BRICKS</span><span class="value" id="left">0</span></div>
      <div class="stat"><span class="label">COMBO</span><span class="value" id="combo">x1</span></div>
    </div>
  </header>
  <div class="stage" id="stage">
    <canvas id="game"></canvas>
    <div class="toast" id="toast"></div>
    <div class="overlay" id="overlay"><div class="panel" id="panel"></div></div>
  </div>
  <div class="hintbar">
    <div>Move: <kbd>←</kbd> <kbd>→</kbd> or mouse &nbsp;•&nbsp; Serve: <kbd>Space</kbd> / click &nbsp;•&nbsp; Restart: <kbd>R</kbd></div>
    <div id="modeTag">DEMO AI — press any key to take control</div>
  </div>
</div>
<script> ... </script>
</body>
</html>
```

Now the JS. Let me write it fully.

```js
(() => {
'use strict';
const W = 900, H = 640;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d', {alpha:false});
const overlay = document.getElementById('overlay');
const panel = document.getElementById('panel');
const elScore = document.getElementById('score');
const elLives = document.getElementById('lives');
const elLeft = document.getElementById('left');
const elCombo = document.getElementById('combo');
const elToast = document.getElementById('toast');
const elMode = document.getElementById('modeTag');

const COLORS = ['#ff4d6d','#ff7b54','#ffc247','#7ee787','#38bdf8','#4f7cff','#a855f7','#f472b6'];
const BASE_SPEED = 330;
const MAX_SPEED = 580;
const BALL_R = 8;
const PADDLE_W = 118, PADDLE_H = 15;
const PADDLE_Y = H - 52; // 588
```

Wait: paddle at H-52 = 588, height 15 → bottom at 603, 37px below for the ball to fall. Ball lost when y - r > H.

Let me set PADDLE_Y = H - 58 = 582. The ball is lost if b.y - BALL_R > H.

Game variables:
```js
let mode='demo';   // demo | player
let state='serve'; // serve | play | over | win
let score=0, lives=3, combo=1, comboTimer=0;
let balls=[], bricks=[], particles=[], drops=[], floaters=[];
let paddle = {x: W/2 - PADDLE_W/2, w: PADDLE_W, targetX: W/2};
let wideTimer=0, serveTimer=0, shake=0, time=0, demoBannerTimer=0;
let keys={left:false,right:false};
let mouseX=null, mouseActive=false;
```

Functions:

buildLevel, resetGame, startLevel, etc.

Let me carefully write the update loop with dt clamped to 1/30.

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

Update:
```js
function update(dt){
  time += dt;
  if (wideTimer>0){ wideTimer -= dt; if(wideTimer<=0){ wideTimer=0; setPaddleWidth(PADDLE_W); } }
  if (comboTimer>0){ comboTimer-=dt; if(comboTimer<=0) combo=1; }
  shake = Math.max(0, shake - dt*22);
  updatePaddle(dt);
  if (state==='serve'){
    serveTimer -= dt;
    // ball rides paddle
    if (balls.length) { const b = balls[0]; b.x = paddle.x + paddle.w/2; b.y = PADDLE_Y - BALL_R - 1; }
    if (serveTimer <= 0) launch();
  } else if (state==='play'){
    for (const b of balls) stepBall(b, dt);
    balls = balls.filter(b => !(b.y - BALL_R > H));
    if (balls.length===0) loseLife();
  }
  updateDrops(dt);
  updateParticles(dt);
  updateFloaters(dt);
  // powerup timers display
}
```

updatePaddle:
```js
function updatePaddle(dt){
  const target = PADDLE_W;
  let desired = paddle.x;
  if (mode==='demo'){
    // AI
    const t = aiTarget();
    desired = t - paddle.w/2;
    const diff = desired - paddle.x;
    const maxMove = 560*dt;
    paddle.x += Math.max(-maxMove, Math.min(maxMove, diff));
  } else {
    if (keys.left) desired -= 640*dt;
    if (keys.right) desired += 640*dt;
    if (mouseActive){
      desired = mouseX - paddle.w/2;
      // smooth follow
      paddle.x += (desired - paddle.x) * Math.min(1, dt*22);
    } else {
      paddle.x += desired - paddle.x; // where desired already includes the delta
    }
  }
  paddle.x = clamp(paddle.x, 0, W - paddle.w);
}
```
Careful with mixing delta-based and absolute-based. Let me restructure: compute `targetX` (absolute left position) each frame:

```js
if (mode==='demo'){ targetX = aiTarget() - paddle.w/2; move with max speed }
else {
  if (keys.left||keys.right){ const d = (keys.right?1:0)-(keys.left?1:0); targetX = paddle.x + d*640*dt; mouseActive=false; }
  else if (mouseActive){ targetX = mouseX - paddle.w/2; }
  else targetX = paddle.x;
  paddle.x += (targetX - paddle.x) * Math.min(1, dt*25);  // smoothing
}
```
For keyboard, smoothing with factor dt*25 is fine (fast enough). Actually let's apply direct movement for keyboard (immediate) to feel snappy: if keys pressed, paddle.x += d*700*dt (no smoothing). Else if mouse, smooth-follow.

Let me write:
```js
function updatePaddle(dt){
  let target;
  if (mode==='demo'){
    target = aiTarget() - paddle.w/2;
    const diff = target - paddle.x;
    const maxStep = 620*dt;
    paddle.x += Math.max(-maxStep, Math.min(maxStep, diff));
  } else {
    const dir = (keys.right?1:0) - (keys.left?1:0);
    if (dir !== 0){ paddle.x += dir*720*dt; mouseActive=false; }
    else if (mouseActive){ target = mouseX - paddle.w/2; paddle.x += (target-paddle.x)*Math.min(1, dt*26); }
  }
  paddle.x = clamp(paddle.x, 4, W-paddle.w-4);
}
```

aiTarget(): find the ball most likely to reach the paddle: among balls with vy>0, compute the predicted landing x and the time; choose the one with the smallest time. If none, aim at the ball's x (or center).

```js
function aiTarget(){
  if (!balls.length) return W/2;
  let best=null, bestT=Infinity;
  for(const b of balls){
    if (b.vy<=0) continue;
    const t = (PADDLE_Y - b.y)/b.vy;
    if (t>0 && t<bestT){ bestT=t; best=b; }
  }
  if(best){
    let nx = best.x + best.vx*bestT;
    // fold into [0,W]
    const period = 2*W;
    nx = ((nx % period)+period)%period;
    if (nx>W) nx = period-nx;
    return nx;
  }
  // no descending ball: follow the lowest ball
  let low = balls[0]; for(const b of balls) if (b.y>low.y) low=b;
  return low.x;
}
```
Hmm but the prediction ignores brick collisions, which is what we want for a decent AI.

Note: the AI target should be clamped so the paddle doesn't run off; the paddle clamp handles it.

launch(): set the ball's velocity at a random-ish angle (avoid too vertical): angle = (Math.random()*0.9-0.45) rad from vertical; vx = speed*sin, vy=-speed*cos.

loseLife(): lives--; update HUD; if lives<=0 → gameOver() else state='serve', serveTimer=1.0, reset combo, and keep a single ball on the paddle.

Actually in demo mode we might want to continue; but losing lives in demo is fine (rare).

stepBall(b,dt): substeps as described.

Collision with bricks: iterate over all bricks (8*13=104) each substep — fine.

Important: after a brick is hit and destroyed, we should break out of the substep loop to avoid multiple hits in one frame? It's fine to continue but let's break out of the substep loop after a brick collision to give a cleaner response (and avoid double-processing the same brick). Actually we should continue moving the ball in the remaining substeps to avoid it being stuck inside. Simpler: after a brick hit, break out of the substeps for this ball this frame (the remaining distance is tiny). But the ball might still be overlapping the brick → next frame it'll resolve. Let's just resolve the position (push out) so it isn't overlapping.

Let's make the brick collision function handle the response, including pushing the ball out.

Additionally, to make sure the ball doesn't get stuck between two bricks, the push-out handles it.

Let me write the brick collision:

```js
function hitBrick(br, b){
  br.hp--;
  br.flash = 0.18;
  combo = Math.min(5, combo+1); comboTimer = 2.2;
  if (br.hp<=0){
    br.alive=false;
    const pts = (br.maxHp===2?120:50)*combo;
    addScore(pts);
    spawnParticles(br);
    if (br.power) spawnDrop(br);
    shake = Math.min(7, shake+ (br.maxHp===2?4:2.4));
    floatText(br.x+br.w/2, br.y, '+'+pts, br.color);
    remaining--;
    if (remaining<=0){ win(); }
  } else {
    addScore(25*combo);  // hmm, partial
    if (!br.cracks) br.cracks = makeCracks(br);
    spawnParticles(br, 5);
    floatText(br.x+br.w/2, br.y, 'crack!', '#ffd166');
  }
}
```

Hmm, score for the first hit on a tough brick: let's give 20 points, and 120 for destroying a tough brick, 50 for a normal one, multiplied by combo.

Combo display: "x2" etc.

spawnDrop(br): create a falling power-up at the brick's center:
```js
drops.push({x:br.x+br.w/2, y:br.y+br.h/2, vy:180, type:br.power, r:13, rot:0, t:0});
```

updateDrops(dt): move down; rotate; check paddle collision → apply effect; if y > H+30 remove.

applyPower(type):
- 'wide': wideTimer = 10; setPaddleWidth(PADDLE_W*1.7); toast('WIDE PADDLE — 10s'); flash.
- 'multi': ensure 3 balls: while(balls.length<3) spawn a new ball from the paddle; also cap at 6. toast('MULTI-BALL x3'); Actually "multi-ball (3 balls)" — after collecting, there should be 3 balls. If you already have 3, maybe spawn up to 5. I'll implement: target = max(3, balls.length+1) capped at 6? Hmm, "gives ... multi-ball (3 balls)" means catching it results in 3 balls. Let me do: if balls.length < 3 → spawn to 3; else spawn 1 extra (max 6). Show toast 'MULTI-BALL ×3' or 'MULTI-BALL +1'.

Spawn new ball: from the paddle position, at an angle spread around -60°..-120°: angle offset from vertical ±(0.35..0.8) rad.

Set the speed of new balls to match the current max speed.

Particles: 
```js
function spawnParticles(br, count){
  const n = count || 12;
  for(let i=0;i<n;i++){
    const a = Math.random()*Math.PI*2, sp = 60+Math.random()*220;
    particles.push({x: br.x+br.w/2+(Math.random()-0.5)*br.w, y: br.y+br.h/2+(Math.random()-0.5)*br.h, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-60, size:2+Math.random()*3.5, life:0.5+Math.random()*0.5, max:1, color: Math.random()<0.3? '#ffffff' : br.color});
  }
}
```
Update: vy += 500*dt; life -= dt; alpha = life/max.

Floaters (score text): {x,y,text,color,life}.

Rendering:

Background: 
```js
function drawBackground(){
  const g = ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#0b1226'); g.addColorStop(0.55,'#080c1a'); g.addColorStop(1,'#05070f');
  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+=45){ctx.beginPath();ctx.moveTo(x+.5,0);ctx.lineTo(x+.5,H);ctx.stroke();}
  for(let y=0;y<H;y+=45){...}
  // vignette
}
```
The grid lines drawn every frame × ~35 lines is fine.

Maybe pre-render the background into an offscreen canvas for performance. Let's do it: create a bg canvas 900x640 and draw the gradient+grid once; then drawImage each frame. That's efficient and simple. But with dpr scaling, the bg canvas should be at dpr resolution too. I'll create it in resize().

Let's keep it simple and just draw it directly; it's fine (a few dozen lines per frame).

Wall glow: draw a subtle border line inside the canvas edges in cyan.

Balls with trails: store trail array per ball: push {x,y} each frame, keep up to 10, draw with decreasing alpha.

Let me write the render:

```js
function render(){
  ctx.save();
  // shake
  if(shake>0){ ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake); }
  drawBackground();
  drawBricks();
  drawDrops();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawFloaters();
  drawHudOverlay();
  ctx.restore();
}
```

drawHudOverlay: power-up timers at the bottom-left, the combo at the top-center, "DEMO" badge in the top-right.

Now the overlay panel content and state transitions.

State machine:
- 'serve': ball on paddle, waiting.
- 'play'
- 'over': game over overlay.
- 'win': victory overlay.

Initial: mode='demo', state='serve' with serveTimer=1.0, and the overlay panel shows the start message. The overlay is pointer-events:none after fading? We want clicking to take control, so keep pointer-events auto on the overlay but also allow the stage to receive clicks. The overlay covers the canvas; clicking the overlay should take control. I'll add a click listener on the overlay and the stage: `stage.addEventListener('pointerdown', onInteract)`.

When the user interacts: mode='player'; hide the overlay (add 'hidden' class); show a toast "You have control!"; the mouse becomes the control (mouseActive = true).

But careful: clicking on the overlay in the over/win state should restart. I'll handle: if state==='over'||state==='win' → restart(); else takeControl().

Also the keydown handler: any key → if over/win → restart(); else if mode==='demo' → takeControl(). But 'r' should restart in any state.

Also need: if the state is 'serve' and the player presses space → launch immediately.

Let's write showPanel(html) and hidePanel().

Initial panel:
```html
<div class="badge">DEMO MODE</div>
<h1>BREAKOUT</h1>
<p>The AI is playing right now.</p>
<p class="cta"><span>Click</span> or <span>press any key</span> to take control</p>
```

Hmm, the requirement says a clear "Click or press a key to start" message. I'll include exactly that phrase.

Game over panel:
```html
<div class="badge">GAME OVER</div>
<h1>OUT OF LIVES</h1>
<p>Final score: <b>1234</b></p>
<p class="cta"><span>Click</span> or press <span>R</span> to play again</p>
```

Victory:
```html
<div class="badge">VICTORY</div>
<h1>ALL BRICKS CLEARED</h1>
<p>Final score: <b>...</b></p>
<p class="cta"><span>Click</span> or press <span>R</span> to play again</p>
```

The overlay must be hidden initially? No — initially shown with the start message, then auto-hide after ~5.5 s even if the user doesn't interact (so the game is visible). Actually if the user never interacts, the demo continues and the overlay stays with the message. That's good: "Click or press a key to start" visible. But it covers the game (with a blur backdrop). At 3s the screenshot would show the overlay over the game. Hmm — the grader wants to see the game in action. A semi-transparent overlay with blur might obscure it. Let's make the initial overlay lighter: a centered panel with a subtle backdrop but the game visible around it. The background of the overlay is rgba(5,8,18,.72) — that's fairly dark, dimming the whole game. Let me reduce to .45 with a blur of 2px, and auto-fade the overlay after 4.5s even without interaction (the demo continues in the background). Also the "DEMO MODE" pill remains.

Better plan: auto-hide the big panel after 4.2 s (fade out), leaving a small "DEMO" badge in the canvas corner. The 3-second screenshot would show the panel still up. Hmm. Let me auto-hide after 2.8 s so that at 3 s it's mostly faded? Risky (mid-transition). 

Alternative: make the overlay non-blocking visually: the panel is a compact card centered, and the overlay background is a mild vignette (rgba(5,8,18,0.35)) with no blur, so the bricks are still clearly visible around and behind the card. The panel itself covers the middle of the play area where bricks are... the bricks are in the upper area (y 64..342) and the card is centered at y=320 which overlaps the lower rows.

Option: position the start panel at the bottom area (below the bricks), e.g., align the overlay items to flex-end with padding-bottom, so the card sits near the bottom over the empty region. That keeps the bricks and ball fully visible. 

Let's do: the overlay uses `align-items:flex-end; padding-bottom: 26px;` for the start panel, and for game over/victory keep centered (those are end states, fine).

Hmm, but with `flex-end`, the panel is at the bottom, overlapping the paddle area. In demo mode, the ball is around the bottom too. It'd cover the action. 

Alternative: put the start card in the upper-left region? Hmm.

Actually simplest and most attractive: keep the overlay centered but make it semi-transparent so the game shows through, and auto-fade it after 3.5 s. Combined with a compact card (max-width 420px, padding 18px 26px), most of the brick field stays visible around it. And the demo badge remains.

Alternatively, make the overlay only cover the top portion (like a banner) with the message "Click or press a key to start — the AI is demoing". A top banner doesn't hide the action. But the requirement mentions a "Click or press a key to start" message which is satisfied by a banner.

I think a good compromise: the overlay is a full-size flex container with `pointer-events:auto` but a very light background tint (rgba(4,7,16,.35)), and the panel is centered but auto-fades after 3.6s. The ball is in the middle of the field at 3s? The ball starts at the paddle (bottom) and bounces around. Fine.

Let me reduce the panel size a bit and add `background: rgba(6,10,22,.55)` with a blur of 4px only on the panel itself, not the whole overlay. Then the game around the card is quite visible.

Also I'll make the panel fade out at 3.5s automatically regardless of interaction (in demo mode). And when the player takes control, it fades immediately.

Actually, there's a subtlety: after the overlay hides, the "Click or press a key to start" message is gone, but the demo badge and the hint bar still say "press any key to take control". Good.

Let me now also handle: if the state is 'over' or 'win', the overlay shows and stays until restart.

Now the HUD lives display: build three dots; update classes.

Let me write the code for updating the HUD:
```js
function updateHud(){
  elScore.textContent = score.toLocaleString();
  elLeft.textContent = remaining;
  elCombo.textContent = 'x'+combo;
  if (elLives.children.length !== 3){ ... create 3 spans ... }
  for (let i=0;i<3;i++) elLives.children[i].classList.toggle('off', i>=lives);
}
```

Now let me write the final code carefully, making sure everything is consistent.

Some more polish ideas:
- Add a subtle "ball trail" using the trail positions.
- Add a glow to the paddle.
- Add a "wide paddle" indicator: the paddle glows gold and a small timer bar is drawn above the paddle.
- Add a combo popup text.
- Add screen flash on power-up.

Let me write the code.

```js
// ---------- helpers ----------
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
function rr(ctx,x,y,w,h,r){ r=Math.min(r,w/2,h/2); ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }
function hexToRgb(h){...}
function shade(hex, amt){ // amt -1..1
  const c = hexToRgb(hex);
  const f = amt<0?0:255, p=Math.abs(amt);
  return `rgb(${Math.round(c.r+(f-c.r)*p)},${Math.round(c.g+(f-c.g)*p)},${Math.round(c.b+(f-c.b)*p)})`;
}
```

Now the level builder:

```js
function buildLevel(){
  bricks=[]; remaining=0;
  const rows=8, cols=13, margin=42, gap=6;
  const bw=(W-margin*2-gap*(cols-1))/cols, bh=26, top=58, vgap=9;
  const rng=mulberry32(20240607);
  for(let r=0;r<rows;r++){
    for(let c=0;c<cols;c++){
      let hp=1;
      if(r===0||r===1) hp=2;
      else if(r===3 && c%5===3) hp=2;
      else if(r===5 && c%7===2) hp=2;
      else if(r===6 && c%6===5) hp=2;
      const br={x:margin+c*(bw+gap), y:top+r*(bh+vgap), w:bw, h:bh, hp, maxHp:hp, color:COLORS[r%COLORS.length], power:null, cracks:null, alive:true, flash:0};
      bricks.push(br);
    }
  }
  // power-up bricks
  const cands = bricks.filter(b=>b.maxHp===1 && b.y>140 && b.y<330);
  shuffle(cands, rng);
  const types=['wide','multi','wide','multi','wide','multi'];
  for(let i=0;i<6;i++){ const b=cands[i]; if(b) b.power=types[i]; }
  remaining=bricks.length;
}
```
Careful: cands must have at least 6 elements — yes, plenty.

Note the y range filter: rows 3..6 have y = 58 + r*35 → r=3 → 163, r=6 → 268. Rows 7 → 303 (excluded if >330? no, 303 < 330 so included). Fine, doesn't matter.

Wait, with 8 rows at vgap 9 and bh 26: total height = 8*26 + 7*9 = 208+63 = 271, from y=58 to 329. Good.

Power-up brick look: color = gold '#ffd166'? Let's give them a distinct color: '#f9c74f' with a bright border and icon. But then the row color variety is lost for those bricks — that's fine, it highlights them.

Actually let's keep the row color but add a golden border + glow + icon. Hmm, "Some bricks visibly carry a power-up icon" — visible icons are what matters. I'll make the power bricks a distinct gold/white color so they're clearly special:
- wide → color '#38bdf8'? no, that's a row color.

Let me use: power bricks keep their row color but get: gold border (2px), a soft golden glow (shadowBlur), and a white icon in the center with a dark rounded backing plate. That looks good and keeps the rainbow.

Cracks generation:
```js
function makeCracks(br){
  const segs=[];
  const cx=0.5, cy=0.5;
  for(let i=0;i<3;i++){
    let x=rng? Math.random():0, y=Math.random();
    const pts=[];
    let px=0.1+Math.random()*0.8, py=Math.random();
    pts.push([px,py]);
    const steps=3;
    for(let s=0;s<steps;s++){
      px=clamp(px+(Math.random()-0.5)*0.5,0,1);
      py=clamp(py+(Math.random()-0.5)*0.5,0,1);
      pts.push([px,py]);
    }
    segs.push(pts);
  }
  return segs;
}
```
Draw: for each polyline, ctx.beginPath, moveTo(pts[0][0]*w + x, pts[0][1]*h + y), lineTo..., stroke with 'rgba(0,0,0,0.5)' lineWidth 1.6, and then a second stroke offset by (1,1) with 'rgba(255,255,255,0.18)'.

Tough bricks that haven't been cracked should still look tough: add a metallic border and small "double" chevrons? I'll add a steel border + a subtle inner line. After cracking, also darken the fill with 'rgba(0,0,0,0.22)'.

Now the paddle drawing:
```js
function drawPaddle(){
  const p=paddle;
  const grad=ctx.createLinearGradient(p.x,0,p.x+p.w,0);
  if(wideTimer>0){ grad.addColorStop(0,'#fde68a'); grad.addColorStop(0.5,'#fbbf24'); grad.addColorStop(1,'#f59e0b'); }
  else { grad.addColorStop(0,'#22d3ee'); grad.addColorStop(0.5,'#60a5fa'); grad.addColorStop(1,'#a855f7'); }
  ctx.save();
  ctx.shadowColor= wideTimer>0? 'rgba(251,191,36,.8)':'rgba(56,189,248,.8)';
  ctx.shadowBlur=18;
  ctx.fillStyle=grad;
  rr(ctx,p.x,p.y,p.w,p.h,p.h/2); ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='rgba(255,255,255,.35)';
  rr(ctx,p.x+4,p.y+2,p.w-8,4,2); ctx.fill();
  if(wideTimer>0){
    // timer bar
    const bw=p.w, bx=p.x, by=p.y-10;
    ctx.fillStyle='rgba(0,0,0,.4)'; rr(ctx,bx,by,bw,5,2.5); ctx.fill();
    ctx.fillStyle='#fbbf24'; rr(ctx,bx,by,bw*clamp(wideTimer/10,0,1),5,2.5); ctx.fill();
  }
  ctx.restore();
}
```

Ball drawing with trail:
```js
function drawBalls(){
  for(const b of balls){
    for(let i=0;i<b.trail.length;i++){
      const t=b.trail[i], a=(i+1)/b.trail.length*0.35;
      ctx.beginPath(); ctx.fillStyle=`rgba(255,255,255,${a})`; ctx.arc(t.x,t.y,BALL_R*(0.3+0.7*(i/b.trail.length)),0,Math.PI*2); ctx.fill();
    }
    ctx.save();
    ctx.shadowColor='rgba(125,211,252,.9)'; ctx.shadowBlur=16;
    const g=ctx.createRadialGradient(b.x-2,b.y-3,1,b.x,b.y,BALL_R);
    g.addColorStop(0,'#ffffff'); g.addColorStop(1,'#38bdf8');
    ctx.fillStyle=g;
    ctx.beginPath(); ctx.arc(b.x,b.y,BALL_R,0,Math.PI*2); ctx.fill();
    ctx.restore();
  }
}
```

Drops drawing:
```js
function drawDrops(){
  for(const d of drops){
    ctx.save();
    ctx.translate(d.x,d.y); ctx.rotate(d.rot);
    ctx.shadowColor = d.type==='wide' ? 'rgba(34,211,238,.9)' : 'rgba(244,114,182,.9)';
    ctx.shadowBlur=14;
    ctx.fillStyle = d.type==='wide' ? 'rgba(14,116,144,.95)' : 'rgba(190,24,93,.95)';
    rr(ctx,-13,-13,26,26,7); ctx.fill();
    ctx.shadowBlur=0;
    ctx.strokeStyle='rgba(255,255,255,.75)'; ctx.lineWidth=1.6; rr(ctx,-13,-13,26,26,7); ctx.stroke();
    drawIcon(ctx, d.type, 0,0, 1);
    ctx.restore();
  }
}
```
drawIcon(ctx,type,x,y,scale): 
- wide: draw a horizontal double arrow: line from -7 to 7 at y, with arrowheads at both ends.
- multi: three dots at -6, 0, 6.

Implement:
```js
function drawIcon(ctx,type,cx,cy,s){
  ctx.save(); ctx.translate(cx,cy); ctx.scale(s,s);
  ctx.strokeStyle='#fff'; ctx.fillStyle='#fff'; ctx.lineWidth=1.8; ctx.lineCap='round'; ctx.lineJoin='round';
  if(type==='wide'){
    ctx.beginPath(); ctx.moveTo(-7,0); ctx.lineTo(7,0); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(-3,-4); ctx.lineTo(-7,0); ctx.lineTo(-3,4); ctx.closePath(); ctx.fill();
    ctx.beginPath(); ctx.moveTo(3,-4); ctx.lineTo(7,0); ctx.lineTo(3,4); ctx.closePath(); ctx.fill();
  } else {
    ctx.beginPath(); ctx.arc(-6,0,2.6,0,Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(0,0,2.6,0,Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(6,0,2.6,0,Math.PI*2); ctx.fill();
  }
  ctx.restore();
}
```

The brick power icon is drawn with the same function, maybe scaled 0.8.

Now the HUD overlay in canvas:
- Top center: combo "COMBO ×3" when combo>1.
- Bottom left: power-up status.
- Top right: "DEMO" badge when mode==='demo'.

Let's draw a rounded badge with the text "DEMO · AI PLAYING".

Also in the bottom-right corner maybe a hint "← → or mouse".

Now let me write the whole file.

Also: handle window resize for the dpr; on resize, set the canvas backing store.

One more: the "serve" state in player mode — after losing a life, show a message "Click or press Space to serve" in the canvas center. In demo mode, auto-serve after 1.1 s.

Let me write the code for `serveTimer` handling:
- state='serve': serveTimer counts down; when it hits 0 → launch. In player mode, show the hint; pressing space/click launches immediately.

Let me now write everything out. I'll be careful with the code.

One more consideration on the ball-brick collision response: The ball may hit the corner of a brick, producing an odd reflection. That's standard.

Also, the ball could get stuck in a horizontal groove between two brick rows? With vertical gaps of 9px and the ball radius of 8, the ball (diameter 16) can't fit through a 9px gap. Good — that means the ball can't pass through the brick field vertically except... it can't, the gaps are 9px < 16px diameter. Hmm! That means the ball can never reach the bottom through the brick field — it must come from the side or bottom. Since the bricks span x=42..858 and the walls are at 0 and 900, there are side channels of 42px wide — the ball (16px) fits through those. So the ball can descend along the left/right edge. Good, that's how breakout works.

But careful: the ball could get stuck in the 42px side channel bouncing between the wall and the brick edge — that's normal breakout (the ball travels down the channel). Fine.

Also: when the ball destroys bricks, it can then pass through the gaps.

Now, about "The ball must never pass through bricks or the paddle" — the substepping ensures this.

Let me now also verify the brick collision with the substep loop: after a collision, I break out of the substep loop for this ball (to avoid weird double hits). But the ball may still overlap the brick; the push-out resolution handles that.

Implementation of the brick collision response:

```js
function collideBrick(b, br){
  const cx = clamp(b.x, br.x, br.x+br.w);
  const cy = clamp(b.y, br.y, br.y+br.h);
  let dx = b.x-cx, dy=b.y-cy;
  let d2 = dx*dx+dy*dy;
  if (d2 > b.r*b.r) return false;
  let nx, ny, depth;
  if (d2 > 1e-8){
    const d=Math.sqrt(d2); nx=dx/d; ny=dy/d; depth=b.r-d;
  } else {
    // center inside the brick
    const ol=b.x-br.x, or=br.x+br.w-b.x, ot=b.y-br.y, ob=br.y+br.h-b.y;
    const m=Math.min(ol,or,ot,ob);
    if(m===ot){nx=0;ny=-1;depth=ot+b.r;}
    else if(m===ob){nx=0;ny=1;depth=ob+b.r;}
    else if(m===ol){nx=-1;ny=0;depth=ol+b.r;}
    else {nx=1;ny=0;depth=or+b.r;}
  }
  b.x += nx*(depth+0.5); b.y += ny*(depth+0.5);
  const dot = b.vx*nx + b.vy*ny;
  b.vx -= 2*dot*nx; b.vy -= 2*dot*ny;
  // keep the speed
  const sp = Math.hypot(b.vx,b.vy) || b.speed;
  b.speed = sp; // speed stays the same after reflection anyway
  // enforce a minimum vertical component
  const minVy = sp*0.28;
  if (Math.abs(b.vy) < minVy){
    const s = b.vy>=0?1:-1;
    b.vy = s*minVy;
    const vx = Math.sqrt(Math.max(0, sp*sp - b.vy*b.vy));
    b.vx = (b.vx>=0?1:-1)*vx;
  }
  hitBrick(br, b);
  return true;
}
```
Wait: `b.vx = (b.vx>=0?1:-1)*vx` — if b.vx is 0, sign = 1. OK.

Hmm, one issue: enforcing a min |vy| after a brick hit can produce a strange direction but prevents horizontal loops. It's a common technique.

Also: after the reflection, the ball's speed magnitude is unchanged (reflection preserves magnitude). Good.

Ball speed increases: let's increase b.speed gradually: every frame, b.speed = min(MAX, b.speed + 0.6*dt)? Or increase on brick hits: b.speed = min(MAX_SPEED, b.speed + 3) for each ball when a brick is hit. I'll do a slow global ramp: speed = min(MAX, BASE + (score/…)). Simplest: in update, for each ball, b.speed = Math.min(MAX_SPEED, b.speed + 1.2*dt) and normalize the velocity vector to b.speed each frame. Let's do that (normalize each frame to keep the speed consistent).

Implement in stepBall: before moving, normalize (vx,vy) to b.speed.

OK. Now writing the final code. Also need `remaining` bricks count and the win check.

Additionally, add a subtle vertical "wall" gradient at the edges.

Let me write out the complete HTML now.

For the overlay auto-hide: I'll use a timer: `setTimeout(()=>{ if(mode==='demo') hideOverlay(); }, 3600)`. But the requirement says the message should be visible. At 3 s the screenshot: the panel is still visible (auto-hide at 3.6 s). Good — the screenshot shows "Click or press a key to start" AND the game in action behind a lightly dimmed overlay. I'll keep the overlay tint light (.35) so the game is clearly visible.

Hmm, but if the overlay is on top of the canvas with pointer-events auto, the demo still runs (canvas is independent). Good.

Let me finalize the CSS for the overlay to be less obtrusive:
```css
.overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at center, rgba(4,7,16,.15), rgba(4,7,16,.55)); transition:opacity .4s ease;}
```
And the panel has a backdrop blur and a solid-ish background.

Let me write it all now.

Also the toast element: position absolute top center, shown for 1.6s.

Final check on responsive: at 375px width, the canvas is 375-24 = ~351px wide → the height scales to 351*640/900 = 250px. The header stats wrap. The panel text scales with clamp. OK.

Let's write the code.

I also want to make sure the game doesn't lose lives too fast in demo mode. The AI tracks the predicted landing point well. One risk: the prediction doesn't account for the ball's speed change (it uses vx, vy directly — fine) or brick collisions (ignored, acceptable).

Another risk: at high ball speeds, the AI's paddle movement max 620 px/s vs. the ball's horizontal speed up to ~500 px/s — the AI moves toward the target with a max step, so it should be fine. But the prediction could be off if the ball hits a brick on the way down (the prediction ignores bricks) — then the actual landing differs, and the AI may be at the wrong place. To mitigate, recompute the target every frame (it does), so as the ball's actual path changes, the AI re-targets. But when the ball is fast and bounces, the AI may lag. It should be mostly fine.

Also I should handle the case where multiple balls exist (multi-ball) — the AI targets the earliest descending one. Good.

Let's also add: when the AI is in control and the player hasn't taken over, the demo tag stays visible.

Alright, writing the code now.

For the lives: 3. The HUD shows 3 dots.

I also want to display "WIDE 8.2s" in the canvas HUD and the combo.

Let me write the final file.

Additional detail: `addScore` should also update the HUD. I'll call updateHud() every frame (cheap enough) or only when the values change. Every frame is fine (a few DOM writes). Actually writing to the DOM every frame (60fps) with textContent may cause layout thrash, but it's minor. I'll update the HUD only when values change by tracking lastScore/lives/remaining/combo.

Let me write the code now, finally.

```js
// ===== Boot =====
resetGame(true);
requestAnimationFrame(frame);
```

resetGame(demo): 
```js
function resetGame(startDemo){
  score=0; lives=3; combo=1; comboTimer=0; wideTimer=0; shake=0;
  buildLevel();
  paddle.w=PADDLE_W; paddle.x=W/2-PADDLE_W/2;
  balls=[]; particles=[]; drops=[]; floaters=[];
  mode = startDemo? 'demo':'player';
  state='serve'; serveTimer = startDemo? 0.8:1.2;
  spawnBallOnPaddle();
  updateHud();
  if(startDemo){ showStartPanel(); } else { hideOverlay(); }
}
```

spawnBallOnPaddle(): balls = [{x:paddle.x+paddle.w/2, y:PADDLE_Y-BALL_R-1, vx:0, vy:0, r:BALL_R, speed:BASE_SPEED, trail:[]}].

launch(): 
```js
function launch(){
  const b=balls[0]; if(!b) return;
  const a=(Math.random()*0.8-0.4); // angle from vertical
  b.speed = Math.min(b.speed, MAX_SPEED);
  b.vx = Math.sin(a)*b.speed; b.vy = -Math.cos(a)*b.speed;
  state='play';
}
```

loseLife():
```js
function loseLife(){
  lives--; combo=1; wideTimer=0; paddle.w=PADDLE_W;
  updateHud();
  if(lives<=0){ gameOver(); return; }
  spawnBallOnPaddle(); state='serve'; serveTimer = 1.0;
  toast(lives+' lives left');
}
```

Wait, in player mode, waiting 1s then auto-launching is fine.

gameOver(): state='over'; show the panel.

win(): state='win'; show the panel. Also maybe spawn a confetti particle burst.

Let me write the panel HTML function:

```js
function showPanel(html){ panel.innerHTML=html; overlay.classList.remove('hidden'); }
function hidePanel(){ overlay.classList.add('hidden'); }
```

Now let me write the code for real. I'll produce the final file.

One more thing: the canvas `cursor:none` might be odd; let's keep the default cursor but hide it? For breakout, hiding the cursor over the canvas is common. I'll set cursor:none and draw a custom paddle cursor? Nah, keep cursor: crosshair? Let's just not hide it — but the mouse control means the paddle follows the mouse, so hiding the cursor is nicer. I'll hide it (cursor:none) and rely on the paddle. Hmm, but the user might want to see where they are. The paddle IS the cursor. Keep cursor:none.

Careful: on mobile/touch, pointermove works with pointer events. OK.

Writing final code now.

Also note: I must prevent the page from scrolling when pressing arrow keys → e.preventDefault() on arrow keys and space.

Let me write the complete file.

For the canvas resolution function:
```js
function fitCanvas(){
  const dpr = Math.min(window.devicePixelRatio||1, 2);
  canvas.width = Math.round(W*dpr);
  canvas.height = Math.round(H*dpr);
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.imageSmoothingEnabled = true;
}
```
Call at boot and on resize.

Since the CSS sets canvas width:100% and height:auto, the intrinsic size (900*dpr x 640*dpr) gives the aspect ratio 900:640. Good.

Now, writing the render for the background: I'll draw it directly.

Let me write out the final HTML file.

I'll now write the code in full detail.

```js
const W=900,H=640;
...
function render(){
  ctx.save();
  if(shake>0.01){ ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake); }
  drawBackground();
  drawBricks();
  drawDrops();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawFloaters();
  drawCanvasHud();
  ctx.restore();
}
```

drawBackground:
```js
function drawBackground(){
  const g=ctx.createLinearGradient(0,0,0,H);
  g.addColorStop(0,'#0a1024'); g.addColorStop(.5,'#070b18'); g.addColorStop(1,'#04060e');
  ctx.fillStyle=g; ctx.fillRect(0,0,W,H);
  // grid
  ctx.strokeStyle='rgba(110,150,255,.045)'; ctx.lineWidth=1;
  ctx.beginPath();
  for(let x=30;x<W;x+=45){ctx.moveTo(x+.5,0);ctx.lineTo(x+.5,H);}
  for(let y=30;y<H;y+=45){ctx.moveTo(0,y+.5);ctx.lineTo(W,y+.5);}
  ctx.stroke();
  // side glow
  const lg=ctx.createLinearGradient(0,0,26,0); lg.addColorStop(0,'rgba(56,189,248,.16)'); lg.addColorStop(1,'rgba(56,189,248,0)');
  ctx.fillStyle=lg; ctx.fillRect(0,0,26,H);
  const lg2=ctx.createLinearGradient(W,0,W-26,0); lg2.addColorStop(0,'rgba(168,85,247,.16)'); lg2.addColorStop(1,'rgba(168,85,247,0)');
  ctx.fillStyle=lg2; ctx.fillRect(W-26,0,26,H);
  // top line
  ctx.fillStyle='rgba(125,211,252,.25)'; ctx.fillRect(0,0,W,2);
}
```

OK. Let me write the final answer.

Also, the `floaters` text rendering uses ctx.font = 'bold 13px system-ui'.

I think we're ready. Let me write it out completely and double-check the logic.

One more: `stepBall` must handle the ball hitting the top wall (y - r < 0 → y = r, vy = |vy|).

Also, the ball should be removed when y - r > H.

Let me write the update for the balls list:
```js
for(let i=balls.length-1;i>=0;i--){ const b=balls[i]; stepBall(b,dt); if(b.y-b.r>H){ balls.splice(i,1); } }
```

Let me now write the final code text.

Also, when there are multiple balls and one falls, don't lose a life until all are gone. Handled by checking balls.length===0 after the loop.

Final: the keyboard handler for 'r' restarts (keeping the current mode? Let's restart in player mode after a restart via R, since the user is interacting). Actually if the user presses R while in demo, restart in player mode (they pressed a key → they want to play). I'll make R restart in player mode.

Also, clicking the stage in the over/win state restarts.

Let me write the interaction handler:

```js
function onInteract(e){
  if(state==='over'||state==='win'){ resetGame(false); hidePanel(); return; }
  if(mode==='demo'){ takeControl(); }
  else if(state==='serve'){ serveTimer=0; launch(); }
}
```
takeControl(): mode='player'; mouseActive=true (if pointermove set the mouse position); hidePanel(); toast('You have control');

For the mouse position, I'll update mouseX on pointermove over the stage; then takeControl uses the last mouseX (default W/2).

Also, the keydown handler: if the state is over/win and the key is 'r' → restart; any other key → also restart? Let's have any key restart in over/win states (matches the "press R or click" message).

Let me finalize.

Writing out now — I'll produce the complete file.

Also I should double check: the demo starts immediately (no user interaction), so the ball auto-launches after 0.8 s. Good: at 3 s, the ball is in play and bricks may have been broken.

Let me write the code.

Regarding `mouseActive`: set to true on pointermove over the canvas. But pointermove only fires when the mouse moves. Initially mouseActive=false, so in player mode the keyboard works.

Edge case: after taking control via a click (without moving the mouse), the paddle stays where it is until the mouse moves. That's fine.

Final code writing.

For the toast function:
```js
let toastTimer;
function toast(msg){ elToast.textContent=msg; elToast.classList.add('show'); clearTimeout(toastTimer); toastTimer=setTimeout(()=>elToast.classList.remove('show'), 1700); }
```

OK, writing now. I'll also include the `elMode` tag update: "DEMO AI — press any key to take control" → "You are in control" when the player takes over.

Let me produce the final file.