Models 44

Qwen3.8 Max (0902)

by Qwenreasoning defaultClean
Open in new window

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

How it went

Total time
15 min 14 s
Cost
$0.238
Output tokens
39,436
Spent thinking
26,051 tokens
Speed
43.3 tok/s
Console errors
0
First token after
2.4 s
First visible text after
11 min 20 s
Prompt tokens
571
Code size
31.7 KB
Finish reason
stop
Served by
Alibaba
Model id
qwen/qwen3.8-max-0902
Reasoning sent
model default
Max output
131,072 tokens
Generated
Nov 12, 2026

What it was asked

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

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

Code run-1.html · 877 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PRISM BREAK · Brick Breaker</title>
<style>
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;overflow:hidden;display:flex;align-items:center;justify-content:center;
    background:radial-gradient(1100px 800px at 50% 16%, #10222c 0%, #090d16 55%, #05060c 100%);
    font-family:Verdana,Geneva,sans-serif;
  }
  .stage{position:relative;border-radius:12px;line-height:0}
  canvas{
    display:block;border-radius:12px;background:#070d16;cursor:crosshair;touch-action:none;
    box-shadow:0 26px 70px rgba(0,0,0,.65),0 0 0 1px rgba(126,214,255,.15),0 0 46px rgba(56,182,255,.10);
  }
  .stage::after{
    content:"";position:absolute;inset:0;border-radius:12px;pointer-events:none;
    background:
      radial-gradient(120% 95% at 50% 45%, rgba(0,0,0,0) 62%, rgba(0,0,0,.34) 100%),
      repeating-linear-gradient(to bottom, rgba(255,255,255,.03) 0 1px, rgba(0,0,0,0) 1px 3px);
  }
</style>
</head>
<body>
<div class="stage"><canvas id="c" aria-label="Prism Break — breakout game"></canvas></div>
<script>
(function(){
"use strict";

/* ================= CONSTANTS ================= */
const W=640, H=800, HUD_H=64;
const BALL_R=7;
const COLS=9, ROWS=8, GAP=6, SIDE=22;
const BW=(W-SIDE*2-GAP*(COLS-1))/COLS, BH=22, BTOP=104;
const PAD_Y=H-56, PAD_H=15, PAD_BASE_W=104, PAD_WIDE_W=172;
const BASE_SP=330, MAX_SP=560, MAX_BOUNCE=1.08, MAX_BALLS=6;

const F_DISPLAY='"Arial Black","Arial Bold",Arial,sans-serif';
const F_MONO='"Courier New",ui-monospace,monospace';
const F_BODY='Verdana,Geneva,sans-serif';

const ROWC=[
  ['#ff5d5d','#ffa294'],['#ff8a3d','#ffc07e'],['#ffc93d','#ffe59a'],['#9ad44a','#cdeb8f'],
  ['#35d1a4','#8df0d4'],['#38b3ff','#93d9ff'],['#5c86ff','#a8c0ff'],['#a86bff','#d3b0ff']
];
const TOUGH_BASE='#5c6f86', TOUGH_DMG='#46566b';

/* ================= CANVAS ================= */
const canvas=document.getElementById('c');
const ctx=canvas.getContext('2d');
let viewScale=1;

function resize(){
  const s=Math.min((innerWidth-18)/W,(innerHeight-18)/H);
  const cssW=Math.max(280,Math.floor(W*s)), cssH=Math.floor(cssW*H/W);
  canvas.style.width=cssW+'px'; canvas.style.height=cssH+'px';
  const dpr=Math.min(2,window.devicePixelRatio||1);
  canvas.width=Math.round(cssW*dpr); canvas.height=Math.round(cssH*dpr);
  viewScale=canvas.width/W;
}
addEventListener('resize',resize); resize();

/* ================= HELPERS ================= */
function clamp(v,a,b){return v<a?a:v>b?b:v}
function rr(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 shade(hex,amt){
  const n=parseInt(hex.slice(1),16);
  const r=clamp((n>>16)+amt,0,255),g=clamp((n>>8&255)+amt,0,255),b=clamp((n&255)+amt,0,255);
  return 'rgb('+r+','+g+','+b+')';
}
function shuffle(a){for(let i=a.length-1;i>0;i--){const j=(Math.random()*(i+1))|0;const t=a[i];a[i]=a[j];a[j]=t;}return a;}
function textC(str,x,y,font,color,align){ctx.font=font;ctx.fillStyle=color;ctx.textAlign=align||'center';ctx.textBaseline='middle';ctx.fillText(str,x,y);}

/* ================= AUDIO ================= */
let AC=null,master=null,muted=false,lastWall=0;
function ensureAudio(){
  if(!AC){
    try{
      AC=new (window.AudioContext||window.webkitAudioContext)();
      master=AC.createGain(); master.gain.value=0.16; master.connect(AC.destination);
    }catch(e){AC=null;}
  }
  if(AC&&AC.state==='suspended')AC.resume();
}
function tone(f0,f1,dur,type,vol,delay){
  if(!AC||muted)return;
  try{
    const t0=AC.currentTime+(delay||0);
    const o=AC.createOscillator(),g=AC.createGain();
    o.type=type||'square';
    o.frequency.setValueAtTime(Math.max(30,f0),t0);
    o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t0+dur);
    g.gain.setValueAtTime(vol||0.4,t0);
    g.gain.exponentialRampToValueAtTime(0.001,t0+dur);
    o.connect(g);g.connect(master);o.start(t0);o.stop(t0+dur+0.03);
  }catch(e){}
}
const sfx={
  paddle:()=>tone(190,270,.06,'square',.35),
  wall:()=>{if(timeAll-lastWall>0.06){tone(150,120,.04,'triangle',.16);lastWall=timeAll;}},
  brick:(row,tough)=>{const f=tough?200:340+(ROWS-1-row)*36;tone(f,f*1.5,.07,'square',.4);if(!tough)tone(f*2,f*2,.05,'square',.16,.03);},
  clink:()=>{tone(120,90,.07,'sawtooth',.32);tone(820,620,.03,'square',.1);},
  launch:()=>tone(240,560,.12,'triangle',.4),
  catchp:()=>[523,659,784,1047].forEach((f,i)=>tone(f,f,.09,'square',.36,i*.06)),
  life:()=>tone(340,60,.5,'sawtooth',.5),
  over:()=>[311,247,175,110].forEach((f,i)=>tone(f,f*.97,.22,'sawtooth',.36,i*.16)),
  win:()=>[392,494,587,784,988].forEach((f,i)=>tone(f,f,.14,'square',.36,i*.09))
};

/* ================= STATE ================= */
let mode='demo';           // demo | play | over | win
let paused=false;
let score=0,best=0,lives=3,combo=0,lastBonus=0,ballSpeed=BASE_SP;
let wideT=0,bricksLeft=0,timeAll=0,shake=0,redFlash=0,scorePulse=0;
let balls=[],bricks=[],drops=[],parts=[],texts=[],buttons=[];
let paddle={x:W/2,w:PAD_BASE_W,squash:0};
let keys={l:false,r:false};
let mx=W/2,my=H/2;

/* ================= BACKGROUND ================= */
let bgGrad=null,nebula=null;
const stars=[],dust=[];
function buildBackground(){
  bgGrad=ctx.createLinearGradient(0,0,0,H);
  bgGrad.addColorStop(0,'#070d16');
  bgGrad.addColorStop(.5,'#0c1a24');
  bgGrad.addColorStop(1,'#121022');
  const nc=document.createElement('canvas');nc.width=W;nc.height=H;
  const nx=nc.getContext('2d');
  const blob=(x,y,r,c)=>{const g=nx.createRadialGradient(x,y,0,x,y,r);g.addColorStop(0,c);g.addColorStop(1,'rgba(0,0,0,0)');nx.fillStyle=g;nx.fillRect(0,0,W,H);};
  blob(140,220,280,'rgba(38,110,120,0.20)');
  blob(520,560,320,'rgba(120,70,40,0.14)');
  blob(330,380,360,'rgba(50,70,140,0.13)');
  nebula=nc;
  for(let i=0;i<110;i++)stars.push({x:Math.random()*W,y:Math.random()*H,z:.3+Math.random()*.7,p:Math.random()*6.28});
  for(let i=0;i<14;i++)dust.push({x:Math.random()*W,y:Math.random()*H,r:1+Math.random()*1.6,s:4+Math.random()*8,p:Math.random()*6.28});
}
buildBackground();

function drawBackground(){
  ctx.fillStyle=bgGrad;ctx.fillRect(0,0,W,H);
  ctx.drawImage(nebula,0,0);
  for(const s of stars){
    const a=(.25+.75*Math.abs(Math.sin(timeAll*s.z*1.6+s.p)))*s.z;
    ctx.fillStyle='rgba(200,235,255,'+a.toFixed(3)+')';
    ctx.fillRect(s.x,s.y,s.z>.75?2:1,s.z>.75?2:1);
  }
  ctx.fillStyle='rgba(140,200,220,0.10)';
  for(const d of dust){
    const yy=(d.y-timeAll*d.s)%H;
    ctx.beginPath();ctx.arc(d.x+Math.sin(timeAll*.5+d.p)*10,(yy+H)%H,d.r,0,6.29);ctx.fill();
  }
}

/* ================= LEVEL ================= */
function makeCracks(){
  const segs=[];
  for(let i=0;i<3;i++){
    const pts=[];
    let x=BW*(0.3+Math.random()*0.4), y=BH*(0.3+Math.random()*0.4);
    const ang=Math.random()*Math.PI*2;
    pts.push([x,y]);
    const steps=3+((Math.random()*2)|0);
    for(let s=0;s<steps;s++){
      const a=ang+(Math.random()-.5)*1.3;
      x+=Math.cos(a)*(5+Math.random()*7);
      y+=Math.sin(a)*(2.5+Math.random()*4);
      pts.push([clamp(x,1,BW-1),clamp(y,1,BH-1)]);
    }
    segs.push(pts);
  }
  return segs;
}
function buildLevel(){
  bricks=[];
  for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
    const tough=Math.random()<0.24;
    bricks.push({
      x:SIDE+c*(BW+GAP),y:BTOP+r*(BH+GAP),w:BW,h:BH,row:r,
      hp:tough?2:1,tough:tough,alive:true,flash:0,power:null,
      cracks:tough?makeCracks():null
    });
  }
  let toughCount=bricks.filter(b=>b.tough).length;
  while(toughCount<10){
    const b=bricks[(Math.random()*bricks.length)|0];
    if(!b.tough){b.tough=true;b.hp=2;b.cracks=makeCracks();toughCount++;}
  }
  const idx=shuffle(bricks.map((_,i)=>i)).slice(0,13);
  idx.forEach((bi,i)=>{bricks[bi].power=(i%2===0)?'wide':'multi';});
  bricksLeft=bricks.length;
}

function newStuckBall(){
  return {x:paddle.x,y:PAD_Y-BALL_R-1,vx:0,vy:0,stuck:true,dead:false,trail:[],
          launchT:(mode==='demo')?0.9:2.2};
}
function spawnServe(){balls=[newStuckBall()];}

function newGame(m){
  mode=m;paused=false;
  score=0;lives=3;combo=0;lastBonus=0;ballSpeed=BASE_SP;wideT=0;
  paddle={x:W/2,w:PAD_BASE_W,squash:0};
  balls=[];drops=[];parts=[];texts=[];buttons=[];
  shake=0;redFlash=0;scorePulse=0;
  buildLevel();
  spawnServe();
}

/* ================= PARTICLES / TEXTS ================= */
function addPart(p){parts.push(p);if(parts.length>520)parts.splice(0,parts.length-520);}
function shardBurst(x,y,w,h,color,n,pow){
  for(let i=0;i<n;i++){
    addPart({kind:'shard',x:x+Math.random()*w,y:y+Math.random()*h,
      vx:(Math.random()-.5)*(pow*2.2),vy:-40-Math.random()*pow,
      g:620,life:.5+Math.random()*.45,max:.95,size:2+Math.random()*3,
      rot:Math.random()*6.28,vr:(Math.random()-.5)*12,
      color:Math.random()<.5?color:shade(color,45)});
  }
}
function sparkBurst(x,y,color,n,pow){
  for(let i=0;i<n;i++){
    const a=Math.random()*6.28,sp=40+Math.random()*pow;
    addPart({kind:'spark',x:x,y:y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,
      g:80,life:.22+Math.random()*.3,max:.5,size:1.4+Math.random()*1.8,color:color});
  }
}
function confetti(){
  for(let i=0;i<90;i++){
    addPart({kind:'shard',x:Math.random()*W,y:-10-Math.random()*60,
      vx:(Math.random()-.5)*120,vy:60+Math.random()*140,g:260,
      life:1.2+Math.random()*.9,max:2.1,size:2.5+Math.random()*3,
      rot:Math.random()*6.28,vr:(Math.random()-.5)*10,
      color:ROWC[(Math.random()*ROWS)|0][0]});
  }
}
function addText(x,y,txt,color,size,big){
  texts.push({x:clamp(x,60,W-60),y:y,txt:txt,color:color,size:size||13,life:big?1.4:.9,max:big?1.4:.9,big:!!big});
}
function toast(txt,color){addText(W/2,215,txt,color||'#eaffff',24,true);}

/* ================= GAME LOGIC ================= */
function sanitizeVel(b){
  const sp=Math.hypot(b.vx,b.vy);if(sp<1)return;
  const minVy=.24*sp;
  if(Math.abs(b.vy)<minVy){
    const sgn=b.vy===0?-1:(b.vy>0?1:-1);
    b.vy=sgn*minVy;
    const vxm=Math.sqrt(Math.max(0,sp*sp-b.vy*b.vy));
    b.vx=(b.vx===0?(Math.random()<.5?-1:1):Math.sign(b.vx))*vxm;
  }
}
function rotVel(b,a){
  const c=Math.cos(a),s=Math.sin(a);
  const vx=b.vx*c-b.vy*s;
  b.vy=b.vx*s+b.vy*c;b.vx=vx;
}
function wallFx(x,y){sfx.wall();sparkBurst(x,y,'#8fe8de',3,90);}

function collidePaddle(b){
  const hw=paddle.w/2,px1=paddle.x-hw,px2=paddle.x+hw,py1=PAD_Y,py2=PAD_Y+PAD_H;
  if(b.x<px1-BALL_R||b.x>px2+BALL_R||b.y<py1-BALL_R||b.y>py2+BALL_R)return;
  const cx=clamp(b.x,px1,px2),cy=clamp(b.y,py1,py2);
  const dx=b.x-cx,dy=b.y-cy,d2=dx*dx+dy*dy;
  if(d2>BALL_R*BALL_R)return;
  if(b.vy>0&&b.y<py1+6){
    const rel=clamp((b.x-paddle.x)/hw,-1,1);
    const ang=rel*MAX_BOUNCE;
    ballSpeed=Math.min(MAX_SP,ballSpeed+7);
    b.vx=Math.sin(ang)*ballSpeed;
    b.vy=-Math.cos(ang)*ballSpeed;
    b.y=py1-BALL_R-.5;
    combo=0;paddle.squash=1;
    sfx.paddle();sparkBurst(b.x,py1,'#bff7ff',6,120);
  }else{
    let nx=0,ny=-1;
    if(d2>1e-6){const d=Math.sqrt(d2);nx=dx/d;ny=dy/d;}
    const dot=b.vx*nx+b.vy*ny;
    if(dot<0){b.vx-=2*dot*nx;b.vy-=2*dot*ny;}
    const pen=BALL_R-Math.sqrt(d2)+.3;
    b.x+=nx*pen;b.y+=ny*pen;
    sanitizeVel(b);
  }
}

function hitBrick(br){
  br.hp--;br.flash=1;
  const cx=br.x+br.w/2,cy=br.y+br.h/2;
  if(br.hp<=0){
    br.alive=false;bricksLeft--;combo++;
    const pts=(br.tough?200:100-br.row*10)+(combo-1)*10;
    score+=pts;scorePulse=1;best=Math.max(best,score);
    addText(cx,cy,'+'+pts,ROWC[br.row][1],13);
    shardBurst(br.x,br.y,br.w,br.h,ROWC[br.row][0],16,140);
    sparkBurst(cx,cy,'#ffffff',9,190);
    shake=Math.min(16,shake+2.4);
    sfx.brick(br.row,br.tough);
    if(br.power)drops.push({x:cx,y:cy,type:br.power,vy:130,ph:Math.random()*6.28});
    if(bricksLeft<=0)endGame(true);
  }else{
    sparkBurst(cx,cy,'#d8e6f2',6,120);
    shake=Math.min(16,shake+1);
    sfx.clink();
  }
}

function collideBricks(b){
  for(let i=0;i<bricks.length;i++){
    const br=bricks[i];
    if(!br.alive)continue;
    const cx=clamp(b.x,br.x,br.x+br.w),cy=clamp(b.y,br.y,br.y+br.h);
    const dx=b.x-cx,dy=b.y-cy,d2=dx*dx+dy*dy;
    if(d2>BALL_R*BALL_R)continue;
    let nx,ny;
    if(d2>1e-6){const d=Math.sqrt(d2);nx=dx/d;ny=dy/d;}
    else{
      const l=b.x-br.x,r=br.x+br.w-b.x,t=b.y-br.y,bo=br.y+br.h-b.y;
      const m=Math.min(l,r,t,bo);
      nx=(m===l?-1:m===r?1:0);ny=(m===t?-1:m===bo?1:0);
    }
    const dot=b.vx*nx+b.vy*ny;
    if(dot<0){b.vx-=2*dot*nx;b.vy-=2*dot*ny;}
    const pen=BALL_R-Math.sqrt(d2)+.2;
    b.x+=nx*pen;b.y+=ny*pen;
    rotVel(b,(Math.random()-.5)*.05);
    sanitizeVel(b);
    hitBrick(br);
    return true;
  }
  return false;
}

function moveBall(b,dt){
  const sp=Math.hypot(b.vx,b.vy)||BASE_SP;
  const steps=Math.max(1,Math.ceil(sp*dt/4));   // substeps: never move more than 4px — no tunneling
  const sdt=dt/steps;
  for(let i=0;i<steps;i++){
    b.x+=b.vx*sdt;b.y+=b.vy*sdt;
    if(b.x<BALL_R){b.x=BALL_R;b.vx=Math.abs(b.vx);wallFx(b.x,b.y);}
    else if(b.x>W-BALL_R){b.x=W-BALL_R;b.vx=-Math.abs(b.vx);wallFx(b.x,b.y);}
    if(b.y<HUD_H+BALL_R){b.y=HUD_H+BALL_R;b.vy=Math.abs(b.vy);wallFx(b.x,HUD_H);}
    collidePaddle(b);
    collideBricks(b);
    if(b.y-BALL_R>H+14){b.dead=true;break;}
  }
}

function launch(b){
  if(!b.stuck)return;
  b.stuck=false;
  const a=(Math.random()*.6-.3);
  b.vx=Math.sin(a)*ballSpeed;b.vy=-Math.cos(a)*ballSpeed;
  sfx.launch();
}
function launchAllStuck(){let any=false;for(const b of balls)if(b.stuck){launch(b);any=true;}return any;}

function spawnExtraBalls(){
  const need=balls.length===0?1:Math.min(2,MAX_BALLS-balls.length);
  const angles=need===1?[0]:[-0.55,0.55];
  for(let i=0;i<need;i++){
    balls.push({x:paddle.x,y:PAD_Y-BALL_R-2,
      vx:Math.sin(angles[i])*ballSpeed,vy:-Math.cos(angles[i])*ballSpeed,
      stuck:false,dead:false,trail:[],launchT:0});
  }
  sparkBurst(paddle.x,PAD_Y,'#ff9fce',14,200);
}

function applyPower(type){
  score+=25;scorePulse=1;
  if(type==='wide'){wideT=10;toast('WIDE PADDLE!','#ffd24a');}
  else{spawnExtraBalls();toast('MULTIBALL!','#ff5fa8');}
  sfx.catchp();
}

function lifeLost(){
  lives--;combo=0;shake=13;redFlash=1;sfx.life();
  wideT=0;drops.length=0;
  if(lives>0){ballSpeed=BASE_SP;spawnServe();}
  else if(mode==='demo'){newGame('demo');}
  else{mode='over';best=Math.max(best,score);sfx.over();}
}

function endGame(win){
  if(mode==='demo'){newGame('demo');return;}
  if(win){
    lastBonus=lives*500;score+=lastBonus;best=Math.max(best,score);
    confetti();sfx.win();mode='win';
  }else{
    sfx.over();mode='over';
  }
  balls.length=0;
}

function startPlay(){
  newGame('play');
  toast('GET READY','#8af0d4');
}

/* demo AI */
function predictX(b){
  const t=(PAD_Y-BALL_R-b.y)/b.vy;
  let x=b.x+b.vx*t;
  const L=BALL_R,span=(W-BALL_R)-L;
  let u=(x-L)%(2*span);if(u<0)u+=2*span;
  return L+(u>span?2*span-u:u);
}
function aiPaddle(dt){
  let target=W/2+Math.sin(timeAll*.8)*30,bestT=Infinity;
  for(const b of balls){
    if(b.stuck){target=b.x;break;}
    if(b.vy>0){
      const t=(PAD_Y-BALL_R-b.y)/b.vy;
      if(t>=0&&t<bestT){bestT=t;target=predictX(b);}
    }
  }
  target+=Math.sin(timeAll*1.9)*8;
  const dx=target-paddle.x,step=Math.min(Math.abs(dx),430*dt);
  paddle.x+=Math.sign(dx)*step;
}

/* ================= UPDATE ================= */
function updateFx(dt){
  for(let i=parts.length-1;i>=0;i--){
    const p=parts[i];
    p.life-=dt;
    if(p.life<=0){parts.splice(i,1);continue;}
    p.vy+=p.g*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;
    if(p.vr)p.rot+=p.vr*dt;
  }
  for(let i=texts.length-1;i>=0;i--){
    const t=texts[i];t.life-=dt;t.y-=30*dt;
    if(t.life<=0)texts.splice(i,1);
  }
  shake=Math.max(0,shake-shake*9*dt-3*dt);
  redFlash=Math.max(0,redFlash-dt*1.4);
  scorePulse=Math.max(0,scorePulse-dt*4);
}

function sim(dt){
  /* paddle */
  if(mode==='demo')aiPaddle(dt);
  else{
    if(keys.l)paddle.x-=560*dt;
    if(keys.r)paddle.x+=560*dt;
  }
  const target=(wideT>0)?PAD_WIDE_W:PAD_BASE_W;
  paddle.w+=(target-paddle.w)*Math.min(1,dt*8);
  paddle.x=clamp(paddle.x,paddle.w/2+6,W-paddle.w/2-6);
  paddle.squash=Math.max(0,paddle.squash-dt*4);
  wideT=Math.max(0,wideT-dt);

  /* balls */
  for(const b of balls){
    if(b.stuck){
      b.x=paddle.x;b.y=PAD_Y-BALL_R-1;
      b.launchT-=dt;
      if(b.launchT<=0)launch(b);
    }else{
      moveBall(b,dt);
      if(!b.dead){b.trail.push({x:b.x,y:b.y});if(b.trail.length>10)b.trail.shift();}
    }
  }
  balls=balls.filter(b=>!b.dead);
  if(balls.length===0&&(mode==='play'||mode==='demo'))lifeLost();

  /* drops */
  for(let i=drops.length-1;i>=0;i--){
    const d=drops[i];
    d.vy=Math.min(235,d.vy+50*dt);d.y+=d.vy*dt;
    if(Math.random()<.25)addPart({kind:'spark',x:d.x+(Math.random()-.5)*20,y:d.y-8,vx:0,vy:-24,g:0,life:.3,max:.3,size:1.4,color:d.type==='wide'?'#ffd24a':'#ff5fa8'});
    const hw=paddle.w/2;
    if(d.y+10>=PAD_Y&&d.y-10<=PAD_Y+PAD_H&&d.x+19>paddle.x-hw&&d.x-19<paddle.x+hw){
      applyPower(d.type);sparkBurst(d.x,PAD_Y,d.type==='wide'?'#ffd24a':'#ff5fa8',14,200);
      drops.splice(i,1);continue;
    }
    if(d.y>H+30)drops.splice(i,1);
  }

  /* brick flash decay */
  for(const br of bricks)if(br.flash>0)br.flash=Math.max(0,br.flash-dt*5);
}

function update(dt){
  timeAll+=dt;
  if(paused)return;
  updateFx(dt);
  if(mode==='play'||mode==='demo')sim(dt);
}

/* ================= DRAWING ================= */
function drawRails(){
  ctx.fillStyle='rgba(80,220,210,.14)';
  ctx.fillRect(0,HUD_H,3,H-HUD_H);ctx.fillRect(W-3,HUD_H,3,H-HUD_H);ctx.fillRect(0,HUD_H,W,3);
  ctx.fillStyle='rgba(150,255,240,.38)';
  ctx.fillRect(1,HUD_H,1,H-HUD_H);ctx.fillRect(W-2,HUD_H,1,H-HUD_H);ctx.fillRect(0,HUD_H+1,W,1);
}

function drawBricks(){
  for(const br of bricks){
    if(!br.alive)continue;
    const base=br.tough?(br.hp===1?TOUGH_DMG:TOUGH_BASE):ROWC[br.row][0];
    ctx.fillStyle=base;rr(br.x,br.y,br.w,br.h,4);ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.26)';rr(br.x+2,br.y+2,br.w-4,5,2);ctx.fill();
    ctx.fillStyle='rgba(0,0,0,.22)';rr(br.x+2,br.y+br.h-5,br.w-4,3,1.5);ctx.fill();
    if(br.tough){
      ctx.fillStyle='rgba(230,240,255,.32)';
      const rv=[[4,4],[br.w-4,4],[4,br.h-4],[br.w-4,br.h-4]];
      for(const p of rv){ctx.beginPath();ctx.arc(br.x+p[0],br.y+p[1],1.5,0,6.29);ctx.fill();}
      if(br.hp===1){
        ctx.fillStyle='rgba(0,0,0,.22)';rr(br.x,br.y,br.w,br.h,4);ctx.fill();
        ctx.strokeStyle='rgba(8,12,18,.8)';ctx.lineWidth=1.4;ctx.lineJoin='round';
        for(const seg of br.cracks){
          ctx.beginPath();
          ctx.moveTo(br.x+seg[0][0],br.y+seg[0][1]);
          for(let i=1;i<seg.length;i++)ctx.lineTo(br.x+seg[i][0],br.y+seg[i][1]);
          ctx.stroke();
        }
      }
    }
    if(br.power){
      const cx=br.x+br.w/2,cy=br.y+br.h/2;
      const col=br.power==='wide'?'#ffd24a':'#ff5fa8';
      ctx.fillStyle='rgba(5,9,15,.72)';ctx.beginPath();ctx.arc(cx,cy,7.5,0,6.29);ctx.fill();
      ctx.strokeStyle=col;ctx.lineWidth=1.6;ctx.beginPath();ctx.arc(cx,cy,7.5,0,6.29);ctx.stroke();
      ctx.strokeStyle='#ffffff';ctx.fillStyle='#ffffff';
      if(br.power==='wide'){
        ctx.lineWidth=1.4;ctx.beginPath();ctx.moveTo(cx-3.6,cy);ctx.lineTo(cx+3.6,cy);ctx.stroke();
        ctx.beginPath();ctx.moveTo(cx-4.8,cy);ctx.lineTo(cx-2.4,cy-1.8);ctx.lineTo(cx-2.4,cy+1.8);ctx.closePath();ctx.fill();
        ctx.beginPath();ctx.moveTo(cx+4.8,cy);ctx.lineTo(cx+2.4,cy-1.8);ctx.lineTo(cx+2.4,cy+1.8);ctx.closePath();ctx.fill();
      }else{
        ctx.beginPath();ctx.arc(cx,cy-2.2,1.4,0,6.29);ctx.arc(cx-2.4,cy+1.6,1.4,0,6.29);ctx.arc(cx+2.4,cy+1.6,1.4,0,6.29);ctx.fill();
      }
    }
    if(br.flash>0){
      ctx.fillStyle='rgba(255,255,255,'+(br.flash*.85).toFixed(3)+')';
      rr(br.x,br.y,br.w,br.h,4);ctx.fill();
    }
  }
}

function drawCapsule(x,y,type,t,scale,wob){
  ctx.save();ctx.translate(x,y);
  if(wob)ctx.rotate(Math.sin(t*5)*.12);
  const s=scale||1;ctx.scale(s,s);
  const w=38,h=20;
  const glow=type==='wide'?'#ffd24a':'#ff5fa8';
  ctx.shadowColor=glow;ctx.shadowBlur=14;
  const g=ctx.createLinearGradient(0,-h/2,0,h/2);
  if(type==='wide'){g.addColorStop(0,'#ffe9a8');g.addColorStop(.5,'#ffc93d');g.addColorStop(1,'#c8871a');}
  else{g.addColorStop(0,'#ffb3d1');g.addColorStop(.5,'#ff5fa8');g.addColorStop(1,'#b3256d');}
  rr(-w/2,-h/2,w,h,h/2);ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  ctx.strokeStyle='rgba(255,255,255,.55)';ctx.lineWidth=1;
  rr(-w/2,-h/2,w,h,h/2);ctx.stroke();
  ctx.strokeStyle='#ffffff';ctx.fillStyle='#ffffff';
  if(type==='wide'){
    ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(-8,0);ctx.lineTo(8,0);ctx.stroke();
    ctx.beginPath();ctx.moveTo(-11,0);ctx.lineTo(-5,-3.4);ctx.lineTo(-5,3.4);ctx.closePath();ctx.fill();
    ctx.beginPath();ctx.moveTo(11,0);ctx.lineTo(5,-3.4);ctx.lineTo(5,3.4);ctx.closePath();ctx.fill();
  }else{
    ctx.beginPath();ctx.arc(0,-3.4,2.2,0,6.29);ctx.arc(-4.4,2.6,2.2,0,6.29);ctx.arc(4.4,2.6,2.2,0,6.29);ctx.fill();
  }
  ctx.restore();
}
function drawDrops(){
  for(const d of drops)drawCapsule(d.x,d.y,d.type,timeAll+d.ph,1+.06*Math.sin(timeAll*6+d.ph),true);
}

function drawPaddle(){
  const w=paddle.w,h=PAD_H*(1-.3*paddle.squash);
  const x=paddle.x-w/2,y=PAD_Y+(PAD_H-h);
  ctx.save();ctx.globalCompositeOperation='lighter';
  const gg=ctx.createRadialGradient(paddle.x,PAD_Y+PAD_H+6,4,paddle.x,PAD_Y+PAD_H+6,w*.62);
  gg.addColorStop(0,wideT>0?'rgba(255,200,80,.35)':'rgba(90,220,255,.32)');
  gg.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=gg;ctx.beginPath();ctx.ellipse(paddle.x,PAD_Y+PAD_H+6,w*.62,16,0,0,6.29);ctx.fill();
  ctx.restore();
  const g=ctx.createLinearGradient(0,y,0,y+h);
  if(wideT>0){g.addColorStop(0,'#fff3c8');g.addColorStop(.45,'#ffd76a');g.addColorStop(1,'#c9861f');}
  else{g.addColorStop(0,'#f2feff');g.addColorStop(.45,'#9fe0ff');g.addColorStop(1,'#3583b8');}
  ctx.fillStyle=g;rr(x,y,w,h,h/2);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.4)';rr(x+6,y+2,w-12,h*.32,3);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.85)';
  ctx.beginPath();ctx.arc(x+4,y+h/2,2,0,6.29);ctx.arc(x+w-4,y+h/2,2,0,6.29);ctx.fill();
}

function drawBalls(){
  ctx.save();ctx.globalCompositeOperation='lighter';
  for(const b of balls){
    for(let i=0;i<b.trail.length;i++){
      const a=(i/b.trail.length)*.3;
      ctx.fillStyle='rgba(90,220,255,'+a.toFixed(3)+')';
      ctx.beginPath();ctx.arc(b.trail[i].x,b.trail[i].y,BALL_R*(.4+.6*i/b.trail.length),0,6.29);ctx.fill();
    }
  }
  ctx.restore();
  for(const b of balls){
    ctx.save();
    ctx.shadowColor='#7fe9ff';ctx.shadowBlur=16;
    const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,BALL_R);
    g.addColorStop(0,'#ffffff');g.addColorStop(.6,'#bdf1ff');g.addColorStop(1,'#3fb9ff');
    ctx.fillStyle=g;ctx.beginPath();ctx.arc(b.x,b.y,BALL_R,0,6.29);ctx.fill();
    ctx.restore();
  }
}

function drawParticles(){
  for(const p of parts){
    if(p.kind!=='shard')continue;
    const a=clamp(p.life/p.max,0,1);
    ctx.save();ctx.globalAlpha=a;ctx.translate(p.x,p.y);ctx.rotate(p.rot);
    ctx.fillStyle=p.color;ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size*.7);
    ctx.restore();
  }
  ctx.save();ctx.globalCompositeOperation='lighter';
  for(const p of parts){
    if(p.kind!=='spark')continue;
    const a=clamp(p.life/p.max,0,1);
    ctx.globalAlpha=a;ctx.fillStyle=p.color;
    ctx.beginPath();ctx.arc(p.x,p.y,p.size*a+.4,0,6.29);ctx.fill();
  }
  ctx.restore();
}

function drawTexts(){
  for(const t of texts){
    const a=clamp(t.life/t.max,0,1);
    ctx.save();ctx.globalAlpha=a;
    textC(t.txt,t.x,t.y,(t.big?'900 ':'bold ')+t.size+'px '+(t.big?F_DISPLAY:F_MONO),t.color);
    ctx.restore();
  }
}

function drawHUD(){
  ctx.fillStyle='rgba(8,10,22,.8)';ctx.fillRect(0,0,W,HUD_H);
  const lg=ctx.createLinearGradient(0,0,W,0);
  lg.addColorStop(0,'#35d1a4');lg.addColorStop(.5,'#ffc93d');lg.addColorStop(1,'#ff5fa8');
  ctx.fillStyle=lg;ctx.fillRect(0,HUD_H-2,W,2);

  try{ctx.letterSpacing='3px';}catch(e){}
  textC('SCORE',58,16,'bold 10px '+F_MONO,'#7f9db3','center');
  try{ctx.letterSpacing='0px';}catch(e){}
  const sz=22+6*scorePulse;
  textC(String(score).padStart(6,'0'),58,38,'900 '+sz+'px '+F_MONO,scorePulse>.3?'#ffe59a':'#eaf6ff','center');

  if(combo>=2){
    const p=1+.08*Math.sin(timeAll*9);
    ctx.save();ctx.translate(W/2,HUD_H/2);ctx.scale(p,p);
    textC('COMBO ×'+combo,0,0,'900 19px '+F_DISPLAY,'#ffd24a');
    ctx.restore();
  }else{
    textC('BRICKS LEFT '+bricksLeft,W/2,HUD_H/2,'bold 12px '+F_MONO,'#7f9db3');
  }

  for(let i=0;i<3;i++){
    const lx=W-26-i*24,ly=HUD_H/2;
    if(i<lives){
      ctx.save();ctx.shadowColor='#7fe9ff';ctx.shadowBlur=8;
      ctx.fillStyle='#9fe8ff';ctx.beginPath();ctx.arc(lx,ly,6.5,0,6.29);ctx.fill();
      ctx.restore();
    }else{
      ctx.strokeStyle='rgba(120,150,170,.35)';ctx.lineWidth=1.5;
      ctx.beginPath();ctx.arc(lx,ly,6,0,6.29);ctx.stroke();
    }
  }

  if(wideT>0){
    const bw=132,bx=W/2-bw/2,by=HUD_H+10;
    ctx.fillStyle='rgba(22,17,4,.78)';rr(bx,by,bw,22,11);ctx.fill();
    ctx.strokeStyle='#ffd24a';ctx.lineWidth=1.4;rr(bx,by,bw,22,11);ctx.stroke();
    textC('WIDE',bx+26,by+11,'bold 10px '+F_MONO,'#ffd24a');
    ctx.fillStyle='rgba(255,210,74,.25)';rr(bx+48,by+8,bw-58,6,3);ctx.fill();
    ctx.fillStyle='#ffd24a';rr(bx+48,by+8,(bw-58)*(wideT/10),6,3);ctx.fill();
  }
}

function drawServeHint(){
  if(mode!=='play'||paused)return;
  const stuck=balls.find(b=>b.stuck);
  if(!stuck)return;
  const a=.55+.45*Math.sin(timeAll*5);
  ctx.save();ctx.globalAlpha=a;
  textC('CLICK OR PRESS SPACE TO LAUNCH',W/2,H-170,'bold 14px '+F_MONO,'#bfeaff');
  ctx.strokeStyle='#bfeaff';ctx.lineWidth=2;
  ctx.beginPath();ctx.moveTo(stuck.x,PAD_Y-24);ctx.lineTo(stuck.x,PAD_Y-38);
  ctx.moveTo(stuck.x-5,PAD_Y-32);ctx.lineTo(stuck.x,PAD_Y-38);ctx.lineTo(stuck.x+5,PAD_Y-32);
  ctx.stroke();
  ctx.restore();
}

/* overlays */
function drawTitle(){
  ctx.fillStyle='rgba(6,9,18,.52)';ctx.fillRect(0,0,W,H);
  const cy=430;
  ctx.save();
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='900 62px '+F_DISPLAY;
  ctx.globalAlpha=.85;
  ctx.fillStyle='#ff5d7a';ctx.fillText('PRISM BREAK',W/2-3,cy-190+2);
  ctx.fillStyle='#4dd7ff';ctx.fillText('PRISM BREAK',W/2+3,cy-190-2);
  ctx.globalAlpha=1;
  ctx.shadowColor='rgba(120,220,255,.6)';ctx.shadowBlur=18;
  ctx.fillStyle='#f4fbff';ctx.fillText('PRISM BREAK',W/2,cy-190);
  ctx.shadowBlur=0;
  try{ctx.letterSpacing='6px';}catch(e){}
  textC('— ARCADE BRICK BREAKER —',W/2,cy-148,'bold 12px '+F_MONO,'#7fd8ff');
  try{ctx.letterSpacing='0px';}catch(e){}

  const a=.55+.45*Math.sin(timeAll*4.2);
  ctx.globalAlpha=a;
  textC('▶ CLICK OR PRESS ANY KEY TO START',W/2,cy-96,'900 18px '+F_DISPLAY,'#ffe59a');
  ctx.globalAlpha=1;
  textC('MOVE: mouse or ◀ ▶   ·   LAUNCH: click / space   ·   P pause',W/2,cy-62,'11px '+F_BODY,'#9fb8c8');

  drawCapsule(W/2-130,cy+8,'wide',timeAll,.95,false);
  textC('WIDE PADDLE · 10s',W/2-130,cy+34,'bold 11px '+F_MONO,'#ffd24a');
  drawCapsule(W/2+130,cy+8,'multi',timeAll+1,.95,false);
  textC('MULTIBALL · ×3',W/2+130,cy+34,'bold 11px '+F_MONO,'#ff8fc4');

  textC('CATCH THE CAPSULES · CLEAR ALL 72 BRICKS · 3 LIVES',W/2,cy+76,'11px '+F_BODY,'#7f9db3');
  if(best>0)textC('BEST '+String(best).padStart(6,'0'),W/2,cy+104,'900 14px '+F_MONO,'#8af0d4');
  textC('ONE FILE · VANILLA JS · CANVAS',W/2,H-20,'9px '+F_MONO,'rgba(140,170,190,.55)');
  ctx.restore();
}

function drawPause(){
  ctx.fillStyle='rgba(5,8,16,.6)';ctx.fillRect(0,0,W,H);
  textC('PAUSED',W/2,H/2-16,'900 44px '+F_DISPLAY,'#eaf6ff');
  textC('P resume · M sound '+(muted?'off':'on'),W/2,H/2+26,'bold 13px '+F_MONO,'#9fb8c8');
}

function drawBtn(b){
  const hov=mx>=b.x&&mx<=b.x+b.w&&my>=b.y&&my<=b.y+b.h;
  b.hover=hov;
  const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
  if(hov){g.addColorStop(0,'#fff6d8');g.addColorStop(.5,'#ffd76a');g.addColorStop(1,'#d99a2b');}
  else{g.addColorStop(0,'#ffe9a8');g.addColorStop(.5,'#ffc93d');g.addColorStop(1,'#b3791d');}
  ctx.save();
  if(hov){ctx.shadowColor='#ffd24a';ctx.shadowBlur=22;}
  ctx.fillStyle=g;rr(b.x,b.y,b.w,b.h,14);ctx.fill();
  ctx.restore();
  ctx.strokeStyle='rgba(255,255,255,.6)';ctx.lineWidth=1.4;rr(b.x,b.y,b.w,b.h,14);ctx.stroke();
  textC(b.label,b.x+b.w/2,b.y+b.h/2+1,'900 19px '+F_DISPLAY,'#22160a');
}

function drawEnd(win){
  buttons.length=0;
  ctx.fillStyle=win?'rgba(10,9,4,.8)':'rgba(8,5,12,.8)';ctx.fillRect(0,0,W,H);
  if(win){
    const g=ctx.createRadialGradient(W/2,270,20,W/2,270,320);
    g.addColorStop(0,'rgba(255,205,80,.22)');g.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  }
  ctx.save();
  ctx.shadowColor=win?'rgba(255,210,74,.65)':'rgba(255,93,109,.6)';ctx.shadowBlur=24;
  textC(win?'SECTOR CLEARED!':'GAME OVER',W/2,252,'900 52px '+F_DISPLAY,win?'#ffd24a':'#ff5d6d');
  ctx.restore();
  textC(win?'all '+bricks.length===0?'72':'72'+' bricks destroyed':'the wall wins… this time',W/2,296,'12px '+F_BODY,'#9fb8c8');

  textC('FINAL SCORE',W/2,352,'bold 11px '+F_MONO,'#7f9db3');
  textC(String(score).padStart(6,'0'),W/2,388,'900 40px '+F_MONO,'#eaf6ff');
  if(win&&lastBonus>0)textC('+'+lastBonus+' LIFE BONUS',W/2,422,'bold 13px '+F_MONO,'#8af0d4');
  if(score>=best&&score>0)textC('★ NEW BEST ★',W/2,(win?448:430),'900 15px '+F_DISPLAY,'#ffd24a');
  else textC('BEST '+String(best).padStart(6,'0'),W/2,(win?448:430),'bold 12px '+F_MONO,'#7f9db3');

  const b={x:W/2-120,y:482,w:240,h:54,label:'▶ PLAY AGAIN',cb:startPlay};
  buttons.push(b);drawBtn(b);
  textC('press R or Enter',W/2,560,'11px '+F_MONO,'#7f9db3');

  const t={x:W/2-110,y:586,w:220,h:26,label:'',cb:()=>newGame('demo')};
  buttons.push(t);
  const hov=mx>=t.x&&mx<=t.x+t.w&&my>=t.y&&my<=t.y+t.h;
  t.hover=hov;
  textC('…or press T for the title screen',W/2,t.y+13,'bold 12px '+F_MONO,hov?'#bfeaff':'#6c8698');
}

function draw(){
  ctx.setTransform(viewScale,0,0,viewScale,0,0);
  drawBackground();
  ctx.save();
  if(shake>0)ctx.translate((Math.random()*2-1)*shake*.5,(Math.random()*2-1)*shake*.5);
  drawRails();
  drawBricks();
  drawDrops();
  drawPaddle();
  drawBalls();
  drawParticles();
  drawTexts();
  ctx.restore();
  drawHUD();
  drawServeHint();
  if(mode==='demo')drawTitle();
  else if(mode==='win')drawEnd(true);
  else if(mode==='over')drawEnd(false);
  if(mode!=='demo'&&mode!=='play')buttons.length&&0; // keep buttons for end screens
  if(paused&&mode==='play')drawPause();
  if(redFlash>0){
    const g=ctx.createRadialGradient(W/2,H/2,H*.25,W/2,H/2,H*.72);
    g.addColorStop(0,'rgba(255,60,60,0)');
    g.addColorStop(1,'rgba(255,50,50,'+(redFlash*.45).toFixed(3)+')');
    ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  }
}

/* ================= INPUT ================= */
function toLocal(e){
  const r=canvas.getBoundingClientRect();
  mx=(e.clientX-r.left)/r.width*W;
  my=(e.clientY-r.top)/r.height*H;
}
addEventListener('pointermove',e=>{
  toLocal(e);
  if(mode==='play'&&!paused)paddle.x=clamp(mx,paddle.w/2+6,W-paddle.w/2-6);
  let hov=false;
  for(const b of buttons)if(b.hover)hov=true;
  canvas.style.cursor=hov?'pointer':'crosshair';
});
canvas.addEventListener('pointerdown',e=>{
  e.preventDefault();
  ensureAudio();toLocal(e);
  if(mode==='demo'){startPlay();return;}
  if(paused){paused=false;return;}
  if(mode==='play'){launchAllStuck();return;}
  for(const b of buttons){
    if(mx>=b.x&&mx<=b.x+b.w&&my>=b.y&&my<=b.y+b.h){b.cb();return;}
  }
});
addEventListener('keydown',e=>{
  const k=e.key;
  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(k))e.preventDefault();
  ensureAudio();
  if(mode==='demo'){startPlay();return;}
  if(k==='m'||k==='M'){muted=!muted;toast(muted?'SOUND OFF':'SOUND ON','#8fe8de');return;}
  if((k==='p'||k==='P')&&mode==='play'){paused=!paused;return;}
  if(paused)return;
  if(mode==='over'||mode==='win'){
    if(k==='r'||k==='R'||k===' '||k==='Enter')startPlay();
    else if(k==='t'||k==='T')newGame('demo');
    return;
  }
  if(k==='ArrowLeft')keys.l=true;
  if(k==='ArrowRight')keys.r=true;
  if(k===' '||k==='ArrowUp'||k==='Enter')launchAllStuck();
});
addEventListener('keyup',e=>{
  if(e.key==='ArrowLeft')keys.l=false;
  if(e.key==='ArrowRight')keys.r=false;
});

/* ================= LOOP ================= */
newGame('demo');
let last=performance.now();
function frame(now){
  const dt=Math.min(.033,(now-last)/1000);
  last=now;
  update(dt);
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (26,051 tokens)
The user is asking for a polished Breakout game in a single HTML file. Let's carefully plan all the requirements:

1. **Paddle controls**: Mouse + left/right arrow keys.
2. **Bounce angle from paddle**: Depends on hit position — center = vertical, edges = sharp angles.
3. **At least 6 rows of bricks**: Mix regular (1 HP) and tough bricks (2 HP, showing cracks after first hit).
4. **Power-ups**: Some bricks visibly display a power-up icon; when destroyed, the power-up drops; if caught, either a wide paddle (10 seconds) or multiball (3 balls).
5. **No tunneling**: Use continuous collision detection / substepping — subdivide ball movement into small steps, and do proper AABB collision detection with per-axis resolution.
6. **Particles when bricks break**.
7. **3 lives, score display**. Game-over and victory screens and restart.
8. **Self-playing demo mode**: The game must start behind a "Click or press a key to start" message in demo mode where a simple AI plays. Since it's displayed in an iframe, the keyboard only works after a click. So auto-play demo with AI paddle tracking the ball.

Important rendering consideration: Must look correct at 1280×800, and adapt down to 375px. Use a responsive canvas scaling approach: fixed internal resolution (e.g. 800×600 or something more portrait-leaning), scaled to fit. Let's pick internal game size around 720×860? Actually, Breakout is often more wide than tall, but at 800px viewport height, we want the game to fit. Let's go internal resolution 760×880? Hmm, at 1280×800 viewport, available height is about 800 minus header. Let's pick 640×800 (portrait-leaning, 4:5) internal game resolution. Fits well at 800px tall viewport, and looks good on mobile 375px wide too (width-constrained).

Actually the classic Breakout is wide. But with 800px viewport height, HUD and margins... let's go 640×800. Scale with CSS to fit container while maintaining aspect ratio. Use `image-rendering` default. Canvas internal resolution can be multiplied by devicePixelRatio for sharpness.

Architecture:

- Fixed logical size: W=640, H=800.
- Brick area: rows start around y=90 (leaves space for HUD in-canvas or as HTML). Let's draw HUD on canvas top bar: score, lives, active powerup timers.
- Brick grid: 8 cols × 7 rows? Requirement is at least 6 rows. Let's go 7 rows × 9 cols... Width 640: side padding 20, brick width = (640-40-(cols-1)*gap)/cols. cols=9, gap=6: (600 - 48)/9 = 61.3. Hmm. Let's do cols=8, gap=6: (600-42)/8 = 69.75. Whatever. Brick height ~26.

Layout: HUD area top ~54px, brick rows from y=100. 7 rows * (26+6) = 224 → bricks end around y=324. Paddle at y ~ 720. Nice playfield.

Ball physics:
- Speed ~360 px/s, gradually increases a bit over time? Keep it simple: base speed 340, slightly increases per paddle hit up to a cap.
- Substepping: Move ball in steps where max movement per substep is less than ball radius (or less than brick wall thickness). Use fixed timestep update with an accumulator, e.g. dt fixed 1/120s, plus substep by max distance. More simply: subdivide each frame's movement into N = ceil(dist / 6) substeps, doing collision detection each substep. Ball radius 7, bricks ~26 thick → substep of max 6px is safe.

Ball-brick collision: treat ball as circle vs AABB. Find nearest point, determine penetration axis; reflect velocity on the appropriate axis. For robustness, check overlap with slightly expanded AABB and decide normal by minimum penetration. Iterate bricks, handle one collision per substep (or allow multiple), mark brick hit.

Ball-paddle collision: circle vs rectangle; on hit, compute the hit position relative to paddle center, offset ∈ [-1,1], angle = offset * maxAngle (maxAngle ~60-75° from vertical). Speed = max(speed, base). Ensure ball moves upward. Also only when ball is moving downward.

Never pass through paddle: same substep guarantee; paddle is 14px thick, substep ≤ 6px so fine. Also clamp: if ball crosses paddle plane within paddle x-range... substepping is enough.

Ball lost: y - r > H → lose ball; if balls.length becomes 0 → lose life; reset ball on paddle (sticky? or auto-launch after delay). Demo mode AI auto-launches.

Lives: 3. Show with little heart/ball icons.

Power-ups:
- Assign to some bricks at level construction (e.g. ~14% chance, guarantee mix of both types). Icon drawn on brick: "W" inside circle for wide, 3 dots / "M" for multi. Better: draw icon glyphs: wide = horizontal arrow ⬌, multi = three little circles.
- Falling: 90px wide rounded rect capsule with icon, falls with velocity ~150 px/s, catch with paddle → apply.
- Wide: paddle width × 1.6 for 10 seconds; show timer in HUD; re-catch refreshes duration.
- Multiball: spawn 2 extra balls (3 total from this... "multiball (3 balls)" — spawn 2 more balls at angles from current ball position, or from paddle). Let's spawn from paddle top at angles -30°, 0°, +30°... More simply: on catch, spawn 2 new balls at paddle center moving up-left and up-right, with existing balls continuing. Cap total balls at say 6.

Cracked bricks: 2 HP; after first hit, draw crack lines (pre-generated crack patterns per brick, or deterministic pseudo-random lines). Tinted differently (darker/stronger). Brick colors per row — I want a distinctive palette. Let's pick a retro-futurist "CRT arcade" theme: deep space navy-teal background with scanlines and starfield, bricks in a warm sunset gradient ramp (amber → coral → magenta), cyan ball glow. Hmm, that's leaning too neon; let me diversify: rows each have their own hue: e.g. top row red/orange family, transition to teal/green at bottom, plus cream accents. Classic Breakout vibe but modern.

Theme idea: "SOLAR BREAK" — deep-space plum/ink background (#171226?), subtle starfield and drifting nebula gradient; bricks: a spectrum from hot coral, tangerine, gold, lime, teal, sky per row. Tough bricks: steel/slate with metallic sheen and rivets, crack overlay when damaged. Power-up icons: cyan capsule for wide, magenta capsule for multi.

Typography: canvas text uses system fonts only (no external fonts). For HUD headings, use a chunky display stack like `Impact, 'Arial Black', sans-serif`, and for body/HUD numbers `'Courier New', monospace`? Pair the display feel with monospace for scores. Fine without external fonts.

Visual polish:
- Starfield parallax background (two layers, slow drift).
- Screen shake on brick break (small) and life loss (larger).
- Ball trail (store recent positions, draw fading circles).
- Particle burst on brick destruction (colored shards + sparks).
- Flash effect on brick hit (non-destroyed) (brief white overlay).
- Paddle with glow, thruster-like gradient.
- Walls: side rails with subtle glow lines.
- Combo? Could add small score popups (+points floating text). Nice touch, cheap.
- Sounds: use WebAudio for beeps? Adds polish; requires user gesture — but the demo mode starts without input. AudioContext creation before gesture is suspended; can resume on first input. Add small synth sounds guarded by try/catch, only after user interaction (start). Also add a mute button. Simple but rich. Sounds: paddle hit (soft blip), brick break (higher blip with pitch per row), powerup (arpeggio), life lost (descending sweep), victory (arpeggio). Use oscillator + gain envelope. Low volume.

Game state:
- `demo` (AI auto-play, overlay title + "Click or press any key to start"),
- `playing`,
- `ballOnPaddle` (serve state) — maybe integrated: after life loss, ball sticks to paddle and auto-launches after 1s (player can also launch by click/space? Requirement is mouse + arrow; click to launch might conflict... Use Space/Up/click for launch; but keyboard only works after iframe click anyway. In demo AI launches automatically.)
- `gameover`, `victory` — overlay screens with restart button (canvas-drawn button; clickable) and keyboard (Enter/R/Space).

Overlay screens drawn on canvas for consistency. Restart resets everything.

Demo AI: paddle moves toward predicted ball x (nearest descending ball), clamped speed. Imperfect: add slight sine-wave error, and react only to the lowest ball. Also AI auto-launches serve. AI can also catch power-ups somewhat (just chasing ball will do). Demo runs behind title overlay (semi-transparent so game is visible). Title: game name large, "Click or press any key to start", maybe controls hint too. Demo score is displayed but reset on start.

Input:
- mousemove on canvas → paddle x = mouse x (scaled). Also touch move for mobile.
- Arrow keys: velocity-based movement while held (e.g. 520 px/s). Track keydown/keyup.
- keydown any key / click during demo → start real game (reset).
- During gameover/victory: click on restart button, or press R/Space/Enter → restart.
- P during play pauses? Nice extra. Keep it small.

Canvas scaling: container centers canvas; CSS computes size to fit while maintaining aspect: use JS resize handler to set canvas.style.width/height, internal buffer = W*dpr, H*dpr. Actually more simply: fix internal to W,H (logical), set canvas.width = W*dpr etc., ctx.scale(dpr,dpr), CSS size computed to fit window with letterbox. Page background around is dark with subtle vignette maybe.

Mouse coords: map clientX relative to canvas rect by rect.width → logical W.

Physics details to avoid tunneling:
```
function moveBall(b, dt) {
  let dist = speed*dt; steps = Math.max(1, Math.ceil(dist / 4)); // max 4px per step
  for i in steps: 
    b.x += vx*dt/steps; b.y += vy*dt/steps;
    collideWalls(); collidePaddle(); collideBricks();
    if lost break;
}
```
At 60fps with speed cap ~640 px/s, dist per frame ≈ 10.7 → 3 substeps. Good.

Brick collision (circle vs AABB):
```
closestX = clamp(b.x, brick.left, brick.right); closestY = clamp(b.y, brick.top, brick.bottom);
dx = b.x-closestX; dy = b.y-closestY; if dx*dx+dy*dy <= r*r → hit.
```
Resolve: if ball center is outside the box, determine normal from (dx,dy). If center is inside box (rare due to substepping), fall back to per-axis overlap check using previous position: if prevX was horizontally outside → reverse vx, else reverse vy. Implementation: compute overlap depths on each axis: pushOut based on minimum axis.

Simpler robust approach: after detecting overlap, compute penetration on x and y:
```
if (|dx| > |dy|) reflect x else reflect y
```
where dx,dy is vector from closest point. If both ~0 (center inside), use previous-position heuristic. Then push ball out along normal to prevent sticking.

Also handle multiple brick hits per substep: after resolving one, continue loop but only resolve one per substep to avoid double-reversal weirdness; with 4px substep that's fine.

Ball-brick hits while moving... standard.

Hit brick: hp--; flashTimer; spawn sparks; if hp<=0 destroy: score += points (row-based? normal 50, tough 100; plus combo multiplier? Let's keep it simple: base points per brick; combo increases when hitting bricks without touching paddle — nice juicy touch with floating text. Adds polish; implement lightly).

Powerup drops: on brick destroy, if brick.power exists → spawn falling capsule.

Victory: all bricks with hp destroyed (count remaining). Show victory overlay with score, "Press R or click to play again".

Life loss: big screen shake, red flash vignette, lives--, if lives==0 → game over, else serve state (ball on paddle, auto-launch after 1.2s or player input).

Serve: ball follows paddle; show "launch" hint in player mode; auto-launch timer ~2s (also auto in player mode? spec doesn't say; typically player launches. But requirements say game must be self-playing demo; once player takes over they can launch with mouse... click already used for start. Use click/space to launch, plus auto-launch after 2.5s fallback so game never stalls.) Do this: after 1.8s if input not happened, auto-launch (works for both modes), plus immediate launch on click/space/up. Hmm, click-to-launch may misfire right after "click to start" click — that click is already consumed by the transition; next click launches, fine.

Wide paddle: basePaddleW=104, wide=168. Animate width smoothly (lerp) toward target. Timer shows as a small bar under HUD.

HUD (canvas top bar): Left: SCORE and number; Center: level name "SECTOR 7"?? one level; Right: life icons. Also active power-up timer chip. Combo indicator.

Edge cases:
- Balls array; life lost only when balls.length===0.
- Cap multiball max at 6.
- Prevent near-horizontal trajectory: enforce minimum |vy| component after paddle bounce and brick bounce? Paddle sets angle so fine. After brick bounce, if |vy| too small nudge. Clamp angle: ensure |vx|/speed <= 0.96 maybe. Add: after paddle bounce always fine; after wall bounce fine. Slightly randomize brick bounce to avoid loops: add small jitter to normal reflection (rotate ±2°)? Careful: keep deterministic-ish but small jitter is good for preventing infinite horizontal loops. Also enforce minimum vertical speed: if |vy| < 0.25*speed, set sign and adjust while preserving speed. Let's implement `sanitizeVelocity(b)`.

Particles: array of {x,y,vx,vy,life,maxLife,color,size,type(shard/spark)}; gravity for shards; additive blend for sparks. Cap ~400.

Floating text: {x,y,text,life,color}.

Screen shake: shake magnitude decays; apply via ctx.translate.

Starfield: pre-generated stars with parallax; twinkle via sin.

Scanlines: subtle CSS overlay gradient on canvas container? or draw cheaply: skip in-canvas scanline draw; CSS overlay div with repeating-linear-gradient, pointer-events none. Good CRT feel, cheap. Slight vignette via CSS radial gradient too.

Outer page: dark background with radial glow, centered canvas, title above? Everything is inside canvas (HUD). Keep page minimal: full viewport, canvas centered with glow shadow. Small footer hint below canvas ("Move: mouse / ← → · Launch: click or space") — but canvas at 800 height fits viewport exactly; use flex center, and canvas scales to min(availW, availH*aspect). Draw hint inside canvas serve state / title instead.

Color palette. Let's define:
- Background deep: #0e0b1e → #1a1035 gradient; stars white/cyan small.
- Walls: subtle line #3b2f63 with glow #7f6bff? Hmm violet-heavy. Alternative palette direction: dark petrol (#071a1d) with sunset bricks. Dark teal/ink background (#0b1620 → #10242e), warm arc of brick colors:
  Row colors (7 rows):
  1 #ff5d5d (coral red)
  2 #ff8f3f (orange)
  3 #ffc23e (amber)
  4 #b8e04a?? maybe #9ad64c (lime)
  5 #3fd6a5 (mint/teal)
  6 #3fb9ff (sky)
  7 #8f7bff (periwinkle) — maybe too many hues; classic rainbow Breakout is exactly this though, and this subject (arcade Breakout) calls for it. Good.

- Tough brick: base #5a6b7d steel with bright top edge; cracks darker. Or gold-rimmed darker "alloy" brick. Let's do slate-blue steel #46586e with top sheen; on damage show crack lines + darker tint. Differentiates clearly from colored bricks.

- Paddle: warm white/cyan gradient with glow; wide power-up tinted gold? Nice: paddle glows gold while wide.
- Ball: white core with cyan glow.
- Powerup capsule: wide = gold capsule with ⬌ arrow icon; multi = magenta capsule with triple-dot icon.

Font: HUD uses bold `'Arial Black', Impact, sans-serif` for headings; score digits monospaced-ish `'Courier New'` bold. Title screen big display: e.g. "NEON BRICKS"? Name: "PRISM BREAK"? Cute. Or "STARFALL BREAKER". Let's go with "PRISM BREAK" — matches rainbow bricks. Subtitle "a brick-breaker arcade".

Title overlay drawn on canvas: darkened backdrop (playing demo behind is visible), big title with layered shadows, subtitle, pulsing "CLICK OR PRESS ANY KEY TO START", controls hint line. Since iframe keyboard needs focus, message emphasizes click.

Game-over overlay: "GAME OVER", final score, best score of session (in-memory only), restart button (canvas-drawn rounded rect) + "or press R". Victory: "SECTOR CLEARED!" with similar.

Restart button hit-testing: store button rect, on click in those states check.

Also need clear affordance for "restart option": draw a button.

Structure of code:

```js
const W=640,H=800;
const state = { mode:'demo', score, lives, balls[], bricks[], particles[], drops[], texts[], combo, shake, ... };
```

Build level:
```js
rows=7, cols=9? 
```
Compute: W=640, padding 22, gap 6. cols=9 → usable=640-44=596; brickW=(596-8*6)/9 = (596-48)/9=60.9. OK. brickH=24. top=104. 7 rows → end 104+7*30-6=308. Paddle y=728. Roomy.

Hmm, more bricks = more fun: maybe 8 rows, brickH=22, gap 6: top=100, end=100+8*28-6=318. Let's do 8 rows × 9 cols = 72 bricks. Tough bricks: pattern — randomize with seeded placement: make ~22% tough, guaranteeing distribution: pick pattern per row: even rows tougher in middle columns etc. More simply: each brick has probability 0.22 to be tough, but ensure at least X count. Random per run is fine. Powerups: assign to ~12 random bricks: mix of wide/multi: maybe 7 wide, 5 multi, placed in normal or tough bricks (icons must be visible — draw icon over brick regardless of hp). Requirement: "some bricks visibly have a power-up icon; when destroyed they drop it." So it drops when brick is fully destroyed. Fine for tough bricks too.

Points: normal 50 + rowIndex*? Keep normal=50, tough=120. Combo: +? Combo increases with each brick destroyed before paddle touch; bonus = 10*combo maybe; floating text shows total for that hit. Cap displayed combo.

Speed progression: ball speed starts 330; +6 per paddle hit up to 520 max. After multiball, balls share speed baseline? Each ball has own speed magnitude = current global speed. Save per-ball speed, use global `speedRef`.

Paddle bounce:
```
rel = clamp((b.x - paddle.x)/(paddle.w/2), -1, 1);
ang = rel * 62° from vertical;
vx = sin(ang)*sp; vy = -cos(ang)*sp;
b.y = paddle.top - r - 0.1;
```
This gives near-vertical at center, sharp angle at edges.

Ball trail: save array of last ~10 positions per ball, draw fading circles with additive.

Demo AI:
```
target = lowest ball with vy>0 ? predict x at paddleY : W/2 + wobble;
predict: t=(paddleY - b.y)/b.vy; x = b.x + b.vx*t; reflect on walls: fold into [r, W-r] by mirroring.
Move paddle toward target at speed 430 (slightly slower than perfect), with small noise.
```
AI launches serve instantly. AI catches power-ups naturally. Demo continues looping: if demo game-over or victory → just reset demo silently and continue. Nice for continuous attract mode.

Main loop: `requestAnimationFrame`, dt clamped to 1/30. Fixed substep for ball is inside move function.

Audio: create AudioContext lazily on first user input; function beep(freq, dur, type, vol, slide). Mute toggle with M key and speaker icon in HUD? Keep as: 'M' toggles, small text in hint. Only sounds after user interaction (autoplay policy). Since ctx is created on first pointerdown/keydown, demo is silent — fine.

Pause: 'p' toggles pause overlay (nice to have; cheap). Include it.

Right, "ball must never pass through bricks or paddle" — with substep 4px and min brick height 22 and paddle 16, we're safe. Walls are clamped similarly.

Also handle ball vs paddle side/underside: substep with circle-rect collision, reflect appropriately (not just top): same circle-AABB resolution as bricks. If hit from side, reverse x. Good.

Prevent ball getting stuck bouncing horizontally between walls forever: enforce minimum |vy| ≥ 0.22*speed after any bounce sanitize; adjust while preserving speed: if |vy| too low, set sign and value, recompute vx = sqrt(sp² - vy²)*sign(vx). Apply small random jitter on brick bounce ±1.5° to avoid perfect loops.

Drop catch: AABB vs paddle rect; apply effect; floating text "WIDE PADDLE!" / "MULTIBALL!"; sound.

Wide implementation: `paddle.baseW=104; paddle.wideUntil = now+10s`... use game-time seconds accumulator. Target width = wide? 168:104; paddle.w lerps toward target at 8/s. If catching wide again, refresh timer. HUD chip shows remaining seconds bar.

Multiball: on catch, take first ball (or paddle pos), spawn 2 additional at paddle top center with angles ±35°, speed = current speed; total capped 6: spawn min(2, 6-balls.length). If no ball exists (weird), spawn one.

Life icons: draw 3 small ball glyphs; lost ones are dimmed.

Score display: "SCORE 000000" padded.

In-canvas HUD layout (top 60px, above playfield but within canvas):
- y 8..52 panel: semi-transparent dark bar with bottom border glow.
- Left: "SCORE" small label, big digits.
- Center: level title "PRISM BREAK" small? Or "BRICKS LEFT: n"? Let's put combo there when active: "COMBO ×5".
- Right: lives as 3 orbs + wide timer chip if active.

Actually the top bar could be 64 tall; bricks start y=100. Walls: left/right/top borders drawn as glowing rails inside canvas edge (ball bounces at x=r and W-r, top y=r+topWall(=8?)... let's have top wall at y=64 (below HUD) so ball doesn't visually overlap HUD. So playTop=64. Ball bounce top: y - r <= playTop → y=playTop+r, vy=abs. Bricks from y=104. OK adjust: HUD bar height 64.

Bottom: ball lost when y - r > H + 20.

Paddle y = H - 60 (=740), height 16. Draw glow beneath it.

Drops fall until y > H → disappear.

Now visual rendering details:

Bricks: rounded rect (r=5), vertical gradient (lighter top), inner highlight line at top, darker at bottom; slight outer glow for colored rows with shadowBlur? shadowBlur is expensive ×72... pre-render once into offscreen canvas per brick style! Since bricks are static style, pre-render each brick color into a small canvas (colored, cracked tough, tough) and drawImage per brick. Efficient. Though per-brick flash overlay is just simple rect fill with alpha. Icons drawn on top dynamically (cheap).

Actually more simply: draw with direct fills without shadowBlur; performance fine (72 rects). Avoid shadowBlur in hot paths except ball and paddle (few). OK direct drawing.

Cracks: draw 3-4 jagged polylines in dark semi-transparent, deterministic per brick (seeded by index). Pre-compute crack paths per brick.

Particles: on destroy spawn 14 shards (brick color, gravity, spin rects) + 8 sparks (additive circles). On hit (tough first hit): 6 sparks.

Floating text rises and fades.

Screen flash on life lost: red vignette alpha decay.

Victory confetti: burst particles from top.

Background rendering: gradient sky, nebula radial blobs (pre-rendered offscreen with low alpha), starfield twinkle, maybe distant grid floor too? Keep: gradient + stars + subtle nebula.

Walls: draw side rails: vertical gradient lines with slight glow, from playTop to bottom.

Title screen text: draw big "PRISM BREAK" with a chunky font, slight perspective / layered color offset (chromatic aberration: draw with ±2 offsets in cyan/red) — bold and intentional. Behind: demo gameplay running (dimmed with rgba(6,8,18,0.55)).

Buttons: rounded rects with gradient, hover state via mousemove tracking (cursor pointer).

Cursor: hide default over canvas during play? Use crosshair maybe. `canvas {cursor: none}`? but then button hover needs pointer. Simpler: cursor: crosshair.

Edge: mouse control — paddle follows mouse x directly (snappy). Also allow arrow keys; if keys used, paddle moves and mouse remains authoritative next move. Fine.

Resize handling: compute scale = min(availW/W, availH/H); canvas.style.width = W*scale. Set on load + on resize. Canvas backing store: W*dpr... but when CSS scales larger than logical, slight blur; better: backing = W*scale*dpr for sharpness, ctx.setTransform(scaleAll). Implement resizeCanvas():
```
const rect fit; canvas.style.width/height; canvas.width = round(W*s*dpr); ctx transform via ctx.setTransform(s*dpr,0,0,s*dpr,0,0) at start of each frame.
```
Simplest: set transform at start of each frame: `ctx.setTransform(pxScale,0,0,pxScale,0,0)` where pxScale = canvas.width / W. All drawing in logical coords.

Mouse map: (e.clientX - rect.left)/rect.width * W.

Touch: touchmove → same; touchstart also acts as click (start/launch). Add `{passive:false}` to preventDefault for scroll.

Now demo → start: on click or keydown (any) when mode==='demo' → startGame() (reset for real, mode='playing', serve). Also ensure audio init/resume.

On gameover/victory: click restart button or press R/Enter/Space → back to ... startGame() directly (playing), not demo. Also "Esc" to return to title? Optional; skip.

Keyboard handling only works when iframe has focus — per the requirement, message prompts a click.

Pause only during playing with 'p'; show overlay "PAUSED — press P to resume". Game time accumulation: use dt-based timers; while paused skip updates but still draw.

Combo resets on paddle touch or life loss.

Score popup on brick: show "+" points (+combo bonus).

Also add a subtle "ball speed" indicator? Skip.

Alright let me code carefully.

Constants:
```
W=640,H=800, HUD_H=64, PLAY_TOP=HUD_H(=64), 
ROWS=8, COLS=9, BRICK_GAP=6, SIDE_PAD=22,
brickW = (W - 2*SIDE_PAD - (COLS-1)*GAP)/COLS = (640-44-48)/9 = 548/9 = 60.89
brickH=22, BRICK_TOP=104
PADDLE_Y=H-56=744, PADDLE_H=15
BALL_R=7
BASE_SPEED=330, MAX_SPEED=560
```

Brick data: {x,y,w,h,hp,maxHp,color(row),tough,power:null|'wide'|'multi',flash,shakeT? , crackSeed, alive}.

Level generation:
```
for r in 0..7, c in 0..8:
  tough = pattern: (r%2==1 && (c%4==1||c%4==2))? that gives too many maybe.
```
Let's do a designed-ish randomness: toughChance 0.24, using seeded RNG (mulberry32 with random seed per game). At least ensure: fine.

Powerups: pick 12 unique brick indices (of 72): type: first 6 'wide' then 'multi' shuffled. Ensure not all tough? Works either way.

Ensure at least 1 multi and 1 wide: yes by construction.

Remaining bricks count for victory: total hp? Victory when all bricks dead. Track `bricksLeft`.

Drop object: {x,y,w:34,h:18,type,vy:150, wobblePhase}.

Catch test: drop.y+h/2 >= paddleTop && overlaps x → apply. Also passing bottom → remove.

Wide effect: `wideTimer` seconds countdown; if >0 → wide.

Multiball spawn:
```
function spawnBallsFromPaddle(n){ angles [-0.6,0,0.6]? Actually 3 balls: if there's an existing ball, add 2 angled ones. Spawn at paddle center top.
```
Requirement: "multiball (3 balls)" — spawn so that 3 balls are in play. Implementation: let need = Math.min(3 - balls.length, 2) if balls.length>=1 else 3; spawn need balls from paddle top at distributed angles [-0.5, 0, 0.5] (take subset). Cap 6 total? if balls.length>3 then just add 2 (still "multiball"). Let's do: add up to 2 new balls; if balls empty (shouldn't be possible since caught during play), add 1 centered.

Serve state: ball stick: ball.stuck=true; sits on paddle center offset; launch: set stuck=false, velocity is upward slightly random angle (angle = (rand-0.5)*0.6). Player hint text: "CLICK / SPACE TO LAUNCH" pulsing; auto-launch after 2.0s.

Life reset: clear drops? Keep drops? Classic: clear drops and effects on life loss (wide timer reset). wideTimer=0, remove drops, keep particles. Reset ball speed to BASE.

Game reset for demo vs play: same function `newGame(isDemo)`.

Score popups and combo:
```
combo++, bonus = (combo-1)*10, pts = brickPoints + bonus, score+=pts, floating text at brick "+pts", if combo>=3 show text in HUD center.
```
Reset combo on paddle contact.

Now render order:
1. Clear + background gradient + nebula + stars (slow parallax drift, subtle reaction to shake? no).
2. Apply shake translate.
3. Wall rails.
4. Bricks (with flash overlay, icons).
5. Drops.
6. Particles behind? Mix: shards normal, sparks additive — draw after bricks.
7. Paddle + ball trails + ball.
8. Floating texts.
9. HUD bar.
10. Serve hint / overlays (title/pause/gameover/victory).
11. Red damage vignette, scanlines via CSS.

HUD design: bar background rgba(10,12,26,0.72) with 1px bottom line gradient cyan→magenta? Let's do: bottom border 2px gradient teal→gold. Left: small "SCORE" letter-spaced (use fillText, manual spacing? just use small caps font), big number 'bold 22px "Courier New"'. Right: LIVES orbs (circle radius 6, filled warm white, dim when lost). Wide chip: rounded rect with gold border, "WIDE" + shrinking bar. Center: combo "COMBO ×N" if combo>=2 with pulse scale.

Also "BEST" score? Track session best (variable). Show on gameover/victory + small at title. Nice.

Title overlay content:
- Big "PRISM BREAK" chromatic
- Tagline "SMASH · CATCH · SURVIVE"
- Pulsing "▶ CLICK OR PRESS ANY KEY TO START"
- Controls line: "MOUSE or ← → to move · catch the capsules"
- Small legend of power-ups: mini wide capsule icon + "WIDE 10s", multi icon + "MULTIBALL". Good info display.

Overlay background: rgba(7,9,20,0.55) + radial spotlight.

Gameover overlay: "GAME OVER" reddish; score; best; button "PLAY AGAIN [R]".
Victory: "SECTOR CLEARED!" gold; score + bonus? Add life bonus: +500 per remaining life, applied at victory (show breakdown). Button "PLAY AGAIN".

Button hover: track mouse pos; cursor pointer via canvas.style.cursor switch.

Sound design (WebAudio):
```
function blip(f0,f1,dur,type,vol)
Paddle: 220→180? square soft 0.05
Brick: freq base per row: 320 + (7-row)*40, destroyed: quick 2-tone; tough hit: metallic 140 with noise? just use triangle.
Powerup catch: arpeggio 523,659,784 quick.
Launch: 300→500 sweep.
Life lost: 400→80 saw 0.5.
Game over: descending.
Victory: little fanfare.
```
Keep volume around 0.12. Master gain. Mute flag 'm'.

Since audio context might be suspended until gesture: create lazily on first gesture; all sounds check existence of ctx and state==='running' (otherwise resume).

Let me write the code in full carefully. Aim for a well-organized ~700-900 lines of clean vanilla JS.

Also confirm: zero console errors; guard divisions; no storage APIs.

Canvas text font: define helper `font(size, weight, family)`. Families: display: `'Arial Black','Arial Bold',Impact,sans-serif`; mono: `'Courier New',monospace`; body: `Verdana, Geneva, sans-serif`. Chunky Arial Black display + Courier mono digits + Verdana body — a distinctive pairing without needing external fonts.

Ball trail implementation: each ball has trail: [], push {x,y} each frame, max length 9; draw with decreasing alpha and radius. Use `globalCompositeOperation='lighter'` for glow pass then reset.

Paddle draw: rounded rect, gradient (#e8f6ff top → #9fd8ff? or warm). Let's do paddle as a sleek cyan-white with a glow, gold while wide. Add small side fins. Underglow ellipse.

Ball draw: radial gradient white core → cyan edge; shadowBlur=12 cyan (only ≤6 balls so OK).

Drops draw: capsule rounded rect with bobbing rotation, icon:
- Wide: two arrows ‹ › or horizontal double arrow drawn as lines/triangles; color gold #ffcf4d; label maybe "W".
- Multi: 3 small balls dots in triangle; color magenta #ff5da2. Label "M"? Icons sufficient. Add small glow pulse.

Slight drop rotation wobble: rot = sin(t*6+phase)*0.15.

Icon over brick for bricks with power: draw a mini glyph: white circle badge with colored ring + glyph in center of brick. To keep readable on 61×22 bricks: badge radius 8. Draw: dark circle bg alpha .55, ring color (gold/magenta), glyph: wide = "↔" made from 2 triangles + bar; multi = 3 dots. Small but readable.

Cracked overlay: pre-computed crack segments per tough brick: from random point near center to edge, 2-3 polylines with jitter, seeded by index: deterministic via simple rng.

Flash on hit: brick.flash=1 → decays 6/s; draw a white alpha overlay = flash*0.8.

Brick death: also small "bump" scale? Skip; particles suffice.

Shake: shake += amount; each frame offset = (rand-0.5)*shake; shake *= exp(-dt*8) or shake = max(0, shake - 40*dt). Use decaying amplitude: shakeMag; ox = (Math.random()*2-1)*shakeMag...

Life loss: shake 14, redFlash=0.8 decay.

Wall bounce particles? Small sparks at wall hit — nice touch, cheap: 3 sparks.

Paddle hit: small sparks + squash animation: paddle.squash=1 decay, scaleY 1 - 0.3*squash? draw via height scaling. Cute juice.

Now demo AI prediction with wall fold:
```
function predictX(b){ if(b.vy<=0) return null; t=(PADDLE_Y-BALL_R- b.y)/b.vy; x = b.x + b.vx*t; fold into [R, W-R] mirror: period = 2*(W-2R); x = (x - R) mod period; if x<0 x+=period; if x> period/2 → x = period - x; return x + R; }
```
Pick the ball with minimum (PADDLE_Y - y)/vy (soonest arrival) among balls with vy>0.

AI paddle speed: 380 px/s + error: target += sin(time*1.7)*10. If no ball, or all ascending: drift toward W/2.

AI launch: stuck ball auto-launches after 0.8s.

Also AI maybe slightly worse: when ball high up, AI moves toward center bias. Keep simple; demo rarely loses life but eventually possible — either way; on demo end (gameover/victory) → newGame(demo) silently.

Note: during demo, HUD shows demo score — fine, and title overlay covers the top partially? overlay covers entire screen with semi-transparent; game visible behind. HUD visible too — good, shows score/lives working.

Keyboard: on iframe window keydown: keys object; if mode is demo → start; if playing: arrows set flags, space/up launches stuck ball, p pauses, m mutes, r restarts on gameover/victory.
Prevent default on arrows/space to avoid scrolling (page doesn't scroll but just in case).

Mouse: mousemove → targetX (immediate). Click: if demo → start; if playing and stuck → launch; if gameover/victory → check button rect → restart; also if hover button, click restart. Escape? skip.

Anywhere click on gameover also restarts? Might be too eager; button only + R key. But user friendliness: also allow clicking anywhere? Let's use button only, plus big text "CLICK THE BUTTON or press R". Hmm keep button.

Cursor management: default crosshair; when hovering button pointer.

Now victory life bonus: at victory, add lives*500 and show "+1500 LIFE BONUS".

Best score: update at end of game and during play if score>best.

Now let me code the collision precisely.

```js
function collideBricks(b){
  for(const br of bricks){
    if(!br.alive) continue;
    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 > BALL_R*BALL_R) continue;
    // hit
    let nx, ny;
    if(d2 > 0.0001){ const d=Math.sqrt(d2); nx=dx/d; ny=dy/d; }
    else {
      // center inside: use velocity to decide axis by minimum penetration
      const left = b.x-br.x, right = br.x+br.w-b.x, top=b.y-br.y, bot=br.y+br.h-b.y;
      const m = Math.min(left,right,top,bot);
      nx = m===left?-1: m===right?1:0;
      ny = m===top?-1: m===bot?1:0;
    }
    // reflect if moving into surface
    const dot = b.vx*nx + b.vy*ny;
    if(dot < 0){ b.vx -= 2*dot*nx; b.vy -= 2*dot*ny; }
    // push out
    const pen = BALL_R - Math.sqrt(d2);
    b.x += nx*(pen+0.2); b.y += ny*(pen+0.2);
    // small jitter to prevent loops
    jitter(b, 0.02);
    sanitize(b);
    hitBrick(br, b);
    return true; // one brick per substep
  }
  return false;
}
```
For d2==0 center-inside case, pen calc uses sqrt(0)=0 → pen=BALL_R, pushing nx*BALL_R — fine.

Paddle collision similar with paddle rect; if contact resolved with ny<=-0.5 from top → apply angle mapping; else side/underside reflect. Paddle top contact: set velocity by angle formula regardless of incoming (classic). Condition: vy>0 and b.y < paddle.y + something. Use circle-rect: closest point; if hit and ball center y < paddle.y (top) and vy>0 → top bounce mapping. Else generic reflection.

```js
function collidePaddle(b){
  const p=paddle; const px=p.x-p.w/2;
  const cx=clamp(b.x,px,px+p.w), cy=clamp(b.y,p.y,p.y+PADDLE_H);
  ...
  if(d2<=r2){
    if(b.vy>0 && b.y <= p.y + 4){ // top hit
      const rel=clamp((b.x-p.x)/(p.w/2), -1,1);
      const ang=rel*MAX_BOUNCE (1.08 rad ≈62°);
      const sp=Math.max(speedNow, BASE_SPEED);
      b.vx=Math.sin(ang)*sp; b.vy=-Math.cos(ang)*sp;
      b.y=p.y-BALL_R-0.5;
      combo=0; squash; sound; sparks;
    } else { // side/underside
      resolve like brick;
    }
  }
}
```

Speed model: maintain global `ballSpeed` that increases per paddle hit (+7 up to MAX). Each ball keeps its velocity magnitude equal to ballSpeed? Simplest: after paddle hit, set ball speed to ballSpeed. Bricks don't change speed. Normalize each frame? Drift can occur after jitter/reflection; magnitude is preserved by reflection; jitter slightly rotates (preserves magnitude). Implement jitter as rotation. So magnitude constant per ball until next paddle hit sets to global. Multiball spawned balls use current global speed. OK.

sanitize: ensure |vy| >= 0.25*sp:
```
const sp=Math.hypot(vx,vy); if(sp<1)return; let vy=b.vy;
if(Math.abs(vy) < 0.25*sp){ vy = (vy>=0?1:-1)*0.25*sp; wait sign may be 0 → default -1 (up)? use sign or -1 if 0.
  const s = vy===0 ? -1 : Math.sign(vy); vy = s*0.25*sp; 
  b.vy = vy; b.vx = Math.sign(b.vx||1)*Math.sqrt(Math.max(0,sp*sp-vy*vy)); }
```
Also cap |vx| <= 0.97*sp — implied.

Walls:
```
if(b.x < BALL_R){b.x=BALL_R;b.vx=Math.abs(b.vx);wallSpark}
right similar; top: if b.y < PLAY_TOP+BALL_R → y=PLAY_TOP+BALL_R, vy=abs.
Bottom: b.y - BALL_R > H+10 → remove ball.
```

Substep update per ball:
```
const sp=Math.hypot(vx,vy); const dist=sp*dt; const n=Math.max(1,Math.ceil(dist/4));
for(i<n){ step dt/n... move, walls, paddle, bricks; }
```

Stuck ball update: x follows paddle.x + offset, y=paddle.y-BALL_R-1; launchTimer -= dt; auto-launch at 0 (demo 0.8, player 2.0).

Now power-up timer uses game time `tGame += dt` while playing.

Wide check: wideTimer>0; decrement. Target width; lerp paddle.w += (target - w)*min(1,dt*10).

Paddle movement: if keys.left/right → p.x ±= 560*dt. Mouse: p.x = mouseX directly (set a flag mouseActive; keys fine either way). Clamp p.x within [w/2+8, W-w/2-8].

Particles update: standard.

Now HUD and overlay drawing. Let me define draw functions.

Also draw subtle bricks remaining count? Show "BRICKS LEFT" small at center HUD? Center is combo. Skip.

Scanlines CSS:
```
.stage::after {content:''; position:absolute; inset:0; pointer-events:none; background: repeating-linear-gradient(0deg, rgba(255,255,255,0.03) 0 1px, transparent 1px 3px); mix-blend? none;}
```
Plus vignette radial. Wrap canvas in .stage sized to canvas.

Page background: body dark radial gradient (deep blue-green ink with subtle warm glow behind canvas):
```
background: radial-gradient(1200px 700px at 50% 20%, #14202e, #070a12 70%);
```
Canvas shadow: box-shadow 0 30px 80px rgba(0,0,0,.6), plus 1px border rgba(120,180,255,.15), border-radius 10px? rounded canvas is fine. Actually let's keep corner radius small 8 and overflow hidden.

Now, code size is fine. Let's write.

Also remember: everything must render on first paint — demo mode plays immediately with bricks, particles, etc. Title overlay semi-transparent so action visible.

Edge: rAF timestamp starts at large value; dt clamp handles.

Fonts inside canvas: use string like `'900 44px "Arial Black", Arial, sans-serif'` — canvas font parses weight 900 fine; Arial Black may ignore weight; fine.

Let me define color palette constants:

```
ROW_COLORS = [
 ['#ff5c5c','#ff8a80'] red
 ['#ff8a3d','#ffb37a'] orange
 ['#ffc93d','#ffe08a'] amber
 ['#8fd646','#c0f08a'] lime
 ['#34d3a6','#8af0d2'] mint
 ['#38b6ff','#8fd6ff'] sky
 ['#7d8cff'?]
]
```
8 rows: add a final row '#b28dff' lavender? or '#ff6fb3' pink? Rainbow order red→violet: last '#a06bff' violet. 8-row list:
1 red #ff5252
2 orange #ff8a3d
3 amber #ffc93d
4 lime #8ed14b? Use #86d24f
5 mint/teal #2fd1a3
6 sky #35b1ff
7 indigo-ish #5f7cff? Getting violet... maybe use #4f8dff cobalt
8 magenta-pink #ff5fa8? That breaks rainbow but adds pop. Or use classic Atari colors from top: firebrick, orange, yellow, green, teal, blue, purple. Let me do 8 rows: [#ff5d5d,#ff9838,#ffd23e,#a3e048,#3fd6a5,#3fb9ff,#6f8bff,#c46bff] → last two violet-ish. Hmm two violets. Use #b06bff and drop one: rows: red, orange, amber, lime, mint, sky, cobalt #5c8bff, violet #a06bff. That's a good spectrum.

Per-row points: top rows worth more: pts = 120 - row*10? Classic: higher rows more points. points = 90 - row*? Let's do: normal brick points = 100 - r*10 (r0=100 ... r7=30)? Simpler: (ROWS - r)*10 → 80..10? +combo bonus. Let's do base = (ROWS-r)*15 → 120..15 rounded. Just use `base = 100 - r*10` (100,90,...,30). Tough bricks: ×2.

Brick gradients: base color + lighter top. Compute lighter via precomputed pair [dark, light]? Let's store row color as [base, light]. Write a small helper shade(hex, amt).

shade(hex,amt): parse, add amt clamp, return rgb string. Write utility.

Tough brick colors: base '#55677d', light '#7e93ab', dark '#3a4757'. After damage (hp1): draw darker + cracks. Also slightly different shape: beveled "armor" with rivets on 4 corners (small circles).

Power badge drawn on top of brick: circle radius 8 dark bg, colored ring, glyph in center.

Glyph wide: draw horizontal arrow: line + two triangles at ends. Multi: 3 dots (radius 1.8) in triangle.

Drop capsule: w=36, h=20 rounded 10, gradient color fill (gold/magenta), white glyph, glow shadowBlur pulse. Also occasional small trail sparkle.

Now overlays:

Title:
```
dim rect rgba(5,8,16,.55)
title y=270: chromatic: draw text 3 times: offset (-3,0) color rgba(255,80,120,.85), (+3,0) cyan rgba(80,220,255,.85), main fill white gradient? Main fill '#fff' with slight shadow.
Font 900 64px 'Arial Black'
title "PRISM BREAK"
subtitle letter-spaced: "A NEON BRICK ARCADE" — draw with manual spacing? ctx has letterSpacing? modern Chrome supports ctx.letterSpacing. Use try; fallback fine. Simpler: just small font text.
pulsing prompt at y ~ 430: alpha = .55+.45*sin(t*4). "▶ CLICK OR PRESS ANY KEY TO START"
controls line y 470: "MOVE mouse · ← → LAUNCH click · space"
Power legend row y ~ 520: 2 capsules drawn + labels.
If bestScore>0, show best.
```

Pause overlay simple.

Gameover/victory overlay: darker (0.72), big word, score panel, button.

Button helper:
```
function button(label, x,y,w,h, hot) returns rect and draws; store in state.buttons for click test (rebuild each frame).
```
Click: iterate buttons and if contains → action.

Implement buttons array each frame during overlay draw; on click use last frame buttons (fine).

Sound implementation:

```js
let AC=null, muted=false, master;
function ensureAudio(){ if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); master=AC.createGain(); master.gain.value=0.16; master.connect(AC.destination);}catch(e){} } if(AC&&AC.state==='suspended') AC.resume(); }
function tone(f0,f1,dur,type='square',vol=1,delay=0){ if(!AC||muted)return; const t0=AC.currentTime+delay; const o=AC.createOscillator(), g=AC.createGain(); o.type=type; o.frequency.setValueAtTime(f0,t0); o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t0+dur); g.gain.setValueAtTime(vol,t0); g.gain.exponentialRampToValueAtTime(0.001,t0+dur); o.connect(g);g.connect(master); o.start(t0);o.stop(t0+dur+.02);}
```
Sounds:
- Paddle: tone(200,240,.06,'square',.5)
- Wall: tone(140,120,.04,'triangle',.3)
- Brick: tone(300+row*30 → ...) two quick tones: tone(f,f*1.4,.07,'square',.5)
- Tough clink: tone(160,140,.06,'sawtooth',.4)? metallic: use square 90.
- Power spawn? skip.
- Catch: arpeggio tone(520,520,.09)... use 3 tones with delay.
- Launch: tone(240,520,.12,'triangle',.5)
- Life lost: tone(320,60,.5,'sawtooth',.6)
- Game over: descending 3 notes.
- Victory: rising 4 notes.

Call ensureAudio on pointerdown/keydown (first user input).

Mute toggle 'm' + small indicator in HUD corner? Small text in HUD right corner? Might clutter; let's put hint only in title + pause. Add small "M" chip near life icons? Add small mute icon in HUD right top corner (speaker unicode?) draw a small speaker glyph. Or just display toast "SOUND ON/OFF" as floating text. Simple toast function.

OK also 'p' pause toast.

Now victory detection: bricksLeft===0 → victory once. During victory, freeze ball (clear), continue particles.

Demo reset: at newGame, mode remains 'demo'.

Also during demo, ignore life HUD? Keep it.

Timing: use `let last=performance.now()` in loop; dt=min((now-last)/1000, 0.033); if paused skip update but keep rendering + overlays.

Also ensure `speed` increase: on each paddle hit, ballSpeed=min(MAX, ballSpeed+8). Reset per life: BASE.

Another anti-tunnel: also check paddle collisions during substeps — yes, included.

Ball-ball collision? Not required; skip (classic ignores).

Now let me think about the HUD chip for wide: while active, draw at top-center below the bar: small pill "WIDE 7.3s" with bar. Position y=HUD_H+8 center. ok.

Serve hint: pulsing text at y ~ H-160: "CLICK OR PRESS SPACE TO LAUNCH" (player), demo hides.

Also draw aim tick on stuck ball? no.

Also show small "DEMO" badge during demo? Title covers it.

Also draw wall rails: x=0..4 and W-4..W gradient line + top rail at PLAY_TOP (y 64..68): draw a 4px bar with cyan soft glow + fade at ends. Balls bounce off it so visual alignment: ball top bounce at y=64+r so rail at y64.

Bricks start y=104 which gives 36px gap below rail.

Let's also add subtle background "dust" particles floating for atmosphere (12 slow drifting dots). Pre-computed, wrap.

Nebula: pre-render offscreen 640x800 with 3 radial gradient blobs (teal #1b4b57 alpha .35 top-left, warm #57341b bottom-right alpha .25, deep blue #24356e center). Draw once.

Stars: array 90 stars {x,y,z(0.3..1),tw phase}; each frame twinkle alpha = .3+.7*abs(sin(t*z*2+phase)); drift y += 2*z*dt wrap.

Alright let me write out the final HTML. Include noscript? no.

Title/meta: "PRISM BREAK — Brick Breaker".

Now, carefully write the code for correctness. Must be zero console errors. Let me pay attention to variable declarations and function ordering (function declarations hoist so fine).

Define all inside IIFE 'use strict'.

Detailed structure:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PRISM BREAK · Brick Breaker</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:...}
  body{display:flex;align-items:center;justify-content:center; ...}
  .stage{position:relative; line-height:0;}
  canvas{display:block;border-radius:10px; box-shadow...; cursor:crosshair; touch-action:none;}
  .stage::after scanlines...
</style>
</head>
<body>
<div class="stage"><canvas id="c" aria-label="Breakout game"></canvas></div>
<script> ... </script>
</body></html>
```

Resize:
```
function resize(){
 const pad=16; const availW=innerWidth-pad, availH=innerHeight-pad;
 const s=Math.min(availW/W, availH/H);
 const cssW=Math.floor(W*s), cssH=Math.floor(H*s);
 canvas.style.width=cssW+'px'; canvas.style.height=cssH+'px';
 const dpr=Math.min(2, window.devicePixelRatio||1);
 canvas.width=Math.round(cssW*dpr); canvas.height=Math.round(cssH*dpr);
 viewScale = canvas.width / W;
}
```
Each frame: ctx.setTransform(viewScale,0,0,viewScale,0,0).

Scanline overlay needs to match canvas size: .stage::after with inset 0 works because stage wraps canvas (inline-block sized by canvas). line-height 0 to avoid gap. Fine; border-radius matches.

Game code:

Constants, state vars:

```
const W=640,H=800,HUD_H=64,BALL_R=7;
const COLS=9,ROWS=8,GAP=6,SIDE=22,BW=(W-SIDE*2-GAP*(COLS-1))/COLS,BH=22,BTOP=104;
const PAD_Y=H-56,PAD_H=15,PAD_BASE_W=104,PAD_WIDE_W=172;
const BASE_SP=330,MAX_SP=560,MAX_BOUNCE=1.08; // rad
```

State:
```
let mode='demo'; // demo|play|over|win|pause? pause separate flag
let score=0,best=0,lives=3,combo=0,ballSpeed=BASE_SP;
let paddle={x:W/2,w:PAD_BASE_W,tw:PAD_BASE_W,squash:0};
let balls=[],bricks=[],drops=[],parts=[],texts=[],toasts? (use texts)
let wideT=0, bricksLeft=0, serveTimer=0, timeAll=0, shake=0, redFlash=0;
let paused=false;
let mouseX=W/2;
let keys={l:false,r:false};
let buttons=[]; // active UI buttons
let demo=true flag in mode.
```

RNG: Math.random fine (cracks stored per brick precomputed — no need determinism between frames; cracks fixed per brick object).

Cracks generation for tough bricks: array of line segments: each crack: start at random edge point? more simple: 3 polylines from center area:
```
function makeCracks(){ const segs=[]; const n=3; for(i<n){ const pts=[]; let x=BW*(0.3+Math.random()*0.4), y=BH*(0.3+Math.random()*0.4); const ang=Math.random()*Math.PI*2; const steps=3+ (Math.random()*2|0); pts.push([x,y]); for(s<steps){ x+=Math.cos(ang+ (Math.random()-.5)*1.2)*(5+Math.random()*7); y+=Math.sin(...)*(3+Math.random()*4); pts.push } segs.push(pts);} return segs;}
```
Draw with stroke rgba(10,14,20,.75) lineWidth 1.5 and slight highlight offset (alpha .15 white shifted +1). Keep dark only.

Level build:

```
function buildLevel(){
 bricks=[];
 for r,c:
   const tough = Math.random()<0.24 && !(r===0&&c===0)... just random; ensure at least 8 tough: post-process count and if <8 flip some random.
   brick={x:SIDE+c*(BW+GAP), y:BTOP+r*(BH+GAP), w:BW,h:BH,row:r,hp:tough?2:1,max:tough?2:1,tough,alive:true,flash:0,power:null,cracks:tough?makeCracks():null}
 pick powers: idx = shuffle [0..71], take 13; types: i%2? 'wide':'multi' mix → assign.
 bricksLeft = bricks.length
}
```
Ensure both types present in powers: with 13 alternating, both present.

Also avoid power bricks all in same cluster — shuffle handles.

newGame(m):
```
mode=m; score=0;lives=3;combo=0;ballSpeed=BASE_SP;wideT=0;
paddle={x:W/2,w:PAD_BASE_W,...};
buildLevel(); balls=[];drops=[];parts=[];texts=[];
spawnServeBall(); shake=0;redFlash=0;paused=false;
```

spawnServeBall():
```
balls=[{x:paddle.x,y:PAD_Y-BALL_R-1,vx:0,vy:0,stuck:true,trail:[],launchT: m==='demo'?0.9:2.0}];
```

launch(ball):
```
ball.stuck=false; const a=(Math.random()*0.6-0.3); ball.vx=Math.sin(a)*ballSpeed; ball.vy=-Math.cos(a)*ballSpeed; sound.
```

Update(dt):
```
timeAll+=dt;
if(paused) return;
// paddle target
if(mode==='demo') aiPaddle(dt); else keyboard paddle move; // mouse sets directly via event, but also smooth? direct.
Keyboard: if(keys.l) paddle.x-=560*dt; if(keys.r)+=;
Clamp
// width
const target=(wideT>0)?PAD_WIDE_W:PAD_BASE_W;
paddle.w += (target-paddle.w)*Math.min(1,dt*8);
wideT=Math.max(0,wideT-dt);
paddle.squash=Math.max(0,paddle.squash-dt*4);

// balls
for each ball idx: if stuck: x=paddle.x; y=PAD_Y-BALL_R-1; launchT-=dt; if(launchT<=0) launch;
 else moveBall(ball,dt)
remove lost balls; if(balls.length===0) lifeLost();

// drops
for d: d.y+=d.vy*dt; d.vy=min(230,d.vy+40*dt)? start 140; wobble
catch test with paddle rect: applyPower
if y>H+30 remove

// particles, texts update
// flash decay on bricks
// shake decay, redFlash decay
```

lifeLost():
```
lives--; combo=0; shake=14; redFlash=0.9; sound;
if(lives<=0){ endGame(false); } else { wideT=0; paddle.w=PAD_BASE_W (target handles it); drops=[]; ballSpeed=BASE_SP; spawnServeBall(); }
```

endGame(win):
```
if(win){ score+=lives*500; victorySound; confetti burst; mode='win'; } else { mode='over'; gameOverSound;}
best=Math.max(best,score);
```

applyPower(type):
```
wide: wideT=10; text "WIDE PADDLE!"; gold flash on paddle? ok
multi: spawnExtraBalls(); text "MULTIBALL!"
sound arpeggio; score+=25? maybe. Add 25 points.
```

spawnExtraBalls():
```
const need = balls.length===0?1: Math.min(2, 6-balls.length);
angles = [-0.55,0.55] or single 0 if 1
for: balls.push({x:paddle.x,y:PAD_Y-BALL_R-2,vx:sin*sp,vy:-cos*sp,...trail:[]})
```
Note existing balls continue; total typically 3 if 1 before.

hitBrick(br):
```
br.hp--; br.flash=1; 
if(br.hp<=0){ destroy: alive=false; bricksLeft--; combo++; const pts=(br.tough?200:100-br.row*10) + (combo-1)*10; score+=pts; addText(br center,'+'+pts,row color); shards+sparks particles; shake+=2.5; sound; if(br.power) drops.push(...); if(bricksLeft===0 && (mode==='play'||mode==='demo')) endGame(true);
} else { tough clink sound; small sparks; shake+=1 }
```

moveBall with substep as planned.

AI:
```
function aiPaddle(dt){
 let target=W/2 + Math.sin(timeAll*0.8)*30;
 let bestT=Infinity;
 for(b of balls){ if(b.stuck){target=paddle.x; break;} if(b.vy>0){ const t=(PAD_Y-BALL_R-b.y)/b.vy; if(t<bestT){bestT=t; target=predictX(b);} } }
 target += Math.sin(timeAll*1.9)*8; // imperfection
 const dx=target-paddle.x; const sp=420*dt;
 paddle.x += clamp(dx,-sp*? use maxStep=Math.min(Math.abs(dx), (mode demo? 430:0)*dt))
}
```
AI also handles launch (stuck ball auto-launch via launchT 0.9). Demo can catch drops with ball-chasing — imperfect and fine.

Predict fold:
```
function predictX(b){ const t=(PAD_Y-BALL_R-b.y)/b.vy; let x=b.x+b.vx*t; const L=BALL_R, R=W-BALL_R, span=R-L; let u=(x-L)%(2*span); if(u<0)u+=2*span; return L+(u>span?2*span-u:u); }
```

Rendering:

```
function draw(){
 ctx.setTransform(vs,0,0,vs,0,0);
 drawBG(); // gradient, nebula, stars, dust
 ctx.save(); ctx.translate(shakeX,shakeY);
 drawRails(); drawBricks(); drawDrops(); drawParticlesBelow? 
 drawPaddle(); drawBalls(); drawParticles(); drawTexts();
 ctx.restore();
 drawHUD();
 if(mode==='demo') drawTitle();
 if(paused) drawPause();
 if(mode==='over') drawEnd(false); if(mode==='win') drawEnd(true);
 if(redFlash>0) red vignette;
 // serve hint when playing & stuck
}
```

drawBG: fill gradient (precomputed LinearGradient per resize? cache once since logical size fixed — create once at init). Nebula offscreen drawn. Stars loop. Dust drift.

Star twinkle uses timeAll.

drawBricks: for alive:
```
const g cached? create gradient per row once and store (8 gradients) using createLinearGradient(0,y..y+BH) for each row — position changes... gradient coordinates are fixed at creation: row y is constant so create once after buildLevel in rowGradCache. Also cache tough gradient.
fill rounded rect r=4 path, fill gradient; highlight: rgba(255,255,255,.25) 2px line at top; darker at bottom.
tough extra: rivets 4 dots rgba(255,255,255,.25) at corners; if hp===1: darker overlay rgba(0,0,0,.28) + draw cracks.
power badge.
flash: fillStyle rgba(255,255,255, flash*.85) over rounded rect.
```
roundRect: use ctx.roundRect (Chrome supports since 99). To be safe define helper rr(x,y,w,h,r) using arcTo manually — guaranteed. Use helper everywhere.

Brick slight 3D too: pre-darken bottom 3px with rgba(0,0,0,.22).

drawDrops: rotate wobble, shadowBlur 12 color; capsule rr fill gradient; glyph; pulse scale 1+0.06*sin(t*6).

drawPaddle:
```
w=paddle.w; x=paddle.x-w/2; squash: h = PAD_H*(1-0.35*squash); y = PAD_Y + (PAD_H-h) (keep bottom)
underglow: ellipse gradient below.
body gradient: while wide gold (#ffd76a→#c98d1f) else cyan-steel (#eaffff→#7fd4ff→#2e7ea8)? Let's do: top #f2feff, mid #9fe0ff, bottom #3583b8. rounded ends (r=h/2).
Center notch light.
Edge caps brighter.
```

drawBalls: trail (lighter composite, cyan alpha), then ball: radial gradient + shadowBlur 14 '#7fe9ff'. Stuck ball same too.

drawParticles: two passes: shards normal (rotating rects, gravity), sparks 'lighter' circles. Particle field {x,y,vx,vy,life,max,size,color,kind,rot,vr,g}.

Texts: rising.

HUD:
```
Bar: rgba(8,10,22,.78) rect 0,0,W,HUD_H; bottom border: gradient line teal→gold→magenta 2px.
SCORE label 9px letter-spaced fill '#8fb3c7'; value '900 24px "Courier New"' '#eaf6ff'.
Combo center: if combo>=2 pulsing scale gold "COMBO ×n".
Lives right: 3 circles radius 6.5 at x=W-24- i*22: filled warm #ffd76a? Lives shown as mini balls (cyan) with glow, lost ones dark outline.
Wide chip below bar when active, centered: rr pill w=120: label 'WIDE' + decreasing bar width = (wideT/10)*100.
Small mute indicator on right corner? skip; mention in title/pause hint.
```
Add "BEST" small under score? clutter; best only in title/end.

Serve hint (playing, ball stuck): pulsing centered text at H-150: "CLICK OR PRESS SPACE TO LAUNCH" + small "auto-launch…" skip.

Demo title overlay: as planned. Also add small footer at bottom of title: "3 LIVES · CLEAR ALL 72 BRICKS".

DrawEnd(win):
```
bg rgba(4,6,14,.78) (+win: gold tint rays? add radial glow)
Heading: win? "SECTOR CLEARED" gold; else "GAME OVER" red-coral.
Score big; "BEST" if relevant; if win, bonus line.
Button in middle: label "PLAY AGAIN"; sub "press R".
Also small "or press R / Enter".
```
Store buttons in `buttons=[{x,y,w,h,cb}]` each frame in overlay draw.

Click handler:
```
canvas.addEventListener('pointerdown', e=>{ ensureAudio(); const p=toGame(e); if(mode==='demo'){startPlay();return;} if(paused){paused=false;return;} if(mode==='play'){ if(any stuck ball) launchAllStuck(); } else { for(b of buttons) if hit → b.cb(); } });
```
Also use pointermove for hover + paddle: use 'pointermove' on window? paddle should track even slightly outside canvas; listen on window and map; fine.

Keyboard:
```
window.addEventListener('keydown', e=>{
 ensureAudio();
 const k=e.key;
 if(['ArrowLeft','ArrowRight',' ','ArrowUp','ArrowDown'].includes(k)) e.preventDefault();
 if(mode==='demo'){ startPlay(); return; } // any key starts — including arrows; ok.
 if(k==='m'||k==='M'){muted=!muted; toastText(...)}
 if(k==='p'||k==='P'){ if(mode==='play'){paused=!paused;} return;}
 if(mode==='over'||mode==='win'){ if(k==='r'||k==='R'||k===' '||k==='Enter') startPlay(); return; }
 if(paused){ if(k==='p')... already handled; return;}
 if(k==='ArrowLeft')keys.l=true; ArrowRight keys.r=true;
 if(k===' '||k==='ArrowUp'||k==='Enter'){ launch stuck balls; }
});
keyup clears arrows.
```
Note: starting play on any key press including 'm' — acceptable ("click or press any key to start").

startPlay(): newGame('play'); toast "GO!"? maybe "GET READY".

Restart from end: startPlay().

Also in demo, if mode==='demo' and (win/over happened) — demo's endGame immediately triggers reset: in update, if mode==='demo' && (state over/win) → newGame('demo'). But endGame sets mode to over/win; better: in demo, endGame(true/false) → after short delay (1.2s) newGame('demo')? Simpler: demo skips end screen: if demo endGame just calls newGame('demo') immediately with a small particle burst. Let's do that: in endGame: `if(wasDemo){ newGame('demo'); return; }`. Store flag demoRun = (mode==='demo') before switching.

Toast: reuse texts array with special big centered text {big:true}.

Combo text HUD draw.

Particles cap: parts.length>500 → splice old.

Victory confetti: 80 particles from random x in top area with random row colors, gravity, longer life.

Also subtle idle attract: paddle sway in demo when no ball — covered.

Now performance: shadowBlur only used on: ball (≤6), drops (≤3), paddle glow ellipse maybe, rails maybe. Fine.

Write helpers:

```
function clamp(v,a,b){return v<a?a:v>b?b:v}
function rr(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();}
```

Fonts:
```
const F_DISPLAY='"Arial Black","Arial Bold",Arial,sans-serif';
const F_MONO='"Courier New",ui-monospace,monospace';
const F_BODY='Verdana,Geneva,sans-serif';
```

Text-draw helpers for centering.

Shuffle: fisher-yates.

Alright, careful about "center = vertical" paddle bounce — formula gives exactly vertical at rel 0. ✓.

Edge angle: rel ±1 → ±62° from vertical → sharp. ✓.

Brick icon visibility requirement ✓ (badge on brick).

"visible cracks after first hit" ✓.

Wide 10s ✓ with HUD timer.

Multiball 3 balls ✓ (existing 1 + 2 spawned).

No tunneling ✓ substep ≤4px.

Particles ✓. Lives 3 ✓ score ✓ gameover/victory + restart ✓.

Self-playing demo with start message ✓.

Now — one nuance: on first paint (~3s), demo should look alive: bricks exist, ball in play, particles maybe. Serve launch is 0.9s so by 3s we have action. Title overlay must not obscure: alpha .55 backdrop + content in center-lower area? Title text placement: title at y≈250 (above bricks? bricks are 104..324 behind, overlay dims). Fine.

Maybe make title overlay lighter (0.45) so demo shines through.

Also add small pulsing ring behind prompt.

Potential problem: pointermove sets paddle.x even during title → paddle moves under overlay: fine (demo AI overrides each frame? AI sets paddle.x in update; mouse events also set; conflict — during demo, ignore mouse for paddle: only set paddle from mouse when mode==='play'&&!paused). ok.

Also keyboard movement during demo ignored — AI drives. Fine.

Now let me write it all out. Take care: no console errors. Variables declared before use in execution order.

Also one more consideration about the top rail: ball bounce PLAY_TOP=HUD_H=64. Draw rail bar at y 60..66. Bricks at 104. ok.

Wall spark helper to push 3 particles.

Also draw a subtle drop shadow under bricks? skip.

Let me write drawBricks with cached gradients:

```
let rowGrad=[], toughGrad=null, toughGradDmg=null;
function buildGradients(){ for r: g=ctx.createLinearGradient(0,BTOP+r*(BH+GAP),0,...+BH); addColorStop(0, light), (0.45, base), (1, shade(base,-30)); rowGrad[r]=g; tough similar }
```
Called once at init (needs ctx). Colors constants.

shade(hex,amt):
```
function shade(hex,amt){const n=parseInt(hex.slice(1),16);let r=(n>>16)+amt,g=(n>>8&255)+amt,b=(n&255)+amt;r=clamp(r,0,255)...return `rgb(${r},${g},${b})`}
```

Row color list [base,light]:
```
const ROWC=[['#ff5d5d','#ff9d8f'],['#ff8a3d','#ffbd7d'],['#ffc93d','#ffe59a'],['#9ad44a','#cdeb8f'],['#35d1a4','#8df0d4'],['#38b3ff','#93d9ff'],['#5c86ff','#a8c0ff'],['#a86bff','#d3b0ff']];
```

Points: base=(ROWS-r)*? normal: 30 + (ROWS-1-r)*10 → r0=100, r7=30. Tough is +80. Combo bonus (combo-1)*10.

Tough gradient: ['#8fa5bd','#5a6e85','#3c4a5c']; damaged overlay.

Rivets: 4 dots radius 1.5 rgba(230,240,255,.35) at inset 5.

Power badge: circle radius 7.5 at center: fill rgba(6,10,18,.6), ring color (wide '#ffd24a', multi '#ff5fa8') lw1.5, glyph white:
- Wide: horizontal line at y with -3.5..3.5, arrow tips.
- Multi: 3 dots radius 1.4: top (0,-2), bottom-left/right (±2.3,1.5).

Glyph on drop capsule similar but larger.

Drop update should spawn occasional sparkle trail too: each frame small chance push a spark particle.

Serve: multiple stuck balls? only 1 after life reset. Launch on input launches all stuck balls.

Also ensure ball cap: MAX_BALLS=6 guard for multiball.

End screen button cb: startPlay.

Now audio arpeggio:
```
function arpeggio(freqs,dur=0.09){freqs.forEach((f,i)=>tone(f,f,dur,'square',.5,i*0.07));}
Catch: [523,659,784,1046]
Victory: [392,523,659,784] longer
Game over: [330,247,165] saw
```
tone supports delay param.

Wall sound maybe too frequent; volume low .15 and only if velocity? Keep small.

Throttle brick sound within same frame: multiple bricks in same frame unlikely. ok.

Mute toast text: use texts.push big center "SOUND OFF/ON" small. Let's make a toast function with style flag.

Pause overlay: "PAUSED" + "P to resume · M mute".

Alright also handle visibilitychange? dt clamp handles.

Now let me write out the final code. Aim for clean structure with section comments. Also add small "bricks remaining" pip? skip.

Re-check predictX formula: u range [0,2span]; if u>span → position = 2span-u → within [0,span]; result L+pos ✓.

Wall bounce sound throttle: track lastWallSound time.

Also ensure demo title shows even before ball launch — yes, mode demo from start.

Edge case: pointerdown on 'over' → button check; if click outside button do nothing (prevent misclick restarts). Good.

Hover cursor: on pointermove compute if over any button → canvas.style.cursor='pointer' else 'crosshair'.

Buttons stored with cb; draw function makes them.

Also add hover brighten: store hover flag during draw via mouse pos.

Mouse position: keep in mx,my in logical coords.

Alright, code writing time. Keep functions in order: helpers → audio → setup → level → entities update → collision → effects → rendering → input → loop.

Sanity check numbers: PADDLE base width 104 vs ball speed 330..560 — demo AI speed 430 makes for decent play. Player fine.

Ball spawn stuck position y=PAD_Y-BALL_R-1=744-8=736.

Drop catch check: overlap with paddle rect (x±w/2, PAD_Y..PAD_Y+PAD_H): d.x+d.w/2>pLeft && d.x-d.w/2<pRight && d.y+d.h/2>=PAD_Y && d.y-d.h/2<=PAD_Y+PAD_H.

Drops disappear at bottom.

Particles on catch: gold ring burst.

Text position clamp inside screen.

OK also victory confetti + end screen show after? Mode 'win' immediately shows overlay (dim) with confetti visible behind — confetti continues updating in draw? update() early-returns in non-play? Let's keep updating particles/texts even in over/win modes so confetti animates behind overlay. Structure update(): if(mode==='play'||mode==='demo'){ full simulation } else { update particles/texts/shake/redFlash }. Also demo reset path: if demo endGame → immediately newGame('demo'), so no end screen in demo. ✓.

Paused: skip everything except rendering; also skip particles (freeze) is fine — but overlay text pulse uses timeAll; keep timeAll+=dt always for pulses. Particles freeze during pause is acceptable.

Combo HUD: draw at center HUD_H-14 with gold.

Also draw "bricksLeft" small at HUD center bottom? Might clutter with combo — draw only when combo<2.

Actually nice: center HUD shows "BRICKS 54" normally, switches to gold "COMBO ×4" when combo>=2.

Now finalize title copy:
- Big: "PRISM BREAK"
- Sub: "ARCADE BRICK BREAKER"
- Prompt: "CLICK OR PRESS ANY KEY TO START" with blink.
- Controls: "MOUSE or ◀ ▶ MOVE · CLICK / SPACE LAUNCH · P PAUSE"
- Legend: [wide capsule] "WIDE PADDLE 10s", [multi capsule] "MULTIBALL ×3"
- If >0, best: "BEST 12,340"

End copy:
- win: "SECTOR CLEARED!" subtitle "all 72 bricks destroyed"; score; life bonus line; button.
- over: "GAME OVER"; "the wall wins… this time"; score; best; button "TRY AGAIN".

Button label: "▶ PLAY AGAIN (R)".

Also show final score large monospaced.

Write drawEnd(win).

Alright — small polish also: score digits flash on change: track scoreFlash timer → draw scale/color pop. Cheap: on score add scorePulse=1 decay; draw with size 24+4*pulse.

Time to write the code. Careful with template literals and string construction.

ctx.font string needs to be valid. e.g. `900 64px ${F_DISPLAY}` — the Arial Black font family quoted inside: `'"Arial Black","Arial Bold",Arial,sans-serif'` → font string: `'900 64px "Arial Black","Arial Bold",Arial,sans-serif'` is valid.

letterSpacing: ctx.letterSpacing='2px' supported in Chrome 99+; guard with try? Assigning unsupported is just ignored (property assignment, no error). Use it in small labels, harmless.

OK code:

Also remember: `image-rendering` default fine.

Potential pitfall: `ctx.roundRect` not used (custom rr). Fine.

Star init: 110 stars random.

Dust: 14 blobs radius 1-2 alpha .1 drift slowly up.

Nebula offscreen: create canvas W,H, draw 3 radial gradients.

drawBG: fill with cached vertical gradient: '#0a1220' top → '#0e1a2b'? Let's pick: deep petrol top #081018, mid #0d1b26, bottom #131021? Slight purple at bottom adds depth. Stops: 0 '#070d16', .5 '#0c1a24', 1 '#121022'.

Nebula alpha low.

Rail color teal #35e0c8 with alpha.

Also draw faint grid? skip.

OK, writing.

After writing, mentally lint: all functions defined before they're called at runtime (loop starts after defs). Input listeners reference functions defined above — all function declarations at top scope hoist.

Also `viewScale` initialized by resize() before first frame; ctx from canvas.

Also compute vs=canvas.width/W for setTransform per frame; vertical same scale since aspect preserved.

Red vignette: radial gradient center transparent → edges rgba(255,60,60,redFlash*.5). Create per draw (cheap enough) or cache; create each frame fine.

Shake: shakeMag decay: shake=Math.max(0,shake - shake*10*dt - 2*dt); offset random per frame.

Now particle spawn helpers:
```
function burstBrick(br,color){ for i<16: shards {x random in brick, vx ±140, vy -60..-220? plus gravity 600, life .5-.9, size 2-4.5, rot random, vr ±6, color: base/light mix} ; for i<10 sparks additive color '#fff'/'#bff' velocity 40-240 random dir life .25-.5 }
```

Text: {x,y,txt,color,life,max,size,big}. Update y-=30*dt.

OK now write final answer file. Must be one fenced html block. No text around.

A few careful implementations:

moveBall:
```js
function moveBall(b,dt){
  if(b.stuck) return;
  const sp=Math.hypot(b.vx,b.vy)||BASE_SP;
  const steps=Math.max(1,Math.ceil(sp*dt/4));
  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<BALL_R){b.x=BALL_R;b.vx=Math.abs(b.vx);wallFx(b.x,b.y);}
    else if(b.x>W-BALL_R){b.x=W-BALL_R;b.vx=-Math.abs(b.vx);wallFx(b.x,b.y);}
    if(b.y<HUD_H+BALL_R){b.y=HUD_H+BALL_R;b.vy=Math.abs(b.vy);wallFx(b.x,b.y);}
    collidePaddle(b);
    if(collideBricks(b)){}
    b.trail.push({x:b.x,y:b.y}); if(b.trail.length>10)b.trail.shift();
    if(b.y-BALL_R>H+14){b.dead=true;break;}
  }
}
```
Trail per substep grows fast; push once per frame outside instead: remove from loop; push once after loop in update. Do that.

collidePaddle detail:
```js
function collidePaddle(b){
  const p=paddle; const hw=p.w/2;
  const px1=p.x-hw, px2=p.x+hw, py1=PAD_Y, py2=PAD_Y+PAD_H;
  if(b.x<px1-BALL_R||b.x>px2+BALL_R||b.y<py1-BALL_R||b.y>py2+BALL_R)return;
  const cx=clamp(b.x,px1,px2), cy=clamp(b.y,py1,py2);
  const dx=b.x-cx, dy=b.y-cy;
  if(dx*dx+dy*dy>BALL_R*BALL_R)return;
  if(b.vy>0 && b.y<py1+6){
     const rel=clamp((b.x-p.x)/hw,-1,1);
     const ang=rel*MAX_BOUNCE;
     ballSpeed=Math.min(MAX_SP,ballSpeed+7);
     const sp=ballSpeed;
     b.vx=Math.sin(ang)*sp; b.vy=-Math.cos(ang)*sp;
     b.y=py1-BALL_R-0.5;
     combo=0; p.squash=1; sfx.paddle(); sparksAt(b.x,py1,'#bff7ff',6);
  } else {
     // generic resolve
     let nx,ny;
     const d2=dx*dx+dy*dy;
     if(d2>1e-6){const d=Math.sqrt(d2);nx=dx/d;ny=dy/d;}
     else{nx=0;ny=-1;}
     const dot=b.vx*nx+b.vy*ny;
     if(dot<0){b.vx-=2*dot*nx;b.vy-=2*dot*ny;}
     const pen=BALL_R-Math.sqrt(d2)+0.3;
     b.x+=nx*pen;b.y+=ny*pen;
     sanitizeVel(b);
  }
}
```

sanitizeVel:
```js
function sanitizeVel(b){
  const sp=Math.hypot(b.vx,b.vy); if(sp<1)return;
  const minVy=0.24*sp;
  if(Math.abs(b.vy)<minVy){
    const sgn=b.vy===0?-1:(b.vy>0?1:-1);
    b.vy=sgn*minVy;
    const vxm=Math.sqrt(Math.max(0,sp*sp-b.vy*b.vy));
    b.vx=(b.vx===0?(Math.random()<0.5?-1:1):Math.sign(b.vx))*vxm;
  }
}
```

collideBricks returns bool, with jitter rotation:
```js
function rotVel(b,a){const c=Math.cos(a),s=Math.sin(a);const vx=b.vx*c-b.vy*s;b.vy=b.vx*s+b.vy*c;b.vx=vx;}
```
Jitter ±0.02 rad after brick bounce + sanitize.

hitBrick(br):
As planned. Also if destroy and drop: push drop {x:cx,y:cy,type:br.power,vy:130,ph:rand*6}.

Brick flash decay in update loop over bricks (only alive). Also demo reset path: endGame sets mode; but ensure update loop for bricks only in play/demo modes.

endGame(winFlag):
```js
function endGame(winFlag){
  if(mode==='demo'){ newGame('demo'); return; }
  if(winFlag){ const bonus=lives*500; score+=bonus; lastBonus=bonus; confetti(); sfx.win(); mode='win'; }
  else { sfx.over(); mode='over'; }
  best=Math.max(best,score);
  balls.length=0;
}
```
Called from hitBrick only when mode play/demo; lifeLost handles over separately:
```js
if(lives<=0){ if(mode==='demo'){newGame('demo');return;} sfx.over(); mode='over'; best=Math.max(best,score); }
```

startPlay(): newGame('play'); toast "GET READY".

newGame(m) resets everything and calls buildLevel.

Build gradients once at init, but rowGrad depends on nothing dynamic; ok create after ctx exists. Note newGame rebuilds bricks but gradients constant.

Draw bricks: iterate; dead skip. Use rowGrad[row] for fill. Tough uses toughGrad (or dmg version).

Also draw a subtle brick shadow line at bottom: after fill, rr bottom band? just include darker stop in gradient. Add 1px stroke rgba(0,0,0,.25)? ok skip stroke; add top highlight line.

Flash overlay uses flash value.

Icon only if power && alive.

HUD score formatting: String(score).padStart(6,'0').

Also show small "HI" with best at HUD right? Keep HUD clean: score left, bricks/combo center, lives right. Fine.

Serve hint arrow: also draw a small pulsing up-arrow above stuck ball.

Title overlay draw order after HUD so it covers.

Buttons: in drawEnd create: buttons=[{x:W/2-110,y:520,w:220,h:54,label:'▶ PLAY AGAIN',sub:'press R',cb:startPlay}]; also add "BACK TO TITLE"? extra small text button: cb: newGame('demo'). Nice: two options. Main at y=500; secondary text button y=575 "…or T for title screen". Handle keydown 't' on end screen → newGame('demo'). Add both.

drawButton(btn): rr gradient fill (hover brighter), border glow, label centered display font 20, sub small below? Include hint text separately.

Click handler uses buttons array (last drawn). pointermove updates mx,my + hover cursor.

Ensure to clear buttons array when not in end screens (to prevent old clicks): at start of draw set buttons=[] and let overlays fill. Since draw happens each frame before click — ok.

Pause blocks input to paddle etc but unpause via p or click.

Toast: function toast(txt,color) pushes text at center y 200 size 26 big.

Alright audio sfx object:
```js
const sfx={
 paddle:()=>tone(190,260,.06,'square',.35),
 wall:()=>{if(timeAll-lastWall>0.05){tone(150,120,.04,'triangle',.18);lastWall=timeAll}},
 brick:(row,tough)=>{const f=tough?180:340+(ROWS-1-row)*36; tone(f,f*1.5,.07,'square',.4); if(!tough)tone(f*2,f*2,.05,'square',.18,.03)},
 clink:()=>{tone(120,90,.07,'sawtooth',.35);tone(800,600,.03,'square',.1)},
 launch:()=>tone(240,560,.12,'triangle',.4),
 catchp:()=>[523,659,784,1047].forEach((f,i)=>tone(f,f,.09,'square',.4,i*.06)),
 life:()=>tone(340,60,.5,'sawtooth',.5),
 over:()=>[311,247,175,110].forEach((f,i)=>tone(f,f*0.98,.22,'sawtooth',.4,i*.16)),
 win:()=>[392,494,587,784,988].forEach((f,i)=>tone(f,f,.14,'square',.4,i*.09)),
};
```
All guarded by if(!AC||muted).

tone impl as planned, using exponentialRamp only when f1>0.

ensureAudio on pointerdown/keydown.

Note: creating AudioContext before user gesture logs a warning in Chrome console ("The AudioContext was not allowed to start"). We only create on user gesture (pointerdown/keydown handler) → no warning. Demo mode silent until interaction.

Edge: `new (window.AudioContext||window.webkitAudioContext)()` inside try.

Now finalize particle draw:
```
Shards: ctx.save translate rotate fillRect(-s/2,-s/2,s,s*0.7) alpha=life/max
Sparks: lighter circle radius size*alpha
```

Confetti(): 90 particles from top random row color vy 60..180 vx ±60 life 1.2-2 g 260 kind shard bigger size.

Particle update: vy+=g*dt; x+=vx; life-=dt.

Also drops sparkle: each frame if(Math.random()<0.25) push small spark at drop position with vy -20.

OK finalize HUD combo/bricks center text:
```
if(combo>=2) gold pulsing "COMBO ×"+combo
else '#7f9db3' "BRICKS LEFT "+bricksLeft
```
Life orbs: draw at x=W-26 - i*24, y=HUD_H/2: if i<lives fill cyan glow else dark stroke.

Wide chip: when wideT>0: center pill at y=HUD_H+14: rr w=132 h=22 center x: bg rgba(20,16,4,.75) border gold; text 'WIDE' left; bar remaining width.

Score pulse too: draw score with size 22+6*scorePulse and color white → normal.

Now assemble the file. Viewport meta also added.

Handle pointercancel too.

Touch: pointer events cover; prevent default on touchstart to avoid scroll: set css touch-action:none on canvas.

Also prevent context menu? skip.

One more: mouse paddle control should use latest mx even after resize: on pointermove store:
```
const rect=canvas.getBoundingClientRect(); mx=(e.clientX-rect.left)/rect.width*W; my=...
if(mode==='play'&&!paused) paddle.x=clamp(mx, ...)
```
Keyboard also adjusts; fine.

Serve auto-launch timer for player: 2.2s; show small countdown ring? just hint text with pulsing.

Write drawServeHint: if(mode==='play'&&!paused&&balls.some(stuck)): at H-170 pulsing: "CLICK OR PRESS SPACE TO LAUNCH" size 15 mono letter-spaced; plus a small arrow above the stuck ball.

Alright also draw small "1UP"? no.

Time to write it all out. Take great care.

Also: `mode==='demo'` AI should also catch drops well? Drops fall near paddle; AI chases ball; incidental catches ok.

Demo paddle initial width etc.

Also confirm no horizontal scrollbar: body overflow hidden; canvas fits within viewport minus padding.

CSS:
```
*{box-sizing:border-box}
html,body{height:100%;}
body{margin:0;display:flex;align-items:center;justify-content:center;background:radial-gradient(1100px 800px at 50% 18%, #10222c 0%, #090d16 55%, #05060c 100%);overflow:hidden;font-family:Verdana,sans-serif;}
.stage{position:relative;border-radius:12px;}
canvas{display:block;border-radius:12px;background:#070d16;cursor:crosshair;touch-action:none;box-shadow:0 24px 70px rgba(0,0,0,.65),0 0 0 1px rgba(126,214,255,.14),0 0 44px rgba(56,182,255,.10);}
.stage::after{content:"";position:absolute;inset:0;border-radius:12px;pointer-events:none;background:repeating-linear-gradient(to bottom,rgba(255,255,255,.028) 0 1px,rgba(0,0,0,0) 1px 3px);mix-blend-mode:overlay? maybe none;} also add radial vignette with another gradient layer
```
Combine: background: vignette radial + scanlines in one: list multiple backgrounds.

```
background:
 radial-gradient(120% 90% at 50% 45%, rgba(0,0,0,0) 60%, rgba(0,0,0,.32) 100%),
 repeating-linear-gradient(to bottom, rgba(255,255,255,.03) 0 1px, transparent 1px 3px);
```
ok.

Now write out JS in full. Write ~700 lines. Ensure no stray identifiers.

Special attention: in drawTitle, draw mini capsules using drawCapsuleShape(x,y,type,scale) reusable for drops and legend.

Capsule draw:
```js
function drawCapsule(x,y,type,t,scale=1,wob=true){
 ctx.save();ctx.translate(x,y);if(wob)ctx.rotate(Math.sin(t*5)*0.12);ctx.scale(scale,scale);
 const w=38,h=20;
 glow color = type==='wide'?'#ffd24a':'#ff5fa8';
 ctx.shadowColor=glow;ctx.shadowBlur=14;
 rr(-w/2,-h/2,w,h,h/2);
 const g=ctx.createLinearGradient(0,-h/2,0,h/2);
 if wide: '#ffe9a8','#ffc93d','#c8871a' else '#ffb3d1','#ff5fa8','#b3256d';
 fill g; shadowBlur=0; stroke rgba(255,255,255,.5) lw1.
 glyph white/dark: dark glyph '#3a2404' on gold; multi '#4a0f2c'? white more readable: use white with slight shadow.
 draw glyph:
 wide: arrow line -8..8 lw2 white + triangles at ends.
 multi: 3 dots radius 2.2.
 ctx.restore();}
```

Badge on brick: smaller: circle radius 8 bg rgba(5,8,14,.72), ring stroke glow color, glyph mini white (line ±3.5 / dots 1.4).

OK final write.

Also in HUD, draw a small game title at center top? Bricks/combo is there. Left has score; right has lives; maybe add small "PRISM BREAK" watermark at bottom-right corner of playfield? skip.

Also draw thin progress: none.

OK, victory confetti colors from ROWC base.

Ensure `lives*500` uses lives before end. On victory lives>=1.

Write drawEnd:
```
buttons.length=0;
dim rgba(4,7,15,.8)
If win: gold radial glow behind heading
Heading y 250 font 54 display: win? '#ffd24a' shadow gold : '#ff5d6d'
sub line
score panel: "FINAL SCORE" label + monospaced big score
win: "+ {bonus} LIFE BONUS"
if(score>=best&&score>0) "★ NEW BEST"
Button PLAY AGAIN at y 470 (rect 240x54)
Secondary: y 545 small text "press T for title screen" as text button? Make it an actual button region with modest style.
hint: "R / ENTER also works"
```
Also draw key hint small.

Pause overlay: dim .55, "PAUSED", "P resume · M sound".

Title overlay detail (draw behind demo simulation):
```
bg: rgba(6,9,18,.5)
Top gradient additional? fine
title y 235: draw chromatic with offsets
sub y 272: "— ARCADE BRICK BREAKER —" mono letter-spaced 12 color #7fd8ff
prompt y 340 pulsing box? pulsing text 18 display white + a rounded pill stroke.
controls y 384: 12px mono '#9fb8c8'
legend y 430..: capsules at x W/2-120 and +120 with labels below 11px
best y 500 if >0
footer y 770: "VANILLA JS · CANVAS · 60FPS" tiny? cute: "ONE FILE · ZERO DEPENDENCIES". Small.
```
Note demo playfield visible around center overlay content? overlay dims all; action visible behind. Also content placed in empty space between bricks bottom (~330) and paddle (744): put content y 380-560 → overlap play area where ball moves — dim + content both visible, arcade-y fine.

Chromatic title:
```
ctx.font='900 62px '+F_DISPLAY; textAlign center;
fill '#ff5d7a' at (x-3,y+2) alpha .8? use globalAlpha
fill '#4dd7ff' at (x+3,y-2)
main fill '#f4fbff' at (x,y) with shadowBlur 18 color rgba(120,220,255,.6)
```
Slight arch? skip.

Now final generation. Watch out for quotes inside template strings.

Also add `lastWall=0`.

Dust: simple.

Ball draw with trail:
```
ctx.save();ctx.globalCompositeOperation='lighter';
for i in trail: alpha=i/len*0.35 radius BALL_R*(0.4+0.6*i/len) fill 'rgba(90,220,255,a)'
ctx.restore();
Ball: shadowColor '#7fe9ff' blur 16; radial gradient white→#9fe8ff→#3fb9ff edge; fill circle; then small highlight.
```

Paddle draw:
```
const w=paddle.w,h=PAD_H*(1-0.3*paddle.squash),x=p.x-w/2,y=PAD_Y+(PAD_H-h);
underglow: ellipse gradient radial below: ctx 'lighter' radial rgba cyan .35→0 at (p.x, PAD_Y+PAD_H+8) rx w*0.6.
body: linear top→bottom: wide? gold stops else cyan stops; rr(x,y,w,h,h/2) fill; 
inner top gloss: rr(x+4,y+2,w-8,h*0.35, ...) rgba(255,255,255,.5)? .35 alpha.
edge lights: 2 small bright dots at ends.
```

Rails:
```
Left: linear gradient fill rgba teal .0→.5: draw rect x0..3 from HUD_H to H: vertical gradient alpha? Simple: fillStyle 'rgba(80,220,210,.16)' rect 0,HUD_H,3,H; plus a brighter core line 1px 'rgba(140,255,240,.35)'.
Top rail: rect 0,HUD_H,W,3 same + a glow line.
```
Also corner ticks.

OK, finalize file. Also confirm `score` display padStart exists (yes).

Potential subtle bug: brick flash decay only in play/demo update; when mode is over, bricks are frozen — fine.

Also in update for drops/particles/texts during over/win modes: run those systems so confetti shows: structure update():
```
timeAll+=dt;
if(paused)return;
updateFx(dt); // particles, texts, shake, redFlash, scorePulse decay
if(mode==='play'||mode==='demo'){ sim(dt); }
```
sim contains paddle/balls/drops/bricks.

Brick flash decay in sim.

Also draw bricks in over/win modes still (they remain). ok.

Also demo endGame reset — confetti skipped there (just reset). ok.

Write lifeLost fully:
```
function lifeLost(){
 lives--; combo=0; shake=13; redFlash=1; sfx.life();
 wideT=0; drops.length=0;
 if(lives>0){ ballSpeed=BASE_SP; spawnServe(); }
 else if(mode==='demo'){ newGame('demo'); }
 else { mode='over'; best=Math.max(best,score); sfx.over(); }
}
```

spawnServe(): balls=[newStuckBall()]; with launchT depending on mode.

Note: multiball spawn during stuck?? Can't — drops require ball in play. fine.

Also on catch power while ball stuck? Can't happen.

OK — now write the code, final pass. Generate the full HTML carefully.

Remember fence is exactly one code block, nothing else.

Also include `aria-hidden` on decorative? skip fine.

Confirm the wide paddle timer chip and paddle gold tint only shows when active, and re-catch refreshes (wideT=10 assignment). ✓.

Also requirement: "multiball (3 balls)" — also spawn if balls.length>=1; total = current + 2. If current is 4 → 6 cap. Fine.

Also prevent spawn > cap.

Alright, writing.

Additional check — paddle bounce angle: "center = vertical" — formula gives exactly vertical at rel 0. ✓.

Edge angle: rel ±1 → ±62° from vertical → sharp. ✓.

Brick icon visibility requirement ✓ (badge on brick).

"visible cracks after first hit" ✓.

Wide 10s ✓ with HUD timer.

Multiball 3 balls ✓ (existing 1 + 2 spawned).

No tunneling ✓ substep ≤4px.

Particles ✓. Lives 3 ✓ score ✓ gameover/victory + restart ✓.

Self-playing demo with start message ✓.

Write out the code block. Take care with template literals and string construction.

One subtle thing: in drawBricks, gradient uses coordinates baked in absolute — rowGrad made with row-specific y; when drawing each brick in that row, gradient aligns ✓. toughGrad baked at BTOP? tough bricks appear across rows — one generic gradient made with coords y=0..BH? Gradient coords are absolute canvas coords; a gradient made 0→BH drawn at brick y=200+ won't show (out of range, clamps to last stop). Fix: create tough gradient with (0,0,0,1)?? Instead: create linear gradient per-brick-y when drawing (cheap enough) OR use normalized via transform. Simplest: draw with base fill + light/dark overlays computed without gradient: fill base color; then rgba(255,255,255,.22) top band; rgba(0,0,0,.25) bottom band. This flat+sheen approach is fast and consistent. Similarly for rows: pre-made per-row gradient works because y fixed. Keep row gradients (position fixed per row) and use fill+sheen for tough.

Actually simplify rows similarly: fill base; top sheen; bottom shade. Flat and crisp — retro. Plus a slightly darker outline. Let's go with that; drop rowGrad entirely. Cleaner and reliable.

Draw brick:
```
ctx.fillStyle=base; rr(x,y,w,h,4); fill;
ctx.fillStyle='rgba(255,255,255,.28)'; rr(x+2,y+2,w-4,5,2); fill; // top sheen
ctx.fillStyle='rgba(0,0,0,.22)'; rr(x+2,y+h-5,w-4,3,1.5); fill;
```
Tough: base '#5c6f86', damaged base darker '#46566b'. Rivets. Cracks when hp1.

Fine.

Also add subtle drop shadow beneath each brick? skip.

Now let me truly write the final. Also `let mx=W/2,my=H/2`.

Button hover check: helper inBtn(b)= mx>=b.x&&mx<=b.x+b.w&&...

pointerdown mapping: use same toLocal.

Also ignore pointerdown when paused → unpause (done).

Keys: arrow also used during serve to move paddle — fine.

Now generate the final HTML. Must be one fenced html block. Nothing around.