Models 44

GLM 5.3 Flash

by Z.aiReasoningClean
Open in new window

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

How it went

Total time
3 min 31 s
Cost
$0.021
Output tokens
42,236
Spent thinking
33,128 tokens
Speed
201.1 tok/s
Console errors
0
First token after
706 ms
First visible text after
2 min 54 s
Prompt tokens
507
Code size
25.1 KB
Finish reason
stop
Served by
Together
Model id
z-ai/glm-5.3-flash
Reasoning sent
{"effort":"max"}
Max output
64,000 tokens
Generated
Nov 10, 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 · 698 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Neon Breakout</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#05070f}
  body{display:flex;align-items:center;justify-content:center;
    background:radial-gradient(1100px 750px at 50% 38%,#0c1130 0%,#05070f 72%)}
  #game{width:min(96vw,136vh);aspect-ratio:3/2;display:block;border-radius:14px;
    background:#0a0d1f;touch-action:none;cursor:crosshair;
    box-shadow:0 0 50px rgba(60,110,255,.22),0 0 110px rgba(130,70,255,.10),0 0 0 1px rgba(130,160,255,.16)}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
'use strict';
(function(){
/* ================= constants ================= */
var W=960,H=640,HUD_H=54;
var COLS=12,ROWS=8,B_TOP=96,B_H=22,B_GAP=5,MARGIN=22;
var B_W=(W-MARGIN*2-(COLS-1)*B_GAP)/COLS;
var P_BASE_W=112,P_H=14,P_Y=H-34;
var BALL_R=7,SPEED0=430,SPEED_MAX=650;
var ROWS_DEF=[
  {hp:2,color:'#c084fc',pts:30},
  {hp:2,color:'#a78bfa',pts:30},
  {hp:2,color:'#818cf8',pts:30},
  {hp:1,color:'#38bdf8',pts:20},
  {hp:1,color:'#2dd4bf',pts:20},
  {hp:1,color:'#4ade80',pts:20},
  {hp:1,color:'#facc15',pts:20},
  {hp:1,color:'#fb923c',pts:20}
];
var TAU=Math.PI*2;
function rand(a,b){return a+Math.random()*(b-a);}
function clamp(v,a,b){return v<a?a:(v>b?b:v);}

/* ================= canvas ================= */
var canvas=document.getElementById('game');
var ctx=canvas.getContext('2d');
var scaleF=1;
function resize(){
  var r=canvas.getBoundingClientRect();
  var dpr=window.devicePixelRatio||1;
  var bw=Math.max(1,Math.round(r.width*dpr)),bh=Math.max(1,Math.round(r.height*dpr));
  if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
  scaleF=bw/W;
}
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();
}
function ls(v){ if('letterSpacing' in ctx) ctx.letterSpacing=v; }

/* ================= audio (safe, gesture-gated) ================= */
var AC=null;
function ensureAudio(){
  try{
    if(!AC){var C=window.AudioContext||window.webkitAudioContext;if(C)AC=new C();}
    if(AC&&AC.state==='suspended')AC.resume().catch(function(){});
  }catch(e){}
}
function tone(freq,dur,type,vol,delay){
  if(!AC||AC.state!=='running')return;
  try{
    var t0=AC.currentTime+(delay||0);
    var o=AC.createOscillator(),g=AC.createGain();
    o.type=type||'square';
    o.frequency.setValueAtTime(freq,t0);
    g.gain.setValueAtTime(0.0001,t0);
    g.gain.exponentialRampToValueAtTime(vol||0.05,t0+0.01);
    g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
    o.connect(g);g.connect(AC.destination);
    o.start(t0);o.stop(t0+dur+0.03);
  }catch(e){}
}
var sfx={
  brick:function(){tone(520,0.07,'square',0.045);tone(260,0.09,'square',0.04);},
  crack:function(){tone(240,0.05,'square',0.035);},
  paddle:function(){tone(330,0.06,'triangle',0.06);},
  wall:function(){tone(190,0.035,'triangle',0.028);},
  power:function(){tone(523,0.09,'square',0.05);tone(659,0.09,'square',0.05,0.08);tone(784,0.14,'square',0.05,0.16);},
  lose:function(){tone(300,0.16,'sawtooth',0.06);tone(150,0.26,'sawtooth',0.06,0.13);},
  win:function(){[523,659,784,1046].forEach(function(f,i){tone(f,0.15,'square',0.05,i*0.11);});},
  over:function(){[330,262,196,131].forEach(function(f,i){tone(f,0.22,'sawtooth',0.05,i*0.15);});},
  launch:function(){tone(440,0.06,'triangle',0.045);}
};

/* ================= state ================= */
var state='demo',demoMode=true; /* demo | playing | over | win */
var score=0,lives=3,ballSpeed=SPEED0,winBonus=0;
var bricks=[],balls=[],capsules=[],parts=[],floats=[];
var paddle={x:W/2,y:P_Y,w:P_BASE_W,tw:P_BASE_W,h:P_H,flash:0};
var wideT=0,shake=0,time=0,overDelay=0;
var mouseX=W/2,mouseControl=true;
var keys={};
var aiNoiseX=0,aiNoiseT=0;

/* ================= level ================= */
function buildBricks(){
  bricks=[];
  for(var r=0;r<ROWS;r++)for(var c=0;c<COLS;c++){
    var d=ROWS_DEF[r];
    bricks.push({x:MARGIN+c*(B_W+B_GAP),y:B_TOP+r*(B_H+B_GAP),w:B_W,h:B_H,
      hp:d.hp,maxHp:d.hp,color:d.color,pts:d.pts,power:null,cracks:null,pulse:Math.random()*TAU});
  }
  /* randomly mark 8 bricks as power-up carriers */
  var idx=bricks.map(function(_,i){return i;});
  for(var i=idx.length-1;i>0;i--){var j=(Math.random()*(i+1))|0;var t=idx[i];idx[i]=idx[j];idx[j]=t;}
  for(var k=0;k<8;k++)bricks[idx[k]].power=(k%2===0)?'wide':'multi';
  /* pre-generate crack paths for tough bricks */
  for(var m=0;m<bricks.length;m++){
    var b=bricks[m];
    if(b.maxHp===2){
      b.cracks=[];
      for(var s=0;s<2;s++){
        var pts=[],px=rand(b.w*0.15,b.w*0.85),py=2;
        pts.push([px,py]);
        for(var q=0;q<3;q++){px+=rand(-14,14);py+=(b.h-4)/3;pts.push([clamp(px,3,b.w-3),Math.min(py,b.h-1)]);}
        b.cracks.push(pts);
      }
    }
  }
}

/* ================= balls / effects ================= */
function spawnStuckBall(){
  balls.push({x:paddle.x,y:paddle.y-BALL_R-1,vx:0,vy:0,stuck:true,off:0,stick:0,trail:[]});
}
function launchBall(b){
  b.stuck=false;
  var a=rand(-0.35,0.35);
  b.vx=Math.sin(a)*ballSpeed;b.vy=-Math.cos(a)*ballSpeed;
  sfx.launch();
}
function launchAll(){for(var i=0;i<balls.length;i++)if(balls[i].stuck)launchBall(balls[i]);}
function burst(x,y,color,n,spd){
  for(var i=0;i<n;i++){
    var a=rand(0,TAU),s=rand(40,spd||240);
    parts.push({x:x,y:y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,life:rand(0.4,0.9),t:0,color:color,size:rand(2,4.5)});
  }
}
function sparks(x,y,color,n){
  for(var i=0;i<n;i++){
    var a=rand(0,TAU),s=rand(30,140);
    parts.push({x:x,y:y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:rand(0.15,0.35),t:0,color:color,size:rand(1.5,3)});
  }
}
function addFloat(text,x,y,color,size){floats.push({text:text,x:x,y:y,life:0.9,t:0,color:color,size:size||14});}

/* ================= power-ups ================= */
function multiball(){
  var need=3-balls.length;
  if(need<=0){score+=150;addFloat('+150 MAX BALLS',paddle.x,paddle.y-30,'#f472b6',15);return;}
  var src=null;
  for(var i=0;i<balls.length;i++)if(!balls[i].stuck){src=balls[i];break;}
  for(var k=0;k<need;k++){
    if(src){
      var sp=Math.hypot(src.vx,src.vy)||ballSpeed;
      var a=Math.atan2(src.vy,src.vx)+(k===0?-0.45:0.45);
      balls.push({x:src.x,y:src.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,stuck:false,off:0,stick:0,trail:[]});
    }else{
      var a2=(-90+(k-(need-1)/2)*55)*Math.PI/180;
      balls.push({x:paddle.x,y:paddle.y-BALL_R-2,vx:Math.cos(a2)*ballSpeed,vy:Math.sin(a2)*ballSpeed,stuck:false,off:0,stick:0,trail:[]});
    }
  }
  addFloat('MULTIBALL!',paddle.x,paddle.y-34,'#f472b6',18);
  burst(paddle.x,paddle.y,'#f472b6',20,260);
}
function applyPower(type){
  sfx.power();
  if(type==='wide'){
    wideT=10;paddle.tw=P_BASE_W*1.6;
    addFloat('WIDE PADDLE!',paddle.x,paddle.y-30,'#4ade80',18);
    burst(paddle.x,paddle.y,'#4ade80',18,240);
  }else multiball();
}

/* ================= game flow ================= */
function resetGame(demo){
  demoMode=demo;state=demo?'demo':'playing';
  score=0;lives=3;ballSpeed=SPEED0;wideT=0;winBonus=0;
  buildBricks();
  balls=[];capsules=[];parts=[];floats=[];
  paddle.x=W/2;paddle.w=P_BASE_W;paddle.tw=P_BASE_W;paddle.flash=0;
  spawnStuckBall();
  shake=0;overDelay=0;
}
function gameOver(){state='over';overDelay=0;capsules.length=0;sfx.over();}
function winGame(){
  if(state!=='playing'&&state!=='demo')return;
  winBonus=lives*100;score+=winBonus;
  state='win';overDelay=0;capsules.length=0;
  sfx.win();burst(W/2,H*0.45,'#fbbf24',50,340);shake=4;
}
function loseBallAt(x){sfx.lose();burst(x,H-10,'#f87171',16,220);shake=Math.min(shake+3,8);}

/* ================= bricks ================= */
function hitBrick(b){
  b.hp--;
  if(b.hp<=0){
    score+=b.pts;
    addFloat('+'+b.pts,b.x+b.w/2,b.y+b.h/2,b.color,14);
    burst(b.x+b.w/2,b.y+b.h/2,b.color,16,260);
    shake=Math.min(shake+2.2,7);
    ballSpeed=Math.min(SPEED_MAX,ballSpeed+3.5);
    for(var i=0;i<balls.length;i++){
      var ob=balls[i],sp=Math.hypot(ob.vx,ob.vy);
      if(sp>0){var k=ballSpeed/sp;ob.vx*=k;ob.vy*=k;}
    }
    if(b.power)capsules.push({x:b.x+b.w/2,y:b.y+b.h/2,vy:135,type:b.power,ph:rand(0,TAU)});
    sfx.brick();
  }else{
    score+=10;
    addFloat('+10',b.x+b.w/2,b.y+b.h/2,'#e2e8f0',12);
    burst(b.x+b.w/2,b.y+b.h/2,'#ffffff',5,140);
    sfx.crack();
  }
  var alive=false;
  for(var j=0;j<bricks.length;j++)if(bricks[j].hp>0){alive=true;break;}
  if(!alive)winGame();
}
function brickCollision(b){
  for(var i=0;i<bricks.length;i++){
    var br=bricks[i];
    if(br.hp<=0)continue;
    if(b.x+BALL_R>br.x&&b.x-BALL_R<br.x+br.w&&b.y+BALL_R>br.y&&b.y-BALL_R<br.y+br.h){
      var cx=br.x+br.w/2,cy=br.y+br.h/2;
      var px=br.w/2+BALL_R-Math.abs(b.x-cx);
      var py=br.h/2+BALL_R-Math.abs(b.y-cy);
      if(px<py){b.x+=(b.x<cx?-px:px);b.vx=-b.vx;}
      else{b.y+=(b.y<cy?-py:py);b.vy=-b.vy;}
      hitBrick(br);
      break;
    }
  }
}
function bouncePaddle(b){
  var hit=clamp((b.x-paddle.x)/(paddle.w/2),-1,1);   /* -1 edge .. 0 center .. +1 edge */
  var maxAng=60*Math.PI/180;
  var ang=hit*maxAng;                                 /* center = straight up, edges = 60deg */
  b.vx=Math.sin(ang)*ballSpeed;
  b.vy=-Math.cos(ang)*ballSpeed;
  b.y=paddle.y-BALL_R-0.5;
  paddle.flash=0.18;
  sfx.paddle();
  sparks(b.x,paddle.y,'#7dd3fc',4);
  b.trail.length=0;
}

/* ================= control ================= */
function playerControl(dt){
  var L=keys['ArrowLeft']||keys['KeyA'],R=keys['ArrowRight']||keys['KeyD'];
  if(L)paddle.x-=640*dt;
  if(R)paddle.x+=640*dt;
  if(!L&&!R&&mouseControl)paddle.x+=(mouseX-paddle.x)*Math.min(1,dt*24);
}
function aiControl(dt){
  aiNoiseT-=dt;
  if(aiNoiseT<=0){aiNoiseT=rand(0.3,0.6);aiNoiseX=rand(-26,26);}
  var target=W/2,bestT=1e9;
  for(var i=0;i<balls.length;i++){
    var b=balls[i];
    if(b.stuck)continue;
    var px=b.x,py=b.y,pvx=b.vx,pvy=b.vy,t=0,step=1/90;
    for(var s=0;s<600;s++){
      px+=pvx*step;py+=pvy*step;
      if(px<BALL_R){px=BALL_R;pvx=Math.abs(pvx);}
      if(px>W-BALL_R){px=W-BALL_R;pvx=-Math.abs(pvx);}
      if(py<HUD_H+BALL_R){py=HUD_H+BALL_R;pvy=Math.abs(pvy);}
      if(pvy>0&&py>=paddle.y-BALL_R){t+=step;break;}
      t+=step;
    }
    if(t<bestT){bestT=t;target=px;}
  }
  for(var c=0;c<capsules.length;c++){
    var cp=capsules[c];
    if(cp.y>H-170&&Math.abs(cp.x-target)<110){target=cp.x;break;}
  }
  target=clamp(target+aiNoiseX,paddle.w/2+4,W-paddle.w/2-4);
  var maxV=480,dx=target-paddle.x;
  paddle.x+=clamp(dx*10,-maxV*dt,maxV*dt);
}

/* ================= physics ================= */
function updateBalls(dt){
  for(var bi=balls.length-1;bi>=0;bi--){
    var b=balls[bi];
    if(b.stuck){
      b.x=paddle.x+b.off;b.y=paddle.y-BALL_R-1;b.stick+=dt;
      if(state==='demo'&&b.stick>0.55)launchBall(b);
      continue;
    }
    b.trail.push({x:b.x,y:b.y});
    if(b.trail.length>11)b.trail.shift();
    var sp=Math.hypot(b.vx,b.vy);
    var steps=Math.max(1,Math.ceil(sp*dt/6)); /* sub-steps: no tunneling */
    var sdt=dt/steps,dead=false;
    for(var s=0;s<steps&&!dead;s++){
      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);sfx.wall();sparks(b.x,b.y,'#7dd3fc',3);}
      else if(b.x>W-BALL_R){b.x=W-BALL_R;b.vx=-Math.abs(b.vx);sfx.wall();sparks(b.x,b.y,'#7dd3fc',3);}
      if(b.y<HUD_H+BALL_R){b.y=HUD_H+BALL_R;b.vy=Math.abs(b.vy);sfx.wall();sparks(b.x,b.y,'#7dd3fc',3);}
      if(b.vy>0&&b.y+BALL_R>=paddle.y&&b.y-BALL_R<=paddle.y+paddle.h&&
         Math.abs(b.x-paddle.x)<=paddle.w/2+BALL_R){bouncePaddle(b);}
      brickCollision(b);
      if(b.y-BALL_R>H+20)dead=true;
    }
    if(dead){loseBallAt(b.x);balls.splice(bi,1);}
  }
  if(balls.length===0){
    lives--;
    wideT=0;paddle.tw=P_BASE_W;capsules.length=0;
    if(lives<=0)gameOver();
    else{spawnStuckBall();addFloat(lives+' LIVES LEFT',paddle.x,paddle.y-44,'#f87171',16);}
  }
}

function update(dt){
  time+=dt;
  shake*=Math.exp(-7*dt);if(shake<0.03)shake=0;
  var i;
  for(i=parts.length-1;i>=0;i--){
    var p=parts[i];p.t+=dt;
    if(p.t>=p.life){parts.splice(i,1);continue;}
    p.vy+=880*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=Math.exp(-0.9*dt);
  }
  for(i=floats.length-1;i>=0;i--){
    var f=floats[i];f.t+=dt;f.y-=44*dt;
    if(f.t>=f.life)floats.splice(i,1);
  }
  if(paddle.flash>0)paddle.flash-=dt;

  if(state==='over'||state==='win'){
    overDelay+=dt;
    if(demoMode&&overDelay>1.8)resetGame(true);
    return;
  }

  if(state==='demo')aiControl(dt);else playerControl(dt);
  paddle.w+=(paddle.tw-paddle.w)*Math.min(1,dt*12);
  paddle.x=clamp(paddle.x,paddle.w/2+4,W-paddle.w/2-4);
  if(wideT>0){wideT-=dt;if(wideT<=0){wideT=0;paddle.tw=P_BASE_W;}}

  updateBalls(dt);

  for(i=capsules.length-1;i>=0;i--){
    var c=capsules[i];c.y+=c.vy*dt;c.ph+=dt*3.2;
    if(c.y-12>H){capsules.splice(i,1);continue;}
    if(c.y+11>=paddle.y&&c.y-11<=paddle.y+paddle.h&&Math.abs(c.x-paddle.x)<=paddle.w/2+16){
      capsules.splice(i,1);applyPower(c.type);
    }
  }
}

/* ================= rendering ================= */
function drawBackground(){
  ctx.fillStyle='#0a0d1f';ctx.fillRect(0,0,W,H);
  var g=ctx.createRadialGradient(W*0.22,H*0.15,0,W*0.22,H*0.15,430);
  g.addColorStop(0,'rgba(58,92,220,0.10)');g.addColorStop(1,'rgba(58,92,220,0)');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  g=ctx.createRadialGradient(W*0.82,H*0.8,0,W*0.82,H*0.8,470);
  g.addColorStop(0,'rgba(146,64,220,0.09)');g.addColorStop(1,'rgba(146,64,220,0)');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  ctx.strokeStyle='rgba(120,150,255,0.05)';ctx.lineWidth=1;
  ctx.beginPath();
  for(var x=48;x<W;x+=48){ctx.moveTo(x+0.5,HUD_H);ctx.lineTo(x+0.5,H);}
  for(var y=HUD_H+48;y<H;y+=48){ctx.moveTo(0,y+0.5);ctx.lineTo(W,y+0.5);}
  ctx.stroke();
  ctx.strokeStyle='rgba(112,150,255,0.4)';ctx.lineWidth=2;
  ctx.beginPath();ctx.moveTo(1,H);ctx.lineTo(1,HUD_H+1);ctx.lineTo(W-1,HUD_H+1);ctx.lineTo(W-1,H);ctx.stroke();
}
function drawBricks(){
  for(var i=0;i<bricks.length;i++){
    var b=bricks[i];
    if(b.hp<=0)continue;
    var x=b.x,y=b.y,w=b.w,h=b.h;
    ctx.save();
    if(b.power){
      ctx.shadowColor='rgba(255,255,255,0.55)';
      ctx.shadowBlur=8+3*Math.sin(time*3+b.pulse);
    }
    ctx.fillStyle=b.color;
    rr(x,y,w,h,5);ctx.fill();
    ctx.shadowBlur=0;
    ctx.fillStyle='rgba(255,255,255,0.20)';ctx.fillRect(x+2,y+2,w-4,h*0.36);
    ctx.fillStyle='rgba(0,0,0,0.20)';ctx.fillRect(x+2,y+h*0.55,w-4,h*0.36);
    if(b.maxHp===2&&b.hp===1){ /* cracked tough brick */
      ctx.fillStyle='rgba(10,5,25,0.28)';rr(x,y,w,h,5);ctx.fill();
      ctx.strokeStyle='rgba(20,10,40,0.85)';ctx.lineWidth=1.4;
      ctx.beginPath();
      for(var c=0;c<b.cracks.length;c++){
        var pts=b.cracks[c];
        ctx.moveTo(x+pts[0][0],y+pts[0][1]);
        for(var q=1;q<pts.length;q++)ctx.lineTo(x+pts[q][0],y+pts[q][1]);
      }
      ctx.stroke();
    }
    if(b.power){ /* power-up icon */
      var cx=x+w/2,cy=y+h/2,a=0.65+0.35*Math.sin(time*4+b.pulse);
      ctx.globalAlpha=a;
      ctx.strokeStyle='#ffffff';ctx.fillStyle='#ffffff';
      if(b.power==='wide'){
        ctx.lineWidth=2;ctx.beginPath();
        ctx.moveTo(cx-9,cy);ctx.lineTo(cx+9,cy);
        ctx.moveTo(cx-9,cy);ctx.lineTo(cx-4,cy-3.5);ctx.moveTo(cx-9,cy);ctx.lineTo(cx-4,cy+3.5);
        ctx.moveTo(cx+9,cy);ctx.lineTo(cx+4,cy-3.5);ctx.moveTo(cx+9,cy);ctx.lineTo(cx+4,cy+3.5);
        ctx.stroke();
      }else{
        ctx.beginPath();
        ctx.arc(cx-6,cy+3,2.6,0,TAU);ctx.arc(cx+6,cy+3,2.6,0,TAU);ctx.arc(cx,cy-4,2.6,0,TAU);
        ctx.fill();
      }
      ctx.globalAlpha=1;
    }
    ctx.strokeStyle='rgba(255,255,255,0.25)';ctx.lineWidth=1;
    rr(x+0.5,y+0.5,w-1,h-1,5);ctx.stroke();
    ctx.restore();
  }
}
function drawCapsules(){
  for(var i=0;i<capsules.length;i++){
    var c=capsules[i];
    ctx.save();
    ctx.translate(c.x,c.y);
    ctx.rotate(Math.sin(c.ph)*0.25);
    var col=c.type==='wide'?'#4ade80':'#f472b6';
    ctx.shadowColor=col;ctx.shadowBlur=12;
    ctx.fillStyle='rgba(10,14,30,0.92)';
    rr(-17,-11,34,22,11);ctx.fill();
    ctx.shadowBlur=0;
    ctx.strokeStyle=col;ctx.lineWidth=2;
    rr(-17,-11,34,22,11);ctx.stroke();
    ctx.fillStyle=col;ctx.strokeStyle=col;
    if(c.type==='wide'){
      ctx.lineWidth=2;ctx.beginPath();
      ctx.moveTo(-8,0);ctx.lineTo(8,0);
      ctx.moveTo(-8,0);ctx.lineTo(-4,-3.5);ctx.moveTo(-8,0);ctx.lineTo(-4,3.5);
      ctx.moveTo(8,0);ctx.lineTo(4,-3.5);ctx.moveTo(8,0);ctx.lineTo(4,3.5);
      ctx.stroke();
    }else{
      ctx.beginPath();
      ctx.arc(-6,3,2.6,0,TAU);ctx.arc(6,3,2.6,0,TAU);ctx.arc(0,-4,2.6,0,TAU);
      ctx.fill();
    }
    ctx.restore();
  }
}
function drawParticles(){
  ctx.save();
  ctx.globalCompositeOperation='lighter';
  for(var i=0;i<parts.length;i++){
    var p=parts[i],k=1-p.t/p.life;
    ctx.globalAlpha=k;
    ctx.fillStyle=p.color;
    ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
  }
  ctx.restore();
  ctx.globalAlpha=1;
}
function drawPaddle(){
  var x=paddle.x,y=paddle.y,w=paddle.w,h=paddle.h;
  ctx.save();
  ctx.shadowColor=wideT>0?'rgba(74,222,128,0.8)':'rgba(56,189,248,0.8)';
  ctx.shadowBlur=16;
  var g=ctx.createLinearGradient(x-w/2,0,x+w/2,0);
  if(wideT>0){g.addColorStop(0,'#22c55e');g.addColorStop(0.5,'#bbf7d0');g.addColorStop(1,'#22c55e');}
  else{g.addColorStop(0,'#0ea5e9');g.addColorStop(0.5,'#e0f2fe');g.addColorStop(1,'#0ea5e9');}
  ctx.fillStyle=g;
  rr(x-w/2,y,w,h,7);ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='rgba(10,20,40,0.35)';
  rr(x-2,y+3,4,h-6,2);ctx.fill();
  if(paddle.flash>0){
    ctx.globalAlpha=Math.min(0.8,paddle.flash*4);
    ctx.fillStyle='#ffffff';
    rr(x-w/2,y,w,h,7);ctx.fill();
    ctx.globalAlpha=1;
  }
  ctx.restore();
  if(wideT>0){
    ctx.fillStyle='rgba(74,222,128,0.9)';
    ctx.fillRect(x-w/2,y-8,w*(wideT/10),3);
  }
}
function drawBalls(){
  for(var i=0;i<balls.length;i++){
    var b=balls[i],j;
    for(j=0;j<b.trail.length;j++){
      var t=b.trail[j],k=j/b.trail.length;
      ctx.globalAlpha=k*0.28;
      ctx.fillStyle='#7dd3fc';
      ctx.beginPath();ctx.arc(t.x,t.y,BALL_R*(0.35+0.6*k),0,TAU);ctx.fill();
    }
    ctx.globalAlpha=1;
    ctx.save();
    ctx.shadowColor='rgba(125,211,252,0.9)';ctx.shadowBlur=14;
    var g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,BALL_R);
    g.addColorStop(0,'#ffffff');g.addColorStop(1,'#7dd3fc');
    ctx.fillStyle=g;
    ctx.beginPath();ctx.arc(b.x,b.y,BALL_R,0,TAU);ctx.fill();
    ctx.restore();
  }
}
function drawFloats(){
  ctx.textAlign='center';ctx.textBaseline='middle';
  for(var i=0;i<floats.length;i++){
    var f=floats[i],k=1-f.t/f.life;
    ctx.globalAlpha=k;
    ctx.fillStyle=f.color;
    ctx.font='700 '+f.size+'px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText(f.text,f.x,f.y);
  }
  ctx.globalAlpha=1;
}
function drawHUD(){
  ctx.fillStyle='rgba(7,9,22,0.94)';ctx.fillRect(0,0,W,HUD_H);
  ctx.fillStyle='rgba(110,150,255,0.35)';ctx.fillRect(0,HUD_H-2,W,2);
  ctx.textBaseline='middle';
  ctx.textAlign='left';
  ls('3px');
  ctx.fillStyle='rgba(150,172,225,0.75)';ctx.font='700 11px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText('SCORE',22,17);
  ls('0px');
  ctx.fillStyle='#eef4ff';ctx.font='700 23px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText(String(score),22,38);
  ctx.textAlign='right';
  ls('3px');
  ctx.fillStyle='rgba(150,172,225,0.75)';ctx.font='700 11px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText('LIVES',W-22,17);
  ls('0px');
  for(var i=0;i<3;i++){
    var x0=W-48-26-i*34;
    if(i<lives){ctx.fillStyle='#38bdf8';rr(x0,26,26,7,3.5);ctx.fill();}
    else{ctx.strokeStyle='rgba(120,140,180,0.45)';ctx.lineWidth=1.5;rr(x0,26,26,7,3.5);ctx.stroke();}
  }
  if(wideT>0){
    ctx.textAlign='center';
    ctx.fillStyle='#4ade80';ctx.font='700 12px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('WIDE PADDLE',W/2,16);
    ctx.fillStyle='rgba(74,222,128,0.25)';ctx.fillRect(W/2-52,26,104,5);
    ctx.fillStyle='#4ade80';ctx.fillRect(W/2-52,26,104*(wideT/10),5);
  }else if(demoMode){
    ctx.textAlign='center';
    ctx.fillStyle='rgba(150,172,225,0.55)';ctx.font='700 11px ui-monospace,Menlo,Consolas,monospace';
    ls('2px');ctx.fillText('DEMO - AI PLAYING',W/2,30);ls('0px');
  }
}
function drawPlayHints(){
  var stuck=false;
  for(var i=0;i<balls.length;i++)if(balls[i].stuck){stuck=true;break;}
  if(!stuck)return;
  var a=0.55+0.45*Math.sin(time*5);
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.globalAlpha=a;
  ctx.fillStyle='#dbeafe';
  ctx.font='700 17px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText('CLICK OR PRESS SPACE TO LAUNCH',W/2,372);
  ctx.globalAlpha=a*0.6;
  ctx.font='600 13px system-ui,Segoe UI,Arial,sans-serif';
  ctx.fillText('MOVE - mouse or arrow keys',W/2,396);
  ctx.globalAlpha=1;
}
function titleGradient(text,y,size){
  ctx.font='800 '+size+'px system-ui,Segoe UI,Arial,sans-serif';
  var g=ctx.createLinearGradient(0,y-size*0.55,0,y+size*0.55);
  g.addColorStop(0,'#9be7ff');g.addColorStop(0.55,'#6d9dff');g.addColorStop(1,'#b57cff');
  ctx.fillStyle=g;
  ctx.shadowColor='rgba(105,140,255,0.55)';ctx.shadowBlur=26;
  ctx.fillText(text,W/2,y);
  ctx.shadowBlur=0;
}
function blinkText(text,y){
  ctx.globalAlpha=0.6+0.4*Math.sin(time*4.5);
  ctx.fillStyle='#ffffff';
  ctx.font='800 20px system-ui,Segoe UI,Arial,sans-serif';
  ctx.fillText(text,W/2,y);
  ctx.globalAlpha=1;
}
function drawOverlay(){
  ctx.textAlign='center';ctx.textBaseline='middle';
  if(state==='demo'){
    ctx.fillStyle='rgba(4,6,16,0.5)';ctx.fillRect(0,0,W,H);
    titleGradient('BREAKOUT',240,80);
    ls('6px');
    ctx.fillStyle='rgba(158,183,255,0.8)';ctx.font='700 15px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('NEON BRICK BREAKER',W/2,300);
    ls('0px');
    ctx.fillStyle='rgba(196,208,240,0.85)';ctx.font='600 15px system-ui,Segoe UI,Arial,sans-serif';
    ctx.fillText('MOVE - mouse or arrow keys  |  LAUNCH - click or space',W/2,348);
    ctx.globalAlpha=0.6+0.4*Math.sin(time*4.5);
    ctx.fillStyle='#ffffff';
    ctx.shadowColor='rgba(125,211,252,0.9)';ctx.shadowBlur=18;
    ctx.font='800 21px system-ui,Segoe UI,Arial,sans-serif';
    ctx.fillText('CLICK OR PRESS ANY KEY TO START',W/2,414);
    ctx.shadowBlur=0;ctx.globalAlpha=1;
    ctx.fillStyle='rgba(140,160,205,0.65)';ctx.font='500 13px system-ui,Segoe UI,Arial,sans-serif';
    ctx.fillText('Demo mode - the AI is playing. Take over whenever you like!',W/2,470);
  }else if(state==='over'){
    ctx.fillStyle='rgba(4,6,16,0.68)';ctx.fillRect(0,0,W,H);
    ctx.fillStyle='#f87171';ctx.shadowColor='rgba(248,113,113,0.7)';ctx.shadowBlur=28;
    ctx.font='800 64px system-ui,Segoe UI,Arial,sans-serif';
    ctx.fillText('GAME OVER',W/2,238);
    ctx.shadowBlur=0;
    ctx.fillStyle='#e8f0ff';ctx.font='700 30px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('SCORE  '+score,W/2,312);
    var cleared=0;
    for(var i=0;i<bricks.length;i++)if(bricks[i].hp<=0)cleared++;
    ctx.fillStyle='rgba(160,178,220,0.8)';ctx.font='600 16px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('BRICKS  '+cleared+' / '+(COLS*ROWS),W/2,348);
    blinkText('CLICK OR PRESS ANY KEY TO RESTART',414);
  }else if(state==='win'){
    ctx.fillStyle='rgba(4,6,16,0.62)';ctx.fillRect(0,0,W,H);
    ctx.font='800 72px system-ui,Segoe UI,Arial,sans-serif';
    var g=ctx.createLinearGradient(0,202,0,274);
    g.addColorStop(0,'#fef3c7');g.addColorStop(0.5,'#fbbf24');g.addColorStop(1,'#f59e0b');
    ctx.fillStyle=g;
    ctx.shadowColor='rgba(251,191,36,0.65)';ctx.shadowBlur=30;
    ctx.fillText('YOU WIN!',W/2,238);
    ctx.shadowBlur=0;
    ctx.fillStyle='#e8f0ff';ctx.font='700 30px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('SCORE  '+score,W/2,312);
    if(winBonus>0){
      ctx.fillStyle='#4ade80';ctx.font='700 17px ui-monospace,Menlo,Consolas,monospace';
      ctx.fillText('+'+winBonus+' LIFE BONUS',W/2,346);
    }
    blinkText('CLICK OR PRESS ANY KEY TO PLAY AGAIN',414);
  }
}
function render(){
  resize();
  ctx.setTransform(scaleF,0,0,scaleF,0,0);
  drawBackground();
  ctx.save();
  if(shake>0)ctx.translate(rand(-shake,shake),rand(-shake,shake));
  drawBricks();
  drawCapsules();
  drawParticles();
  drawPaddle();
  drawBalls();
  drawFloats();
  ctx.restore();
  drawOverlay();
  drawHUD();
  if(state==='playing')drawPlayHints();
}

/* ================= input ================= */
function primaryAction(){
  if(state==='demo'||state==='over'||state==='win')resetGame(false);
}
window.addEventListener('keydown',function(e){
  if(e.code==='ArrowLeft'||e.code==='ArrowRight'||e.code==='Space')e.preventDefault();
  keys[e.code]=true;
  if(e.code==='ArrowLeft'||e.code==='ArrowRight'||e.code==='KeyA'||e.code==='KeyD')mouseControl=false;
  ensureAudio();
  if(e.code==='Space'&&state==='playing')launchAll();
  else primaryAction();
});
window.addEventListener('keyup',function(e){keys[e.code]=false;});
window.addEventListener('pointermove',function(e){
  var r=canvas.getBoundingClientRect();
  if(r.width>0)mouseX=(e.clientX-r.left)/r.width*W;
  mouseControl=true;
});
window.addEventListener('pointerdown',function(e){
  ensureAudio();
  var r=canvas.getBoundingClientRect();
  if(r.width>0)mouseX=(e.clientX-r.left)/r.width*W;
  mouseControl=true;
  if(state==='playing')launchAll();
  else primaryAction();
});

/* ================= boot ================= */
resize();
resetGame(true); /* self-playing demo behind the start message */
var last=performance.now();
function frame(now){
  var dt=(now-last)/1000;last=now;
  if(dt>0.05)dt=0.05;
  update(dt);
  render();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Reasoning trace (33,128 tokens)
Let me build a polished Breakout game in a single HTML file. Requirements:

1. Paddle controlled with mouse and left/right arrow keys
2. Bounce angle off paddle depends on hit position
3. One level with at least 6 rows of bricks, mixing normal and 2-hit tougher bricks that visibly crack
4. Some bricks visibly carry a power-up icon; when broken they drop it — wider paddle (10s) or multi-ball (3 balls)
5. Ball must never pass through bricks or paddle (tunneling prevention)
6. Particles when brick breaks
7. 3 lives, score displayed
8. Game over and victory screens with restart
9. Must start in self-playing demo mode behind "Click or press a key to start" message
10. No console errors, works in sandboxed iframe, no localStorage etc.

Key implementation details:

**Canvas setup**: Fixed logical size (e.g., 1280×800 or something like 900×640), scaled with CSS to fit viewport while maintaining aspect ratio. Use devicePixelRatio for crispness. Actually simpler: canvas fixed internal resolution, CSS scales it. Handle mouse coordinates by scaling.

Let me use a logical game size of 960×720 or so. Let's do 900×640? The viewport is 1280×800. Let me make canvas 960×600 with aspect fit. Actually let's do a nice layout: canvas logical size 960×640, CSS max-width/height with aspect-ratio preserved.

**Collision — tunneling prevention**: Use continuous collision or sub-stepping. Sub-step: move ball in small increments (e.g., steps = ceil(speed / min(brickHeight, ballRadius)) each frame, checking collisions each step). That's robust and simple. Ball speed ~ 420 px/s, dt ~ 16ms → ~7px per frame; brick height ~ 22px, ball radius 7. Sub-stepping at 4px increments is safe. Alternatively do swept AABB vs bricks. Sub-stepping is fine.

Actually a more elegant approach: swept circle vs AABB is complex; sub-stepping with proper resolution (reflect on axis with least penetration) works well.

**Paddle bounce angle**: hitPos = (ballX - paddleCenter) / (paddleHalfWidth). Clamp to [-1, 1]. Angle = hitPos * maxAngle (e.g., 60°). New velocity: speed * sin(angle) horizontally, -speed * cos(angle) vertically. That gives center = straight up, edges = sharp angle.

**Bricks**: grid, e.g., 12 columns × 8 rows. Mix: top rows tougher (2 HP), bottom rows normal. Tough bricks show crack overlay after first hit. Power-up bricks: random selection, ~6 bricks carry icons — draw a small icon inside the brick (e.g., "W" glyph for wide, "×3" or three dots for multiball). When broken, drop a falling capsule with the icon; catch with paddle to activate.

- Wide paddle: 10 seconds, paddle width increases (e.g., 1.5×), timer displayed. Maybe animate shrink at end.
- Multi-ball: spawn 2 extra balls (total 3) from the position of an existing ball or from paddle? Requirement says multi-ball (3 balls). So when caught, if there's 1 ball, spawn 2 more (total 3). If multiple balls, spawn up to 3 total? Simplest: clone balls until total 3, each new ball with slightly different angle.

**Lives**: 3. Ball lost when all balls gone. Reset ball on paddle (launch on... in demo mode AI launches automatically; for the player, launch with click or space or after moving? Let's have the ball stick to the paddle until click/space/auto-launch after a moment). Actually to keep it simple: ball launches automatically after sticking for a short moment? For player control, launch on click or Space. Show hint. In demo mode, AI launches immediately.

**Demo mode / AI**: Before start, game plays itself: AI paddle tracks the ball's predicted landing x with some lag/error, auto-launches balls. Overlay: "Click or press a key to start". When user clicks or presses key, switch to player control, reset game (fresh level, 3 lives, score 0). Actually maybe demo runs the same game; on start, reset and begin.

Keep the AI playing during demo; when the demo game ends (game over/victory), restart demo automatically.

**Screens**: 
- Start/demo overlay: title "BREAKOUT", instructions, "Click or press a key to start".
- Game over: score, "Click to restart" (or press key).
- Victory: same.
These drawn on canvas or DOM overlay? Canvas-drawn keeps it self-contained. I'll draw overlays on canvas with semi-transparent dark backdrop.

**Particles**: On brick break, spawn ~14 particles with brick color, velocity spread, gravity, fade. Also small spark particles on paddle hit maybe.

**Score**: brick break +points (tough brick more), power-up catch +50. Display top bar with score, lives (draw small paddles or hearts), level maybe.

**Extra polish**:
- Trail on ball (fading circles or motion streak).
- Screen shake tiny on brick break? Subtle.
- Brick colors per row (gradient palette).
- Glow effects via shadowBlur sparingly (performance ok).
- Combo multiplier? Keep simple: maybe consecutive brick hits without paddle touch increase combo, +bonus. Could add but keep scope manageable. I'll add a small combo display maybe not necessary. Skip combo to keep code tight but polished.

**Visual style**: Dark background with subtle grid/vignette. Neon-ish bricks. Top HUD bar drawn on canvas. Walls: draw side/top borders.

Let me plan the layout in logical coordinates:
- Canvas: W=960, H=640.
- HUD height: top 48px area (score left, lives center/right, power-up timers right).
- Walls: playfield inset? Simpler: playfield edges at x=0..W, top at y=HUD (56). Bricks start y=90.
- Bricks: 12 cols, rows 8. Brick area width: leave margin 24 each side → 912 wide, brick width = 912/12 = 76, gap 4? Let's compute: padding 20, cols 12, gap 5. brickW = (960-40-11*5)/12 = (920-55)/12 = 865/12 ≈ 72. Let's just compute programmatically: brickW = (W - 2*margin - (cols-1)*gap) / cols. brickH = 24. Rows: 8 rows starting y = 100, gap 5 → total height 8*29 = 232, ends ~332. Paddle at y = 600. Ball radius 7.

Row composition (8 rows):
- Row 0-1: tough (2 HP), color red/orange, score 30.
- Row 2-3: tough? Mix: maybe rows 0-2 tough (3 rows), rows 3-7 normal. Requirement: "mixing normal bricks and tougher bricks". Also "at least 6 rows" — 8 rows good.
- Power-ups: randomly assign ~7 bricks across grid, avoiding top-left? Just random with no duplicates per row adjacency concerns—fine random. Ensure at least e.g. 3 wide and 3 multi icons. I'll assign icons: pick random cells, assign alternately.

Better: deterministic-ish assignment: shuffle all brick positions, take first 8, assign 4 wide, 4 multi. Icons drawn inside brick.

**Power-up capsules**: falling at ~140 px/s, wobble slightly (sin). Drawn as rounded capsule with letter/icon. Catch when overlapping paddle → activate. Miss → falls off bottom, maybe small negative? No, just lost.

- Wide: paddleWidth target = base * 1.55 for 10 s. Timer bar in HUD or around paddle glow. When active and ball respawns after life lost, reset paddle width to base (or keep timer?). Simpler: losing a life clears power-ups. Timer displayed in HUD with small bar.
- Multi-ball: on catch, for each existing ball spawn clones up to 3 total. E.g., if balls.length === 1, add 2 clones with ±25° rotation of velocity. If more balls exist, add up to 3 max. Cap total balls at like 6 for sanity? Requirement: 3 balls. I'll make total become exactly... "multi-ball (3 balls)": spawn until count is 3 (if fewer). If already ≥3, maybe spawn 2 more anyway up to cap 6? I'll do: target count = max(3, balls+2)? Keep simple: while (balls.length < 3) spawn clone. Plus if balls.length >= 3, still add 2 (cap 6)? Hmm. Simplest faithful: ensure 3 balls: add clones until 3. If somehow more, leave. I'll implement spawnMultiball(): const need = 3 - balls.length; for i<need clone ball i with rotated velocity. That matches "3 balls".

**Ball-brick collision with sub-stepping**: 
Each frame: compute steps = Math.ceil((speed*dt) / 6) (6px max step). For each step: move x,y by vx*dt/steps. Then check walls, paddle, bricks. Resolve collisions by reflection.

Brick collision: find bricks whose AABB expanded by ball radius contains ball center. Determine penetration on each axis: compute overlapX = (ball.r + brick.w/2) - |ball.x - brickCx|, overlapY similarly. Reflect on axis with smaller overlap (axis of least penetration), push ball out. Damage brick.

To handle multiple collisions per step, break after first brick hit per step (with sub-steps it's fine).

Paddle collision: only when ball moving down and ball bottom >= paddle top and within x range (+radius). Reflect using angle formula. Also push ball above paddle top. Prevent multiple triggers: after bounce, ball vy negative.

Edge case: ball hitting paddle side — with angle-based reflection it's fine.

Wall collisions: left/right reflect x; top (y = hudTop) reflect y; bottom: ball lost when y - r > H.

**Paddle movement**: 
- Mouse: mousemove over canvas → target paddle center x = scaled mouse x. Paddle follows (maybe directly, with slight smoothing for demo? For player, direct positioning is fine but smooth follow with lerp factor ~0.5-1.0 per frame; direct is fine. I'll do direct set but clamp).
- Keys: left/right arrows move at ~600 px/s. Track key state.
- Combined: keys move paddle; mouse sets target when moved. Keep lastMouseX; when arrow key pressed, ignore mouse target until mouse moves again.

**Demo AI**: compute predicted ball landing x: simulate ball with wall bounces until y reaches paddle top (simple: linear prediction with reflections off left/right walls, ignoring bricks — fine for demo; occasionally add error so it sometimes misses? Demo should look competent but it's okay if it occasionally loses a life—game restarts demo. Add small noise + reaction lag). AI also auto-launches stuck balls after 0.6s. AI catches power-ups naturally by tracking ball; capsules falling — AI could target capsule if close? Keep simple: AI tracks ball only; catches whatever happens.

Also AI paddle max speed limit (e.g., 500 px/s) so it looks natural.

**Ball stuck to paddle**: state ball.stuck = true, position follows paddle + offset; launch on click/space (player) or timer (demo). Launch angle: slight random or based on offset. Initial launch: angle = -75°..-90°? Straight-ish up with tiny random.

**Speed**: ball base speed ~ 430 px/s, increases slightly per brick broken (+2 per break, cap ~640) or over time. Add small speedup per paddle hit? Keep modest: speed increases 6 px per brick hit capped.

**Sound**: Could add WebAudio beeps — optional, sandbox allows audio after user gesture; but demo autoplays without gesture → AudioContext may be suspended; wrap in try/catch and resume on first interaction. Sounds add polish. I'll add tiny synth sounds (square blips) with a mute-safe wrapper. Keep volume low. On first user gesture, resume context. If suspended during demo, just skip (context.state !== 'running' → skip). Fine.

**Restart**: On game over/victory overlay: "Click or press a key to play again". Restart resets everything, starts player game (not demo).

**Pause**: Maybe P to pause — optional. Add Escape/P pause? Not required; skip to reduce complexity. Actually a pause is nice but let's keep scope. Skip.

**HUD**: 
- Left: "SCORE 001240" 
- Center: title small "BREAKOUT" maybe not; better: lives as mini-paddles.
- Right: active power-up timers ("WIDE 6.2s", "MULTI"?) and level? Only one level. Show "HIGH" no (no storage). Keep: Score left, Lives right, power-up indicator under score or right.

Also show "WIDE" timer bar top-right when active.

**Fonts**: Use system stack with fallback; canvas text with 'Segoe UI'/'Arial'. Maybe monospace for score: 'ui-monospace, Consolas'. No external fonts allowed.

**Canvas scaling**: 
CSS: canvas { width: min(96vw, calc(96vh * 1.5)); aspect-ratio: 3/2 } with logical 960×640 (1.5 ratio). 1280×800 viewport: 96vh*1.5 = 1152 ≤ fine. On mobile 375px: canvas ~ 375 wide, height 250 — playable-ish; also handle touch? Not required but mouse events also fire from touch? Add touchmove handler mapping to paddle x for bonus. Small effort, include.

DPR: set canvas.width = 960*dpr etc., ctx.scale(dpr,dpr) once. But CSS scaling handles display size; internal resolution 960×640 might look slightly soft when scaled up to 1152 wide. Better: render at DPR-scaled backing store relative to displayed size? Simpler robust approach: fixed logical coordinate system 960×640; backing store = 960*dpr... but CSS may scale beyond. Use transform trick: on resize, set canvas.width = cssWidth * dpr, and ctx.setTransform(scale) where scale = cssWidth*dpr/960. This keeps crispness. Let me implement:

```
function resize() {
  const rect = canvas.getBoundingClientRect();
  const dpr = window.devicePixelRatio || 1;
  canvas.width = Math.round(rect.width * dpr);
  canvas.height = Math.round(rect.height * dpr);
  scaleFactor = canvas.width / LOGICAL_W;
}
```
In render: ctx.setTransform(scaleFactor,0,0,scaleFactor,0,0). Since aspect ratio fixed by CSS, scaleY matches. Good.

Mouse mapping: (e.clientX - rect.left) / rect.width * LOGICAL_W.

**Page layout**: Full-viewport flex center, dark background (#0b0e1a-ish), maybe subtle radial gradient. Canvas with border/glow, rounded corners. Title above? Everything in canvas is cleaner. Body margin 0, overflow hidden. Add a subtle outer glow via CSS box-shadow.

Prevent horizontal scrollbar: html,body { width:100%; height:100%; margin:0; overflow:hidden }.

**Code structure**:

```js
const LOGICAL_W = 960, LOGICAL_H = 640;
const HUD_H = 56;
let state = 'demo' | 'playing' | 'gameover' | 'victory' | 'paused'? 
```

Game object with reset(demoMode).

Bricks array: {x,y,w,h,hp,maxHp,color,hasPower,type:'wide'|'multi',alive}. Cracked visual: if maxHp==2 && hp==1 draw crack lines (predrawn random cracks per brick — generate crack segments at creation for determinism, or draw simple zigzag based on brick seed).

Draw crack: with ctx lines: from center-ish random zigzag. Precompute crack path points per brick when created (only for tough bricks). Draw dark lines.

Brick colors by row: palette from top: reds → orange → yellow → green → cyan/blue. Tough bricks: purple/indigo family? Let's design: rows 0-2 tough (color: '#a78bfa' violet, '#8b5cf6'), rows 3-7 normal with gradient palette ['#f87171','#fb923c','#fbbf24','#34d399','#22d3ee','#60a5fa']. Hmm 8 rows: rows 0-1 tough violet, row 2 tough? Let me do rows 0-2 = tough (violet shades), rows 3-7 = normal (5 colors). Tough score 25, normal 15? Common: top rows worth more. Fine.

Power icon: draw inside brick center: for 'wide', an icon like ↔ (two triangles + line) or letter 'W'? A capsule icon: draw small horizontal double arrow. For 'multi': three small dots (circles). Draw white with slight glow. Also subtle pulsing brightness.

**Particles**: array {x,y,vx,vy,life,maxLife,color,size}. gravity 700. On break: 16 particles. On ball lose: small burst too maybe. Update & draw as squares/circles fading. Also add "spark" on wall/paddle hit: 4 particles.

**Screen shake**: shake magnitude decays; on brick break shake += 2 (max 6). Apply translate in render. Subtle.

**Ball trail**: store last N positions per ball (e.g., 10) and draw fading circles. Or draw with globalAlpha. Cheap.

**Main loop**: requestAnimationFrame with dt clamp (max 32ms). Fixed sub-step for physics inside update.

**Ball-lost**: when balls empty: lives--; if lives<=0 → gameover (state, show overlay). else spawn stuck ball on paddle (clears wide power? typical arkanoid keeps; I'll clear wide to keep paddle standard? Actually keep active timers running but reset paddle width if wide... simpler: clear power-ups on life loss. Hmm, wide paddle remaining could be nice. I'll clear power-up effects on death — classic behavior-ish.)

Multiball balls that fall: just removed; no life lost as long as one ball remains.

**Victory**: all bricks alive==0 → victory screen, score bonus maybe +500.

**Demo specifics**: 
- AI computes targetX: predict ball path: iterate ball position with vx,vy, reflect off walls (x in [r, W-r], top y=HUD+r) until y >= paddleY - r, up to N steps. If ball stuck, target = paddle center. Add noise ±30 and clamp paddle speed.
- If no balls (life lost during demo), auto reset lives? Demo has lives too; if demo game over → restart demo (new game) automatically after 1.5s. 
- AI launch: stuck timer > 0.5s → launch.
- Overlay drawn: title "BREAKOUT", subtitle, blinking "Click or press a key to start", plus small "Demo — AI is playing" tag. Keep game visible behind (overlay semi-transparent dark at bottom? Make overlay covering whole canvas with alpha 0.55 so game visible).

When user clicks/keys: state 'playing', resetGame(player). First gesture also resumes audio.

During 'playing' click also launches stuck ball. Careful: the click that starts the game shouldn't immediately launch? It's fine if it does—actually resetting on click then that same click shouldn't launch. Use event order: start on click → set state playing, resetGame → ball stuck; the same click event already consumed. Launch only on subsequent click. Good.

Keys: ArrowLeft/ArrowRight held. Space launches. Also 'A'/'D'? optional; add for convenience.

**Text on canvas**: crisp: ctx.textAlign etc. Use font '700 22px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' for HUD numbers and '700 64px system-ui' title.

**Victory/GameOver overlays**: dark overlay, big title ("YOU WIN!" / "GAME OVER"), final score, bricks destroyed?, "Click or press a key to restart". Maybe starfield background behind game? Background: subtle animated gradient? Keep static vignette + faint grid.

Now write code carefully to avoid bugs.

Detailed constants:
```
W=960,H=640,HUD=56
paddleBaseW=110, paddleH=14, paddleY=H-36 (=604)
ballR=7
speed base 430, max 660
brick rows=8, cols=12, margin=20 (left/right), gap=5, top=96, brickH=22
brickW=(960-40-55)/12=72.08 → compute float fine.
```
Rows 8 → bottom = 96 + 8*(22+5) - 5 = 96+216-5=307. Good gap to paddle.

Row config:
```
const rowDefs = [
 {hp:2, c1:'#7dd3fc'?...}
```
Let me choose palette (top → bottom):
- r0: tough '#f472b6' pink? Let's use violet family for tough: base '#8b5cf6' (violet), lighter top? I'll give each brick a two-tone: fill main color, top highlight strip lighter, bottom shade darker for depth. Crack overlay dark.

Colors:
tough rows (0-2): ['#c084fc','#a78bfa','#818cf8'] (purple→indigo)
normal rows (3-7): ['#38bdf8','#2dd4bf','#4ade80','#facc15','#fb923c'] (sky, teal, green, yellow, orange)

Hmm that's cyan→orange nice gradient. Scores: tough 30, normal 20? Give row-based: normal 10 + (7-row)? Keep flat: normal 15, tough 30. Power catch 50. Victory bonus 500? plus remaining lives*100? Show "Life bonus +200". Keep: victory adds lives*100 bonus shown.

Power-up assignment: collect all bricks indices, shuffle, take 8, assign: first 4 'wide', next 4 'multi'? Randomly shuffle types too. Ensure icons visible. 8 of 96 bricks ~ 8%. During demo, catches happen sometimes.

Power-up drop: capsule {x (brick center), y, vy:130, type, w:34,h:20, wob}. Draw rounded rect with color (wide: '#4ade80' green with ↔ icon; multi: '#f472b6' pink with 3 dots) and pulsing outline. Catch: overlap with paddle rect → apply.

Wide effect: paddle.wTarget = base*1.6 (176). Animate width toward target with lerp for smoothness. timer 10s. If caught again while active → reset timer to 10. HUD shows remaining bar.

Multi effect: instant. Small flash. New balls: clones of a random ball (prefer first) with velocity rotated ±22°, ensure not duplicate identical. Position slightly offset to avoid immediate overlap with bricks? They spawn at ball position—could overlap a brick; sub-step collision handles (they'll bounce). Fine. Ensure clone not stuck downward? Rotation keeps speed same direction. If the source ball is stuck to paddle... multiball caught while a ball stuck: clones from a moving ball only; if only stuck ball exists, launch clones upward at angles from paddle? Handle: if all balls stuck, spawn 2 at paddle with angles -60°,-120°. Edge case handled simply: spawn from paddle position with upward angles.

Ball stuck offset: ball.stuckOffset = ball.x - paddle.x at time of sticking (or 0 initially).

Physics update per ball:
```
if (ball.stuck) { ball.x = paddle.x + ball.offset; ball.y = paddle.y - ballR - 1; return; }
const speed = hypot(vx,vy);
steps = Math.max(1, Math.ceil(speed*dt/6));
sdt = dt/steps;
for step:
  x+=vx*sdt; y+=vy*sdt;
  // walls
  if (x < r + wallPad) {x = r+wallPad; vx=abs(vx); spark}
  ...
  top: y < HUD + r → y=HUD+r; vy=abs(vy)
  // paddle
  if (vy>0 && y+r >= paddle.y && y+r <= paddle.y+paddle.h+ ... && |x-paddle.x| <= paddle.w/2 + r)
     reflect via angle; y = paddle.y - r; 
     Also apply paddle "english": add tiny influence of paddle velocity? skip.
  // bricks: find hit
```
Paddle collision condition more precisely: ball crossing paddle top plane this step: prev y? Since sub-steps small (6px) and paddle h 14, using y+r >= py && y - r <= py+ph && horizontal overlap works; reflect and clamp y = py - r. Only when vy > 0.

Angle formula:
```
const hit = clamp((x - paddle.x) / (paddle.w/2), -1, 1);
const maxAng = 60 * PI/180;
const ang = hit * maxAng;
const sp = speed; // preserve
vx = sp * Math.sin(ang);
vy = -sp * Math.cos(ang);
```
Center → straight up. Edge (±1) → 60° from vertical, i.e., vx = .866sp, vy=-.5sp. Good sharp angle.

Guarantee min vertical: with maxAng 60°, vy min magnitude = 0.5sp — fine, never too horizontal.

Brick collision per step:
```
for brick of alive bricks near:
  if (x + r > b.x && x - r < b.x+b.w && y + r > b.y && y - r < b.y+b.h) {
     // resolve: penetration
     const px = (b.w/2 + r) - Math.abs(x - (b.x+b.w/2));
     const py = (b.h/2 + r) - Math.abs(y - (b.y+b.h/2));
     if (px < py) { x += (x < cx ? -px : px); vx = -vx; }
     else { y += (y < cy ? -py : py); vy = -vy; }
     hitBrick(b, ball);
     break; // one brick per sub-step
  }
```
hitBrick: hp--; spark particles small; if hp<=0: alive=false; spawn particles, drop power if any, score += pts, speedUp. If hp==1 && maxHp==2: show crack, score += 10 (chip points)? Give +10 for crack hit, +30 on break. Simple: crack hit +10, break +30 (tough) / +15 (normal)? Let's do: each hit on tough = 15, final break of tough total extra? Simplify: normal brick break +20; tough crack +10, tough break +30 (total 40). Fine.

Sound blips: freq by event.

Speed increase: on brick break, global ballSpeed target += 4 up to max; existing balls accelerate: multiply velocity to new magnitude on next paddle hit? Changing mid-flight direction fine: rescale each ball velocity to new speed gradually. Simpler: store game.ballSpeed; on paddle bounce use ballSpeed; also after brick break, rescale each ball's velocity to ballSpeed immediately (keeps consistency). I'll rescale on brick break for all balls (cheap, n≤6).

Speed also +8 per paddle bounce? That'd ramp fast. Skip; ramp by bricks only, plus tiny time-based ramp? Keep bricks only.

**Rendering order**: bg, grid vignette, bricks, cracks, power icons, capsules, particles, paddle (with glow, wide timer arc?), balls + trails, HUD, overlays, shake transform around everything except overlay? Apply shake to world only.

**Paddle draw**: rounded rect, gradient (white-ish center to cyan edges?), glow when wide active (green tint + remaining time ring or bar under paddle). Let's draw timer as thin bar above paddle? HUD right side shows "WIDE" with bar. Also tint paddle green while active.

**Balls draw**: white core with cyan glow (shadowBlur 12 — okay for ≤6 balls + trail... shadowBlur is expensive but fine at this scale). Trail: previous positions, draw circles decreasing radius/alpha without shadow.

**HUD**: 
- bg: rect 0..HUD dark #0e1226 with bottom border line gradient.
- Score: left "SCORE" small label + value monospace.
- Lives: right: draw 3 mini paddles (rounded rects 26×6) filled/dimmed.
- Power indicator: center-right? Place after lives: if wide active, text "WIDE 6s" + bar. Also "MULTI" flash briefly when caught (just particle burst + text popup "MULTIBALL!" floating text). Floating texts: array {text,x,y,life,color} rising & fading. Nice polish for score pops too (+30 at brick pos).

Floating score texts on brick break — yes, small.

**Overlays**:
- Demo: centered panel: big title "BREAKOUT" with gradient fill; subtitle "neon brick breaker"; blinking "CLICK OR PRESS ANY KEY TO START"; below: "Watch the demo — the AI is playing"; controls hint: "MOUSE / ← → to move · SPACE to launch".
- Playing: nothing.
- Ball stuck (player): hint text above paddle "SPACE / CLICK to launch" (only until first launch? show each time briefly).
- Game over: "GAME OVER" red-ish, "Score: N", "Bricks: x/96"? show score + "CLICK OR PRESS ANY KEY TO RESTART".
- Victory: "YOU WIN! 🎉" — avoid emoji maybe; draw text "YOU WIN!" with gold gradient. Score + life bonus. Restart hint.

Blink via time-based alpha.

**State transitions**:
- state: 'demo', 'playing', 'over', 'win'.
- In demo: full game runs; on over/win → after 1.2s delay, resetGame(demo) again.
- startPlayer(): resetGame(false).
- In 'over'/'win': input → startPlayer().

**Input handling**:
```
window.addEventListener('keydown', e => {
  if (['ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
  keys[e.code]=true;
  handleAnyKey();
  if (state==='playing') { if (e.code==='Space') launchStuck(); }
});
```
Prevent scrolling with space/arrows — iframe sandbox: preventDefault works.

Click: on canvas (or window?): pointerdown on window so clicking anywhere starts. But ensure click on page. Use window 'pointerdown'. Also 'mousemove' on window maps paddle.

For touch: pointermove with pointerType touch also maps. pointermove covers mouse+touch. Use pointermove → paddle target. But keys vs mouse conflict: last input wins: mousemove sets mouseX target & mouseActive=true; keydown arrows sets mouseActive=false... Actually mouseActive should disable when arrows pressed, re-enable on pointer move. Implement: on pointermove: mouseControl=true. On arrow keydown: mouseControl=false.

Paddle update:
```
if (left) paddle.x -= 620*dt; right += ...
if (mouseControl && !left && !right) paddle.x += (mouseX - paddle.x) * min(1, dt*20); // smooth follow
clamp paddle.x to [w/2+wall, W-w/2-wall]
```
Walls: side walls at x=0..W? I said bricks margin 20; ball bounces at x=r and x=W-r. Playfield full canvas width. Draw subtle side wall glow lines.

Actually give playfield visible frame: stroke rect border with glow. Top boundary at HUD bottom.

**Crack rendering**: For tough brick with hp==1: draw crack: precomputed polyline(s). At brick creation for maxHp 2, generate cracks: 2 polylines each of 4 points random within brick. Draw with strokeStyle rgba(20,10,40,0.85), lineWidth 1.5. Also darken brick slightly (overlay black alpha .15).

**Brick body**: rounded rect radius 4, fill main color; inner top highlight: rect with lighter (use globalAlpha .35 white top 40%); bottom inner shadow. Simple: draw base color, then top half overlay 'rgba(255,255,255,.18)', bottom 'rgba(0,0,0,.18)'. Stroke 'rgba(255,255,255,.15)'.

Power icon: brick with power: draw pulsing white icon centered:
- wide: horizontal line with arrowheads: line from -9..9 at y0, arrows. Or double-headed arrow.
- multi: three circles arranged triangle.
Pulse: alpha = .6+.4*sin(t*4 + seed).

Also add subtle outline glow for power bricks: stroke 'rgba(255,255,255,.5)'.

**Particles**: bricks breaking: color from brick; count 14; vx random ±180, vy -220..80; gravity 900; life .5-.9s; size 2-4 squares rotating? draw as rects with alpha life-based. Additive glow? use ctx.globalCompositeOperation 'lighter' for particles — nice neon. Careful perf fine.

Sparks on paddle/wall: 3-5 small white.

**Floating texts**: {text,x,y,vy:-40,life:0.8,color,size}. Draw with alpha.

**Sound** (tiny):
```
let AC=null;
function ensureAudio(){ if(!AC){ try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){} } if(AC&&AC.state==='suspended') AC.resume().catch(()=>{}); }
function beep(freq,dur=0.06,type='square',vol=0.04){ if(!AC||AC.state!=='running')return; ... osc + gain envelope }
```
Call ensureAudio on first user gesture. During demo before gesture, AC null → silent (good, autoplay-safe).
Events: brick hit (440+row?), brick break (220 square + higher), paddle (330), wall (200 quieter), power catch (arpeggio 523/659/784), ball lost (descending 200→100), game over (low), win (melody). Keep minimal but nice. Implement beep with detune param. Add tiny arpeggio helper: scheduled beeps with setTimeout? Use AC time scheduling: pass when = AC.currentTime + offset. Extend beep(freq,{delay}).

Careful: zero console errors. Wrap in try/catch.

**HUD lives icons**: for i<3: rounded rect at right; filled if i<lives else stroke faint.

**Wide timer HUD**: if wideT>0: draw at right side (left of lives?) text 'WIDE' + bar width 60 * remaining/10, green.

**Ball trail**: per ball keep array trail of {x,y}; push each frame (cap 12). Draw before ball: for i, alpha = i/len * 0.25, radius scaled r*(0.4+0.6*i/len). Use ball color cyan #9be8ff.

**Game reset**:
```
function resetGame(demo){
  state = demo?'demo':'playing';
  score=0; lives=3; ballSpeed=430; combo? no
  buildBricks();
  balls=[]; particles=[]; capsules=[]; floats=[];
  paddle = {x:W/2, w:paddleBaseW, targetW:paddleBaseW, y:paddleY, h:14};
  wideT=0; 
  spawnStuckBall();
  demoTimer=0; overDelay=0;
  shake=0;
}
```
spawnStuckBall: ball {x:paddle.x, y:paddle.y-r-1, vx:0,vy:0, stuck:true, offset:0, stickT:0}.

launch: stuck balls → vx = sin(a)*speed with a random ±20°, vy=-cos.

For demo AI launch automatically after 0.55s stick.
For player: click or space. Show hint text while stuck: pulsing "CLICK OR PRESS SPACE TO LAUNCH" above paddle... draw under bricks area center y ~ 380.

**AI**:
```
function aiUpdate(dt){
  // find first non-stuck ball (prefer lowest? the one closest to paddle / soonest to land)
  let target = paddle.x;
  let best = null; let bestT=1e9;
  for (ball of balls) if(!ball.stuck){
     // predict
     let px=ball.x, py=ball.y, pvx=ball.vx, pvy=ball.vy, t=0;
     // simulate until py >= paddle.y - r (and vy>0 eventually). Simple loop steps of 1/120 up to 4s
     ... reflect at walls.
     if (t < bestT) {bestT=t; target=predictX;}
  }
  if (best===null) target = W/2; // all stuck → center
  target += aiNoise; // noise updated occasionally (every 0.4s random ±26)
  // move paddle toward target with max speed 480
}
```
Prediction loop: dt small 1/60, max 300 iters. If pvy<0, it will bounce off top; fine simulate until py>=paddle.y - ballR && pvy>0.

Also AI catches capsules? It tracks the soonest ball; capsules often missed. To make demo show power-ups, bias: if a capsule is within 120px above paddle, target capsule x (weighted priority higher than ball? If AI chases capsule it might miss ball...). Compromise: if capsule y > H-160 and |capsule.x - target| < 90, target capsule.x (likely ball also heading near). This will show wide/multiball activations in demo. Good.

AI stuck launch: if any stuck ball, stickT>0.55 → launch.

Also demo AI should be decent but not perfect: noise resampled every 0.5s ±24 px, plus maxSpeed 460 (ball horizontal can outrun at edges → occasional misses → drama). Fine.

**Overlay drawing** helper: 
```
function overlay(alpha){ ctx.fillStyle=`rgba(6,8,20,${alpha})`; ctx.fillRect(0,0,W,H); }
```

Title style: gradient text: create linearGradient.

Blinking: alpha = .55+.45*sin(t*4)? Use smooth pulse.

**Victory bonus**: on win: score += lives*100 (apply once when transitioning; store). Show "LIFE BONUS +300".

Implement transition: when last brick broken and balls remain → state 'win', winTime. Also stop balls.

Wait — multiball clones could break bricks after? On win, freeze updates (state check). Show overlay after small delay 0.5s? Immediately fine with particles still updating? Let particles/capsules continue updating in win/over states for polish (update particles & floats always; balls stop). I'll keep updating particles/floats/capsules? capsules irrelevant. Update particles + floats + shake decay in all states.

**Game over**: when last ball lost & lives==1 consumed: lives-- → 0 → state 'over'. Ball lost sequence: flash red at bottom? small particle burst at ball loss position.

**Level layout double-check**: cols 12 * rows 8 = 96 bricks; tough rows 3 → 36 tough, 60 normal. Power-ups 8. Good.

**Testing mental run of collisions**: ball speed 430, dt .016 → 6.9 px/frame → steps=ceil(6.9/6)=2 → 3.4px steps. Good. At max 660: 10.6 → 2 steps of 5.3. Good.

Sub-step count: Math.ceil(speed*dt/6). With dt clamp 0.033 and speed 660 → 3.63 → 4 steps fine.

**Paddle width transition**: paddle.w += (targetW - w)*min(1,dt*10). When wide expires targetW=base. Also ensure ball on paddle re-centers fine (offset keeps).

Clamp paddle.x after width change.

**Ball-paddle side hit**: ball may hit paddle side when moving horizontally-ish down; the angle formula handles any hit within overlap; but if ball center beyond paddle edge + r, no collision → passes. That's correct behavior (missed it).

Also ensure ball never gets trapped inside paddle: after reflect set y = paddle.y - r (above). If ball hits while moving up (vy<0) ignore.

**Ball-ball collisions**: not needed.

**Balls array cap**: multiball ensures ≤3 (spawns to reach 3). If caught again with 3 balls, need = 0 → nothing? Requirement satisfied. Maybe give +100 score instead if full? Add: if no spawn, score += 100 "MAX BALLS +100". Eh keep simple: still flash text "MULTIBALL!". I'll spawn up to 5 max: need = min(3 - balls.length...) hmm. Decide: need = 3 - balls.length; if need<=0 → score+=150 float "+150". Fine.

**Score popups**: float text at brick: '+30' color brick color.

**Prevent key scroll**: preventDefault for Space, arrows.

**Start overlay input**: any key/click starts player game. Also on 'over'/'win' any key restarts. But avoid instant restart from the same keydown that... fine.

Edge: When clicking to start from demo, also ensureAudio (user gesture). pointerdown handler: ensureAudio(); if state demo → startPlayer(); else if over/win → startPlayer(); else if playing → launch stuck balls.

Keydown: ensureAudio(); same branching; space in playing launches.

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

Update in states:
- demo: full physics with AI control.
- playing: physics with player control.
- over/win: update particles, floats, shake decay only; overDelay for demo restart.

Demo over: after bricks cleared or lives 0 in demo → set state 'over'/'win' but flag demoMode=true; after 1.4s → resetGame(true). Overlay during that: show small "demo restarting..."? Just show overlay like normal but subtitle "demo". Hmm over overlay says "click to restart" — in demo auto-restart. Show overlay with "DEMO" tag. Fine: draw overlay with title; if demoMode, small text "restarting demo…" blinking, and click also starts player game. Good — clicking during demo-over starts player game.

Track `demoMode` boolean separate from state.

**Render bg**: 
- fill '#0a0d1f' base. 
- radial gradient vignette darker corners.
- faint grid lines every 48px alpha .04.
- playfield border: left/right/top lines with gradient glow stroke 'rgba(90,140,255,.35)' 2px + shadow.
- Also subtle animated star dots? skip.

Actually add slight moving background "aurora"? Keep static for perf simplicity but add two large soft radial gradients (blue/purple) at low alpha for depth. Static.

**Shake**: shake decays: shake=max(0,shake- dt*20)? use exponential: shake*=Math.pow(0.001,dt)? simpler shake=Math.max(0,shake-shake*8*dt+...). Just shake *= Math.exp(-8*dt). Offset = (rand-0.5)*shake.

Apply to world rendering: ctx.translate(ox,oy) before drawing world, restore before HUD/overlay? Shake HUD too is fine/normal. I'll shake world only.

**Ball radius vs brick gap**: gap 5, ball r 7 — ball can't slip between bricks vertically (gap 5 < 14 diameter) horizontally gap 5 too. Good, ball can't pass through gaps — requirement "never pass through bricks" satisfied by geometry + substeps.

**Paddle draw**:
```
roundRect(x-w/2, y, w, h, 7)
gradient horizontal: edges '#22d3ee' center '#e0f2fe'? Let's: outer glow shadowBlur 18 color rgba(56,189,248,.8). Fill gradient top '#f0f9ff' bottom '#38bdf8'. Center notch line.
If wide: tint green: gradient '#dcfce7'→'#4ade80', glow green.
```
Plus small "wings" when wide? nah.

**Capsule draw**: rounded rect 34×20 r=10, fill dark with colored border & icon; slight wobble rotation sin. Glow shadow color.

Catch check: capsule.y + 10 >= paddle.y && capsule.y - 10 <= paddle.y + paddle.h && |x - paddle.x| < paddle.w/2 + 14.

**HUD font sizes**: label 11px uppercase letterspacing (draw manually with spacing? just normal). Score value 24px bold monospace. 

Letterspacing: canvas has ctx.letterSpacing in newer Chrome — usable but guard: try setting; it's fine (Chrome supports). I'll just use spaces in label like "S C O R E"? simpler: draw 'SCORE' normal.

**Numbers**: score display padded? Just number.

**Code size** ~ 700-800 lines. That's fine.

Now, write it.

Also remember: reply with exactly ONE fenced code block, no text around.

Let me write the code carefully.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NEON BREAKOUT</title>
<style>
  html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#05070f;}
  body{display:flex;align-items:center;justify-content:center;
       background:radial-gradient(1200px 800px at 50% 40%, #0d1230 0%, #05070f 70%);}
  #game{display:block;border-radius:14px;
       box-shadow:0 0 60px rgba(56,120,255,.25), 0 0 120px rgba(120,60,255,.12), inset 0 0 0 1px rgba(140,170,255,.18);
       width:min(96vw, 138vh); /* 960/640=1.5 → width = height*1.5; 96vh*1.5=144vh so cap 138vh keeps margin */
       aspect-ratio:3/2;
       touch-action:none;
       cursor:crosshair;
       background:#0a0d1f;}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
'use strict';
(function(){
const W=960,H=640;
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
let scaleF=1;
function resize(){
  const rect=canvas.getBoundingClientRect();
  const dpr=window.devicePixelRatio||1;
  const bw=Math.max(1,Math.round(rect.width*dpr)), bh=Math.max(1,Math.round(rect.height*dpr));
  if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
  scaleF=bw/W;
}
window.addEventListener('resize',resize);
```
CSS width: min(96vw, 138vh). At 1280×800: 96vw=1228.8, 138vh=1104 → 1104 wide, 736 high. Fits. At 375 wide: 96vw=360 → height 240. Good.

Aspect-ratio 3/2 with width set → height auto. Good.

Continue:

```
// ---------- helpers ----------
const rand=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const TAU=Math.PI*2;
function roundRect(c,x,y,w,h,r){ ... path }
```
Use ctx.roundRect native? Chrome supports ctx.roundRect. But safer custom path function for compat. Write helper drawRR(x,y,w,h,r) building path on ctx.

Audio:

```
let AC=null;
function ensureAudio(){
  try{
    if(!AC){const C=window.AudioContext||window.webkitAudioContext; if(C) AC=new C();}
    if(AC&&AC.state==='suspended') AC.resume().catch(()=>{});
  }catch(e){}
}
function tone(freq,dur,type,vol,delay){
  if(!AC||AC.state!=='running')return;
  try{
    const t0=AC.currentTime+(delay||0);
    const o=AC.createOscillator(), g=AC.createGain();
    o.type=type||'square'; o.frequency.setValueAtTime(freq,t0);
    g.gain.setValueAtTime(0.0001,t0);
    g.gain.exponentialRampToValueAtTime(vol||0.05,t0+0.008);
    g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
    o.connect(g); g.connect(AC.destination);
    o.start(t0); o.stop(t0+dur+0.02);
  }catch(e){}
}
const sfx={
  brick:(hp)=>tone(300+hp*160,0.07,'square',0.045),
  crack:()=>tone(240,0.05,'square',0.04),
  paddle:()=>tone(330,0.06,'triangle',0.06),
  wall:()=>tone(190,0.04,'triangle',0.03),
  power:()=>{tone(523,0.09,'square',0.05);tone(659,0.09,'square',0.05,0.08);tone(784,0.12,'square',0.05,0.16);},
  lose:()=>{tone(300,0.15,'sawtooth',0.06);tone(150,0.25,'sawtooth',0.06,0.12);},
  win:()=>{[523,659,784,1046].forEach((f,i)=>tone(f,0.14,'square',0.05,i*0.11));},
  over:()=>{[330,262,196,131].forEach((f,i)=>tone(f,0.2,'sawtooth',0.05,i*0.14));},
  launch:()=>tone(440,0.06,'triangle',0.05)
};
```

Game constants:

```
const HUD_H=54, WALL=10? 
```
Hmm walls: I'll play left wall at x=8 visually with ball bouncing at x=r+8? Simpler: bounce at x=r and W-r; draw border lines at x=6 etc decorative. Keep physics at edges: left 0, right W, top HUD_H.

Bricks:

```
const COLS=12, ROWS=8, B_TOP=96, B_H=22, B_GAP=5, MARGIN=22;
const B_W=(W-MARGIN*2-(COLS-1)*B_GAP)/COLS;  // (960-44-55)/12=861/12=71.75
```

Row defs:

```
const ROWS_DEF=[
 {hp:2,color:'#c084fc',score:30},
 {hp:2,color:'#a78bfa',score:30},
 {hp:2,color:'#818cf8',score:30},
 {hp:1,color:'#38bdf8',score:20},
 {hp:1,color:'#2dd4bf',score:20},
 {hp:1,color:'#4ade80',score:20},
 {hp:1,color:'#facc15',score:20},
 {hp:1,color:'#fb923c',score:20},
];
```

Wait: tough bricks "need 2 hits and visibly crack after the first one" — good.

Paddle:

```
const P_BASE_W=112, P_H=14, P_Y=H-34;
const BALL_R=7;
const SPEED0=430, SPEED_MAX=650;
```

State vars:

```
let state='demo', demoMode=true;
let score=0,lives=3,ballSpeed=SPEED0;
let bricks=[],balls=[],capsules=[],parts=[],floats=[];
let paddle={x:W/2,y:P_Y,w:P_BASE_W,tw:P_BASE_W,h:P_H};
let wideT=0;
let shake=0, time=0, overDelay=0, winBonus=0;
let mouseX=W/2, mouseControl=true;
const keys={};
let aiNoiseX=0, aiNoiseT=0;
let hintT=0; // launch hint pulse
```

buildBricks:

```
function buildBricks(){
  bricks=[];
  for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
    const d=ROWS_DEF[r];
    const b={x:MARGIN+c*(B_W+B_GAP), y:B_TOP+r*(B_H+B_GAP), w:B_W, h:B_H,
             hp:d.hp, maxHp:d.hp, color:d.color, pts:d.score,
             power:null, cracks:null, pulse:Math.random()*TAU};
    bricks.push(b);
  }
  // assign power-ups
  const idx=[...bricks.keys()];
  for(let i=idx.length-1;i>0;i--){const j=(Math.random()*(i+1))|0;const t=idx[i];idx[i]=idx[j];idx[j]=t;}
  const nPower=8;
  for(let i=0;i<nPower;i++){
    const b=bricks[idx[i]];
    b.power = i%2===0?'wide':'multi';
  }
  // pre-generate cracks for tough bricks
  for(const b of bricks){
    if(b.maxHp===2){
      b.cracks=[];
      for(let k=0;k<2;k++){
        const pts=[];let px=rand(b.w*0.15,b.w*0.85), py=2;
        pts.push([px,py]);
        for(let s=0;s<3;s++){px+=rand(-14,14);py+=(b.h-4)/3;pts.push([clamp(px,3,b.w-3),Math.min(py,b.h-1)]);}
        b.cracks.push(pts);
      }
    }
  }
}
```

Hmm cracks as local coords, drawn translated.

spawnBall:

```
function spawnStuckBall(){
  balls.push({x:paddle.x,y:paddle.y-BALL_R-1,vx:0,vy:0,stuck:true,off:0,stick:0,trail:[]});
}
function launchBall(b){
  b.stuck=false;
  const a=rand(-0.35,0.35);
  b.vx=Math.sin(a)*ballSpeed; b.vy=-Math.cos(a)*ballSpeed;
  sfx.launch();
}
```

launchAllStuck(): for balls stuck → launch.

resetGame(demo):

```
function resetGame(demo){
  demoMode=demo; state=demo?'demo':'playing';
  score=0;lives=3;ballSpeed=SPEED0;wideT=0;winBonus=0;
  buildBricks();
  balls=[];capsules=[];parts=[];floats=[];
  paddle.x=W/2;paddle.w=P_BASE_W;paddle.tw=P_BASE_W;
  spawnStuckBall();
  shake=0;overDelay=0;
}
```

Power activation:

```
function applyPower(type){
  sfx.power();
  if(type==='wide'){
    wideT=10; paddle.tw=P_BASE_W*1.6;
    addFloat('WIDE PADDLE!',paddle.x,paddle.y-26,'#4ade80',18);
    burst(paddle.x,paddle.y,'#4ade80',18);
  } else {
    const need=3-balls.length;
    if(need>0){
      const src=balls.find(b=>!b.stuck)||balls[0];
      for(let i=0;i<need;i++){
        if(!src){break;}
        if(src.stuck){
          const a=(-90+ (i-(need-1)/2)*50 )*Math.PI/180;
          const nb={x:paddle.x,y:paddle.y-BALL_R-2,vx:Math.sin? ...}
        }
      }
    }
  }
}
```

Hmm messy. Let me write multiball clean:

```
function multiball(){
  const need=3-balls.length;
  if(need<=0){ addFloat('+150',paddle.x,paddle.y-26,'#f472b6',16); score+=150; return; }
  let src=balls.find(b=>!b.stuck);
  for(let i=0;i<need;i++){
    if(src){
      const sp=Math.hypot(src.vx,src.vy)||ballSpeed;
      const ang=Math.atan2(src.vy,src.vx)+(i===0? -0.45: 0.45);
      const nb={x:src.x,y:src.y,vx:Math.cos(ang)*sp,vy:Math.sin(ang)*sp,stuck:false,off:0,stick:0,trail:[]};
      balls.push(nb);
    } else {
      const a=(-90+(i-(need-1)/2)*55)*Math.PI/180;
      balls.push({x:paddle.x,y:paddle.y-BALL_R-2,vx:Math.cos(a)*ballSpeed,vy:Math.sin(a)*ballSpeed,stuck:false,off:0,stick:0,trail:[]});
    }
  }
  addFloat('MULTIBALL!',paddle.x,paddle.y-30,'#f472b6',18);
  burst(paddle.x,paddle.y,'#f472b6',20);
}
```

atan2 with rotated angle: if src.vy negative (upwards) fine. Rotation ±0.45 rad.

applyPower(type): if wide → wide branch else multiball().

Particles:

```
function burst(x,y,color,n,spd){
  for(let i=0;i<n;i++){
    const a=rand(0,TAU), s=rand(40,spd||240);
    parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-60,life:rand(0.4,0.9),t:0,color,size:rand(2,4.5)});
  }
}
function addFloat(text,x,y,color,size){floats.push({text,x,y,life:0.9,t:0,color,size:size||14});}
```

Floats update: y-=45*dt; life countdown.

Brick hit:

```
function hitBrick(b,ball){
  b.hp--;
  if(b.hp<=0){
    b.alive=false -> we use hp<=0 check via filter or flag. Set b.dead=true.
    score+=b.pts;
    addFloat('+'+b.pts,b.x+b.w/2,b.y+b.h/2,b.color,14);
    burst(b.x+b.w/2,b.y+b.h/2,b.color,16,260);
    shake=Math.min(shake+2.2,7);
    ballSpeed=Math.min(SPEED_MAX,ballSpeed+3.5);
    for(const ob of balls){const sp=Math.hypot(ob.vx,ob.vy); if(sp>0){const k=ballSpeed/sp; ob.vx*=k;ob.vy*=k;}}
    if(b.power){capsules.push({x:b.x+b.w/2,y:b.y+b.h/2,vy:135,type:b.power,ph:rand(0,TAU)});}
    sfx.brick(0);
  } else {
    score+=10;
    addFloat('+10',b.x+b.w/2,b.y+b.h/2,'#e2e8f0',12);
    burst(b.x+b.w/2,b.y+b.h/2,'#ffffff',5,140);
    sfx.crack();
  }
  // check victory
  if(bricks.every(bb=>bb.hp<=0)) winGame();
}
```

Bricks stored with hp; dead when hp<=0. Collision loops skip hp<=0. Count alive maybe for HUD "BRICKS LEFT"? optional; skip.

winGame():

```
function winGame(){
  if(state!=='playing'&&state!=='demo')return;
  winBonus=lives*100; score+=winBonus;
  state='win'; overDelay=0;
  sfx.win();
  burst(W/2,H/2,'#fbbf24',60,320);
}
```

gameOver():

```
function gameOver(){
  state='over'; overDelay=0; sfx.over();
}
```

Ball loss:

```
function loseBall(x){
  sfx.lose();
  burst(x,H-8,'#f87171',14,200);
}
```
In update after moving all balls, filter out balls with y> H+30. If balls.length===0 → lives--; wideT=0; paddle.tw=P_BASE_W; capsules=[]? clear capsules maybe keep. Clear capsules for cleanliness. If lives<=0 → gameOver() else spawnStuckBall().

Update function:

```
function update(dt){
  time+=dt;
  shake*=Math.exp(-7*dt); if(shake<0.05)shake=0;
  // particles/floats always
  for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.t+=dt;if(p.t>=p.life){parts.splice(i,1);continue;}p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=850*dt;p.vx*=Math.exp(-1.2*dt);}
  for(let i=floats.length-1;i>=0;i--){const f=floats[i];f.t+=dt;f.y-=42*dt;if(f.t>=f.life)floats.splice(i,1);}
  
  if(state==='win'||state==='over'){
    overDelay+=dt;
    if(demoMode&&overDelay>1.6)resetGame(true);
    return;
  }
  
  // paddle control
  if(state==='demo') aiControl(dt); else playerControl(dt);
  
  // paddle width tween
  paddle.w+=(paddle.tw-paddle.w)*Math.min(1,dt*12);
  const half=paddle.w/2;
  paddle.x=clamp(paddle.x,half+4,W-half-4);
  
  // wide timer
  if(wideT>0){wideT-=dt;if(wideT<=0){wideT=0;paddle.tw=P_BASE_W;}}
  
  // balls
  const subDt... per ball physics
  updateBalls(dt);
  
  // capsules
  for(let i=capsules.length-1;i>=0;i--){
    const c=capsules[i]; c.y+=c.vy*dt; c.ph+=dt*3;
    if(c.y-10>H){capsules.splice(i,1);continue;}
    if(c.y+10>=paddle.y && c.y-10<=paddle.y+paddle.h && Math.abs(c.x-paddle.x)<=paddle.w/2+16){
      capsules.splice(i,1);
      applyPower(c.type);
    }
  }
}
```

updateBalls:

```
function updateBalls(dt){
  let lostAny=false;
  for(let bi=balls.length-1;bi>=0;bi--){
    const b=balls[bi];
    if(b.stuck){
      b.x=paddle.x+b.off; b.y=paddle.y-BALL_R-1; b.stick+=dt;
      if(state==='demo'&&b.stick>0.55) launchBall(b);
      continue;
    }
    // trail
    b.trail.push({x:b.x,y:b.y}); if(b.trail.length>11)b.trail.shift();
    
    const sp=Math.hypot(b.vx,b.vy);
    const steps=Math.max(1,Math.ceil(sp*dt/6));
    const sdt=dt/steps;
    for(let s=0;s<steps;s++){
      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);sfx.wall();sparks(b.x,b.y);}
      else if(b.x>W-BALL_R){b.x=W-BALL_R;b.vx=-Math.abs(b.vx);sfx.wall();sparks(b.x,b.y);}
      if(b.y<HUD_H+BALL_R){b.y=HUD_H+BALL_R;b.vy=Math.abs(b.vy);sfx.wall();sparks(b.x,b.y);}
      // paddle
      if(b.vy>0 && b.y+BALL_R>=paddle.y && b.y-BALL_R<=paddle.y+paddle.h &&
         Math.abs(b.x-paddle.x)<=paddle.w/2+BALL_R){
        bouncePaddle(b);
      }
      // bricks
      brickCollision(b);
      if(b.y-BALL_R>H+24){lostAny=true;loseBall(b.x);balls.splice(bi,1);break;}
    }
  }
  if(balls.length===0 && (state==='playing'||state==='demo')){
    lives--;
    wideT=0;paddle.tw=P_BASE_W;capsules.length=0;
    if(lives<=0){gameOver();}
    else{spawnStuckBall();}
  }
}
```

Careful: splice inside loop while iterating downward — fine. But 'lostAny' variable unused then; remove. Also after splice break out of steps loop.

Wait: if last ball lost and lives becomes 0 → gameOver. Also multiball: only when ALL balls lost we lose a life — correct.

brickCollision:

```
function brickCollision(b){
  for(const br of bricks){
    if(br.hp<=0)continue;
    if(b.x+BALL_R>br.x && b.x-BALL_R<br.x+br.w && b.y+BALL_R>br.y && b.y-BALL_R<br.y+br.h){
      const cx=br.x+br.w/2, cy=br.y+br.h/2;
      const px=br.w/2+BALL_R-Math.abs(b.x-cx);
      const py=br.h/2+BALL_R-Math.abs(b.y-cy);
      if(px<py){ b.x+= (b.x<cx?-px:px); b.vx=-b.vx; }
      else { b.y+= (b.y<cy?-py:py); b.vy=-b.vy; }
      hitBrick(br,b);
      break;
    }
  }
}
```

bouncePaddle:

```
function bouncePaddle(b){
  const hit=clamp((b.x-paddle.x)/(paddle.w/2),-1,1);
  const maxAng=60*Math.PI/180;
  const ang=hit*maxAng;
  b.vx=Math.sin(ang)*ballSpeed;
  b.vy=-Math.cos(ang)*ballSpeed;
  b.y=paddle.y-BALL_R-0.5;
  sfx.paddle();
  sparks(b.x,paddle.y,'#7dd3fc',4);
  b.trail.length=0;
}
```

Also add slight paddle-velocity influence? skip.

Edge: ball hits paddle while overlapping deep (e.g., from side when ball below paddle top)? Condition requires vy>0 and y+R>=paddle.y and y-R<=paddle.y+h. If ball already passed below paddle top significantly (y-R > paddle.y+h) no hit → lost. If ball enters from side at paddle level, y between → reflect up. OK. But potential double-bounce: after reflect vy<0 so condition vy>0 false. Good.

Corner case: ball moving down hits paddle edge where |dx|<=half+R but ball center outside paddle — bounces anyway (edge forgiveness). Fine.

playerControl:

```
function playerControl(dt){
  const L=keys['ArrowLeft']||keys['KeyA'], R=keys['ArrowRight']||keys['KeyD'];
  if(L)paddle.x-=640*dt;
  if(R)paddle.x+=640*dt;
  if(!L&&!R&&mouseControl){
    paddle.x+=(mouseX-paddle.x)*Math.min(1,dt*22);
  }
}
```

aiControl:

```
function aiControl(dt){
  aiNoiseT-=dt;
  if(aiNoiseT<=0){aiNoiseT=rand(0.3,0.6);aiNoiseX=rand(-26,26);}
  let target=W/2;
  let bestT=Infinity;
  for(const b of balls){
    if(b.stuck){if(b.stick>0.55){/* launched in updateBalls */} continue;}
    // predict
    let px=b.x,py=b.y,pvx=b.vx,pvy=b.vy,t=0;
    const step=1/90;
    for(let i=0;i<600;i++){
      px+=pvx*step;py+=pvy*step;
      if(px<BALL_R){px=BALL_R;pvx=Math.abs(pvx);}
      if(px>W-BALL_R){px=W-BALL_R;pvx=-Math.abs(pvx);}
      if(py<HUD_H+BALL_R){py=HUD_H+BALL_R;pvy=Math.abs(pvy);}
      if(pvy>0&&py>=paddle.y-BALL_R){t+=step;break;}
      t+=step;
    }
    if(t<bestT){bestT=t;target=px;}
  }
  // chase nearby capsules sometimes
  for(const c of capsules){
    if(c.y>H-170 && Math.abs(c.x-target)<110){target=c.x;break;}
  }
  target=clamp(target+aiNoiseX,paddle.w/2+4,W-paddle.w/2-4);
  const maxV=470;
  const dx=target-paddle.x;
  const mv=clamp(dx*8,-maxV,maxV);
  paddle.x+=clamp(mv,-maxV*dt,maxV*dt);
}
```

Hmm clamp(mv,...) then again*dt — write: const step=clamp(dx*10, -maxV*dt, maxV*dt); paddle.x+=step.

Note: stuck-ball auto-launch handled in updateBalls (state==='demo').

Input:

```
window.addEventListener('keydown',e=>{
  if(e.code==='ArrowLeft'||e.code==='ArrowRight'||e.code==='Space')e.preventDefault();
  keys[e.code]=true;
  if(e.code==='ArrowLeft'||e.code==='ArrowRight'||e.code==='KeyA'||e.code==='KeyD')mouseControl=false;
  ensureAudio();
  if(e.code==='Space'&&state==='playing')launchAll();
  else primaryAction();
});
window.addEventListener('keyup',e=>{keys[e.code]=false;});
```

primaryAction(): if state demo → resetGame(false); if over/win → resetGame(false). But careful: pressing space while playing: first branch launches (and shouldn't also run primaryAction — handled by else). But arrow key during playing shouldn't restart — primaryAction only acts on demo/over/win states. Good. Any key in demo starts. Good.

Wait order: if space & playing → launch; else primaryAction(). If space in demo → primaryAction starts game. OK.

pointer:

```
window.addEventListener('pointermove',e=>{
  const rect=canvas.getBoundingClientRect();
  mouseX=(e.clientX-rect.left)/rect.width*W;
  mouseControl=true;
});
window.addEventListener('pointerdown',e=>{
  ensureAudio();
  const rect=canvas.getBoundingClientRect();
  mouseX=(e.clientX-rect.left)/rect.width*W;
  mouseControl=true;
  if(state==='playing')launchAll(); else primaryAction();
});
```

launchAll(): for stuck balls launch (also reset stick).

Rendering. Let's write render():

```
function render(){
  resize(); // keep synced (also handles dpr change)
  ctx.setTransform(scaleF,0,0,scaleF,0,0);
  // bg
  drawBackground();
  ctx.save();
  if(shake>0){ctx.translate(rand(-shake,shake),rand(-shake,shake));}
  drawBricks();
  drawCapsules();
  drawParticles();
  drawPaddle();
  drawBalls();
  ctx.restore();
  drawFloats(); // world-ish but unshaken fine; actually inside shake? put before restore. I'll put floats inside shake region.
  drawHUD();
  drawOverlays();
}
```

Put floats inside the shake save/restore.

drawBackground:

```
function drawBackground(){
  // base
  ctx.fillStyle='#0a0d1f';
  ctx.fillRect(0,0,W,H);
  // soft glows
  let g=ctx.createRadialGradient(W*0.25,H*0.2,0,W*0.25,H*0.2,420);
  g.addColorStop(0,'rgba(56,90,220,0.10)');g.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  g=ctx.createRadialGradient(W*0.8,H*0.75,0,W*0.8,H*0.75,460);
  g.addColorStop(0,'rgba(150,60,220,0.08)');g.addColorStop(1,'rgba(0,0,0,0)');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  // grid
  ctx.strokeStyle='rgba(120,150,255,0.05)';ctx.lineWidth=1;
  ctx.beginPath();
  for(let x=40;x<W;x+=40){ctx.moveTo(x,HUD_H);ctx.lineTo(x,H);}
  for(let y=HUD_H+40;y<H;y+=40){ctx.moveTo(0,y);ctx.lineTo(W,y);}
  ctx.stroke();
  // frame
  ctx.strokeStyle='rgba(110,150,255,0.35)';ctx.lineWidth=2;
  ctx.beginPath();
  ctx.moveTo(1,H);ctx.lineTo(1,HUD_H);ctx.lineTo(W-1,HUD_H);ctx.lineTo(W-1,H);
  ctx.stroke();
  // top glow line under HUD
  ctx.strokeStyle='rgba(110,150,255,0.2)';
  ...
}
```

Grid lines each frame — 24+14 lines fine.

drawBricks:

```
function drawBricks(){
  for(const b of bricks){
    if(b.hp<=0)continue;
    const {x,y,w,h}=b;
    ctx.save();
    // glow for power bricks
    if(b.power){
      ctx.shadowColor='rgba(255,255,255,0.55)';
      ctx.shadowBlur=8+3*Math.sin(time*3+b.pulse);
    }
    ctx.fillStyle=b.color;
    rr(x,y,w,h,5); ctx.fill();
    ctx.shadowBlur=0;
    // top highlight
    ctx.fillStyle='rgba(255,255,255,0.22)';
    rr(x+2,y+2,w-4,(h-8)/2,3);ctx.fill(); // hmm simpler: rect
    // bottom shade
    ctx.fillStyle='rgba(0,0,0,0.22)';
    rr(x+2,y+h/2,w-4,h/2-2,3);ctx.fill();
```
Simplify: draw base; then ctx.fillStyle='rgba(255,255,255,.18)'; fill rect x+2,y+2,w-4,h*0.38; then 'rgba(0,0,0,.2)' rect x+2, y+h*0.55, w-4, h*0.38. Without rounding—slight corner overflow acceptable but corners rounding r5 with inner square might poke out at corners? Inner rects inset by 2 so fine.

```
    // cracked state
    if(b.maxHp===2&&b.hp===1){
      ctx.fillStyle='rgba(10,5,25,0.28)';
      rr(x,y,w,h,5);ctx.fill();
      ctx.strokeStyle='rgba(20,10,40,0.85)';ctx.lineWidth=1.4;
      ctx.beginPath();
      for(const pts of b.cracks){
        ctx.moveTo(x+pts[0][0],y+pts[0][1]);
        for(let i=1;i<pts.length;i++)ctx.lineTo(x+pts[i][0],y+pts[i][1]);
      }
      ctx.stroke();
    }
    // power icon
    if(b.power){
      const cx=x+w/2, cy=y+h/2;
      const a=0.65+0.35*Math.sin(time*4+b.pulse);
      ctx.globalAlpha=a;
      ctx.fillStyle='#ffffff';
      ctx.strokeStyle='#ffffff';
      if(b.power==='wide'){
        ctx.lineWidth=2;
        ctx.beginPath();
        ctx.moveTo(cx-9,cy);ctx.lineTo(cx+9,cy);
        ctx.moveTo(cx-9,cy);ctx.lineTo(cx-4,cy-3.5);ctx.moveTo(cx-9,cy);ctx.lineTo(cx-4,cy+3.5);
        ctx.moveTo(cx+9,cy);ctx.lineTo(cx+4,cy-3.5);ctx.moveTo(cx+9,cy);ctx.lineTo(cx+4,cy+3.5);
        ctx.stroke();
      } else {
        ctx.beginPath();
        ctx.arc(cx-6,cy+3,2.6,0,TAU);
        ctx.arc(cx+6,cy+3,2.6,0,TAU);
        ctx.arc(cx,cy-4,2.6,0,TAU);
        ctx.fill();
      }
      ctx.globalAlpha=1;
    }
    // border
    ctx.strokeStyle='rgba(255,255,255,0.25)';ctx.lineWidth=1;
    rr(x+0.5,y+0.5,w-1,h-1,5);ctx.stroke();
    ctx.restore();
  }
}
```

rr helper builds path only; caller fills/strokes. Define:

```
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();
}
```

drawPaddle:

```
function drawPaddle(){
  const {x,y,w,h}=paddle;
  ctx.save();
  ctx.shadowColor=wideT>0?'rgba(74,222,128,0.8)':'rgba(56,189,248,0.8)';
  ctx.shadowBlur=16;
  const g=ctx.createLinearGradient(x-w/2,0,x+w/2,0);
  if(wideT>0){g.addColorStop(0,'#22c55e');g.addColorStop(0.5,'#bbf7d0');g.addColorStop(1,'#22c55e');}
  else{g.addColorStop(0,'#0ea5e9');g.addColorStop(0.5,'#e0f2fe');g.addColorStop(1,'#0ea5e9');}
  ctx.fillStyle=g;
  rr(x-w/2,y,w,h,7);ctx.fill();
  ctx.shadowBlur=0;
  // center line
  ctx.fillStyle='rgba(10,20,40,0.35)';
  rr(x-2,y+3,4,h-6,2);ctx.fill();
  ctx.restore();
  // wide timer bar above paddle
  if(wideT>0){
    const p=wideT/10;
    ctx.fillStyle='rgba(74,222,128,0.9)';
    ctx.fillRect(x-w/2,y-8,w*p,3);
  }
}
```

Hmm timer bar moving with paddle width — draws shrinking bar. Nice.

drawBalls:

```
function drawBalls(){
  for(const b of balls){
    // trail
    for(let i=0;i<b.trail.length;i++){
      const t=b.trail[i], k=i/b.trail.length;
      ctx.globalAlpha=k*0.28;
      ctx.fillStyle='#7dd3fc';
      ctx.beginPath();ctx.arc(t.x,t.y,BALL_R*(0.35+0.6*k),0,TAU);ctx.fill();
    }
    ctx.globalAlpha=1;
    ctx.save();
    ctx.shadowColor='rgba(125,211,252,0.9)';
    ctx.shadowBlur=14;
    const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,BALL_R);
    g.addColorStop(0,'#ffffff');g.addColorStop(1,'#7dd3fc');
    ctx.fillStyle=g;
    ctx.beginPath();ctx.arc(b.x,b.y,BALL_R,0,TAU);ctx.fill();
    ctx.restore();
  }
}
```

Trail for stuck ball: trail pushes each frame while stuck — pushes same pos; trail length grows with identical points, harmless (drawn under ball). But trail only pushed when not stuck (in updateBalls stuck branch `continue`s before trail push). Good.

drawParticles:

```
ctx.save();ctx.globalCompositeOperation='lighter';
for(const p of parts){
  const k=1-p.t/p.life;
  ctx.globalAlpha=k;
  ctx.fillStyle=p.color;
  ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
}
ctx.restore();ctx.globalAlpha=1;
```

drawCapsules:

```
for(const c of capsules){
  ctx.save();
  ctx.translate(c.x,c.y);
  ctx.rotate(Math.sin(c.ph)*0.25);
  const col=c.type==='wide'?'#4ade80':'#f472b6';
  ctx.shadowColor=col;ctx.shadowBlur=12;
  ctx.fillStyle='rgba(10,14,30,0.9)';
  rr(-17,-11,34,22,11);ctx.fill();
  ctx.shadowBlur=0;
  ctx.strokeStyle=col;ctx.lineWidth=2;
  rr(-17,-11,34,22,11);ctx.stroke();
  ctx.fillStyle=col;ctx.strokeStyle=col;
  if(c.type==='wide'){
    ctx.lineWidth=2;
    ctx.beginPath();ctx.moveTo(-8,0);ctx.lineTo(8,0);
    ctx.moveTo(-8,0);ctx.lineTo(-4,-3.5);ctx.moveTo(-8,0);ctx.lineTo(-4,3.5);
    ctx.moveTo(8,0);ctx.lineTo(4,-3.5);ctx.moveTo(8,0);ctx.lineTo(4,3.5);
    ctx.stroke();
  } else {
    ctx.beginPath();ctx.arc(-6,3,2.6,0,TAU);ctx.arc(6,3,2.6,0,TAU);ctx.arc(0,-4,2.6,0,TAU);ctx.fill();
  }
  ctx.restore();
}
```

drawFloats:

```
for(const f of floats){
  const k=1-f.t/f.life;
  ctx.globalAlpha=k;
  ctx.fillStyle=f.color;
  ctx.font='700 '+f.size+'px ui-monospace, Menlo, Consolas, monospace';
  ctx.textAlign='center';
  ctx.fillText(f.text,f.x,f.y);
}
ctx.globalAlpha=1;
```

drawHUD:

```
function drawHUD(){
  ctx.fillStyle='rgba(8,10,24,0.92)';
  ctx.fillRect(0,0,W,HUD_H);
  ctx.fillStyle='rgba(110,150,255,0.4)';ctx.fillRect(0,HUD_H-2,W,2);
  // score
  ctx.textAlign='left';ctx.textBaseline='middle';
  ctx.fillStyle='rgba(150,170,220,0.8)';
  ctx.font='700 11px ui-monospace, Menlo, Consolas, monospace';
  ctx.fillText('SCORE',22,18);
  ctx.fillStyle='#e8f0ff';
  ctx.font='700 24px ui-monospace, Menlo, Consolas, monospace';
  ctx.fillText(String(score),22,38);
  // lives
  ctx.textAlign='right';
  ctx.fillStyle='rgba(150,170,220,0.8)';
  ctx.font='700 11px ui-monospace, Menlo, Consolas, monospace';
  ctx.fillText('LIVES',W-22,18);
  for(let i=0;i<3;i++){
    const lx=W-22-26*(i+1)+... 
```
Lives icons: draw 3 mini paddles right-aligned: positions x = W-22 - (i+1)*34 + 34... Let me compute: rightmost at W-30; each 26 wide, gap 8: i-th (0..2) x0 = W-30-26 - i*(34). Draw at y=30: filled if i<lives.

```
  for(let i=0;i<3;i++){
    const x0=W-30-26-i*34;
    if(i<lives){
      ctx.fillStyle='#38bdf8';
      rr(x0,28,26,7,3.5);ctx.fill();
    } else {
      ctx.strokeStyle='rgba(120,140,180,0.4)';ctx.lineWidth=1.5;
      rr(x0,28,26,7,3.5);ctx.stroke();
    }
  }
```
Hmm i<lives means filled count = lives, positioned from left of the trio. Fine.

  // wide indicator (center-right)
```
  if(wideT>0){
    ctx.textAlign='center';
    ctx.fillStyle='#4ade80';
    ctx.font='700 12px ui-monospace, Menlo, Consolas, monospace';
    ctx.fillText('WIDE '+wideT.toFixed(1)+'s',W/2+140,20);
    ctx.fillStyle='rgba(74,222,128,0.35)';
    ctx.fillRect(W/2+90,28,100,4);
    ctx.fillStyle='#4ade80';
    ctx.fillRect(W/2+90,28,100*(wideT/10),4);
  }
```
Position: maybe center of HUD: W/2. Put centered: label at W/2. Let me center it at W/2, no offset. Actually lives right, score left, center free. Put at center:

```
    const bx=W/2-50;
    ctx.fillText('WIDE PADDLE',W/2,20);
    ctx.fillStyle='rgba(74,222,128,0.3)';ctx.fillRect(bx,28,100,4);
    ctx.fillStyle='#4ade80';ctx.fillRect(bx,28,100*(wideT/10),4);
```

  // demo tag
```
  if(demoMode){
    ctx.textAlign='center';
    ctx.fillStyle='rgba(150,170,220,0.6)';
    ctx.font='700 11px ...';
    ctx.fillText('DEMO · AI PLAYING',W/2,HUD_H-... 
```
Hmm center conflicts with WIDE. In demo, wideT may be active... Move demo tag next to score? Put demo tag under score? HUD height 54; score value at 38 baseline middle... Let me restructure HUD: 
- Left: SCORE label y16, value y36.
- Right: LIVES label + icons.
- Center: if wideT: WIDE bar; else if demoMode: 'DEMO — AI PLAYING' text centered.
If both in demo: show wide bar, skip demo text momentarily. Fine.

Also show hint in playing when stuck ball exists: draw in playfield (not HUD).

drawOverlays():

```
function drawOverlays(){
  ctx.textBaseline='middle';
  if(state==='demo'){
    ctx.fillStyle='rgba(5,7,18,0.55)';ctx.fillRect(0,0,W,H);
    panel(...)? draw centered:
    title 'BREAKOUT' gradient 72px bold, y 210
    subtitle 'NEON BRICK BREAKER' letterspaced 16px rgba, y 258
    instructions y 320: 'MOUSE or ← → MOVE · SPACE / CLICK LAUNCH'
    blinking 'CLICK OR PRESS ANY KEY TO START' y 400 size 20 pulsing
    small 'the AI is warming up behind the scenes…' maybe y 460 'Watch the demo, then take the paddle!'
  } else if(state==='over'){
    ctx.fillStyle='rgba(5,7,18,0.66)';ctx.fillRect(0,0,W,H);
    'GAME OVER' 64px '#f87171' glow, y 250
    'SCORE '+score y 320 28px
    blink 'CLICK OR PRESS ANY KEY TO RESTART' y 390
  } else if(state==='win'){
    overlay
    'YOU WIN!' gradient gold y 240
    'SCORE '+score (+ '+'+winBonus+' LIFE BONUS') y 310/340
    blink restart y 400
  }
  if(state==='playing' && balls.some(b=>b.stuck) ... hint
}
```

Hint: in playing with stuck ball: draw pulsing text at y 360: 'CLICK OR PRESS SPACE TO LAUNCH'. Also arrows hint 'MOUSE / ← → TO MOVE' smaller y 388? Show both only for the first launch of the game? Simple: always show when stuck — fine, it's hidden after launch.

But in demo mode, don't show the launch hint (AI launches). The overlay covers anyway; skip hint in demo.

Blink function: alpha=0.6+0.4*Math.sin(time*5).

Title gradient: 

```
function gradTitle(text,y,size){
  ctx.font='800 '+size+'px system-ui, "Segoe UI", Arial, sans-serif';
  ctx.textAlign='center';
  const g=ctx.createLinearGradient(0,y-size/2,0,y+size/2);
  g.addColorStop(0,'#9be7ff');g.addColorStop(0.5,'#6ea8ff');g.addColorStop(1,'#b07aff');
  ctx.fillStyle=g;
  ctx.shadowColor='rgba(110,150,255,0.6)';ctx.shadowBlur=24;
  ctx.fillText(text,W/2,y);
  ctx.shadowBlur=0;
}
```

Also add letter-spacing feel by drawing text with spaces? Use ctx.letterSpacing='6px' guarded:

```
try{ctx.letterSpacing='6px';}catch(e){}
```
Actually assigning unsupported property is harmless (no throw) in JS — setting unknown property on ctx object just sets it. Chrome supports letterSpacing. Safe either way. But remember to reset to '0px'.

Main loop + init:

```
resize();
resetGame(true);
let last=performance.now();
function frame(now){
  let dt=(now-last)/1000;last=now;
  if(dt>0.05)dt=0.05;
  update(dt);
  render();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
```

resize on every render start — getBoundingClientRect each frame is okay-ish; but calling resize every frame does canvas.width check (no reset unless changed) fine.

Wait: resize() sets canvas.width when rect changes → clearing canvas; we then redraw everything — fine. But note: setting canvas.width even to same value clears; we guard with if!==. Good.

One catch: `ctx.setTransform(scaleF,...)` each render — good with dpr.

Now let me double check tricky logic points:

1. **Demo AI launching stuck ball**: In updateBalls, for stuck ball: `if(state==='demo'&&b.stick>0.55) launchBall(b);` — good. For playing, launch requires click/space.

2. **First render before font load** — system fonts fine.

3. **Victory check inside hitBrick** uses bricks.every — 96 elements each break, fine.

4. **hitBrick called from brickCollision inside sub-step loop; after brick dies, loop breaks.** Also after hitBrick, if state changed to 'win', updateBalls continues stepping remaining balls... After win, state='win'; update() early-returns next frame. Current frame continues; harmless.

But: in win state, balls frozen (update returns before ball update) — balls drawn static. Fine.

5. **gameOver when all balls lost**: lives-- then if <=0 gameOver. When lives>0 spawnStuckBall. Note: capsules cleared, wide reset. Good.

6. **Demo restart after over**: overDelay>1.6 → resetGame(true). Good. If player clicks during that window → primaryAction → resetGame(false). Good.

7. **Space in 'over' state**: primaryAction → resetGame(false) → playing. Good.

8. **Ball speed rescale after brick break also affects stuck balls?** stuck balls have vx=vy=0; hypot=0, guard `if(sp>0)`. Good. Their launch uses current ballSpeed. Good.

9. **Trail when ball respawns** — new ball has trail=[]. Good.

10. **mouseX initial = W/2**; mouseControl=true initially but in demo AI overrides paddle anyway. On start (resetGame(false)) paddle.x=W/2; if user's mouse elsewhere, paddle lerps toward mouse — acceptable. Actually may feel like paddle jumps; lerp dt*22 quick. Fine.

Hmm: one subtlety — when starting player game via click, mouseX updates to click position (pointerdown sets mouseX) — paddle glides to mouse. OK.

11. **Paddle clamp with wall margin 4**: bricks margin 22; paddle can reach x=half+4 → paddle edge at 4. Ball bounces at x=BALL_R=7. Ball can slip along wall past paddle edge? Paddle edge at 4 < 7... paddle edge x=4, ball center min at bounce x=7 with radius 7 → ball spans 0..14. Paddle spans 4..4+w. Overlap when ball at wall: |ball.x - paddle.x| <= half+7 → paddle.x=half+4 → |7-(half+4)|=half-3 <= half+7 ✓. So corner reach covered. Good — no gap at walls.

12. **Ball hitting brick from below while moving up** — resolved via penetration axis. Good.

13. **Ball tunneling between paddle and bottom** — sub-steps handle.

14. **Multiball spawn inside brick**: clone at src position which is ball-collision-free position. Fine.

15. **Performance**: shadows used per brick (only power bricks have shadowBlur) — ≤8. Balls ≤ 3-ish. Fine.

16. **Canvas letterSpacing**: I'll use it for small labels; reset after. To be safe define helper `ls(v){ if('letterSpacing' in ctx) ctx.letterSpacing=v; }`. Chrome supports; guard harmless.

17. **e.preventDefault() on space** — also prevents page scroll in iframe. Keydown listener on window.

18. `pointermove` on window: rect from canvas each event — fine.

19. iOS/touch: pointermove works with touch; touch-action none set on canvas CSS. Also prevent default on pointerdown? Not needed.

20. Ensure no console errors: guard AudioContext creation, etc.

21. **HUD baseline**: set ctx.textBaseline each usage to avoid leaks. I'll set explicitly in HUD and overlays.

Now, one more requirement check: "The bounce angle off the paddle depends on where the ball hits it (center = straight up, edges = sharp angle)" ✓ (maxAng 60°).

"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" ✓ sub-stepping + resolution.

"Particles when a brick breaks" ✓.

"3 lives, score displayed" ✓.

"Game over and victory screens with a restart option" ✓.

"6 rows of bricks, mixing normal and tougher (2 hits, cracks)" ✓ 8 rows, 3 tough rows.

"Demo mode behind 'Click or press a key to start'" ✓.

Ball stuck position offset: `b.off` set where? When ball respawns stuck, off=0 → centered. If we wanted to keep offset from previous life — nah, 0.

For multiball with src stuck (rare: catch capsule while ball stuck? capsules only fall after a brick broke, and stuck ball only at life start... possible if capsule still falling when ball lost — capsules cleared on life loss, so src stuck only if capsule caught in same frame ball lost... edge: capsule caught while the only ball is stuck? Brick breaks require a ball hit; after break ball continues moving; stuck only after life lost which clears capsules. So src stuck can't coexist with a fresh capsule... Actually capsules cleared when lives lost. When multiball applied there's ≥1 moving ball typically. The `else` fallback covers impossible case; keep for safety.)

One more: **ball trapped bouncing horizontally** — maxAng 60° prevents pure horizontal from paddle. From brick reflections vy could become small if hitting brick side repeatedly? Vertical reflection preserves |vy|; horizontal reflection preserves |vx|. vy only changes sign, never magnitude — so a ball launched at 60° keeps vy=0.5*speed forever; fine, always progresses vertically. Good—no degenerate horizontal loops.

But wait: bricks reflect based on penetration axis; vy sign flips on vertical axis resolution. Magnitudes conserved except paddle re-aim and speed rescale. Safe.

**Ball bouncing on paddle corners with hit clamp ±1 → 60°, vx=0.866sp** — near-wall corner could push ball into wall region? Ball at x near wall, vx large toward wall → wall bounce handles.

**Paddle moving into ball** (ball falling onto moving paddle): condition met, reflect. Ball could be beside paddle moving down; paddle slides under ball; reflect sets ball above paddle. Good.

**Ball squeezed between paddle and bottom?** Paddle at fixed y; ball below paddle top but overlapping horizontally gets pushed up. Ball below paddle entirely → lost. OK.

Also add small anti-stuck: if ball gets vy exactly 0? Can't happen (magnitudes conserved; initial vy ≠0; paddle bounce cos(60°)≥0.5).

Edge: ball spawn stuck at paddle — drawn above paddle; collide check requires vy>0 — stuck ball vy=0 no collision. Launched upward immediately. Good.

**HUD 'SCORE' letterSpacing**: set ctx.letterSpacing='2px' maybe; reset '0px'.

Now overlays details:

Demo overlay:
```
ctx.fillStyle='rgba(4,6,16,0.62)';fillRect
// title
gradTitle('BREAKOUT',230,78)
// subtitle
ctx.fillStyle='rgba(160,190,255,0.75)'; font 700 15px mono ls 4px; 'NEON BRICK BREAKER' at 272
// controls
font 600 15px system-ui; fillStyle 'rgba(190,205,240,0.8)'; 'MOUSE / ←→ — MOVE      SPACE / CLICK — LAUNCH' at 330? Use two lines:
line1: 'MOVE — mouse or ← → keys' y 322
line2: 'LAUNCH — click or space' y 348
// blinking CTA
alpha pulse; font 800 22px system-ui; fillStyle '#fff' with shadow cyan; 'CLICK OR PRESS ANY KEY TO START' y 415
// demo note
'AI demo in progress — score, lives & power-ups are live' y 470 rgba(140,160,200,0.6) 13px
```

Game over:
```
'GAME OVER' fill '#ff6b6b' shadow red blur 30, font 800 64, y 240
'SCORE  1234' font 700 30px mono '#e8f0ff' y 305
'BRICKS CLEARED x / 96'? need count: compute alive count. Add: const cleared = bricks.filter(b=>b.hp<=0).length; show 'BRICKS '+cleared+'/96' small y 340. Also in win screen.
blink 'CLICK OR PRESS ANY KEY TO RESTART' y 400
```

Win:
```
'YOU WIN!' gradient gold: create linear gradient ['#fde68a','#f59e0b','#fbbf24'], shadow amber. y 235, 70px.
'SCORE '+score y 300
'+'+winBonus+' LIFE BONUS' y 335 color '#4ade80' 16px (if >0)
blink restart y 400
```

Stuck hint (playing):
```
pulse alpha; font 700 16px mono; 'CLICK OR PRESS SPACE TO LAUNCH' at (W/2, 380); smaller 'MOUSE / ← → TO MOVE' at 404 alpha lower.
```
Position 380 is between bricks (307) and paddle (604) — visible. Good.

Let me also double-check the demo AI paddle speed vs ball: ball vy ≥ 0.5*430=215 initially; horizontal up to 372. AI maxV 470 > 372 fine at base speed; at max speed 650: vy min 325, vx max 563 > 470 → AI can miss at extreme angles at high speed. Occasional misses fine (adds drama, demo restarts).

Wait, but if AI misses repeatedly demo never shows victory. It's fine either way.

Also AI noise ±26 could cause misses when ball far — paddle reaches in time usually.

Let me also add ball speed slight increase over time in demo? no.

**Check victory possible in demo**: AI decent → yes plausible within reasonable time. Not required anyway.

Now finalize numbers:
- HUD_H=54.
- B_TOP=96. Rows end y=96+8*27-5=307.
- Paddle y=H-34=606, h=14 → top 606, bottom 620. H=640 → 20px below paddle. Ball lost when y-R>664.

AI predict target uses paddle.y — good.

Wide paddle: 112*1.6=179.2 — spans 179/960 ≈ 19%. OK.

Capsule catch tolerance: paddle half + 16.

Now write drawHUD demo tag placement: center. If wideT>0 show wide widget center; elif demoMode show 'DEMO · AI PLAYING' centered small at y 30.

Also maybe show a small level label 'LEVEL 1' under score? Skip.

Score count-up animation? Simple immediate. Fine.

Also brick break adds shake — ensure shake applied only to world (already).

One more polish: paddle hit flash — paddle brightness bump via `paddleFlash` timer: when bouncePaddle, flash=0.12; draw extra white overlay alpha flash*? Minor; add: paddle.flash=0.15 on bounce; in drawPaddle, if flash>0 draw white rounded rect alpha flash*3 (cap). decay in update. Small but nice. Eh, keep — cheap.

Also ball spawn: show it resting on paddle.

Write the final code now, carefully, in one pass, watching for syntax errors.

Also: `keys` uses e.code; ArrowLeft etc. Some browsers: e.code for arrows = 'ArrowLeft' ✓.

preventDefault also for KeyA/KeyD? Not needed.

One concern: `window.addEventListener('keydown')` — iframe receives keys only after click; we show the CTA; good.

Also when state==='playing' and user presses any key (e.g., 'x') → primaryAction does nothing (state playing) ✓.

primaryAction:
```
function primaryAction(){
  if(state==='demo'){resetGame(false);}
  else if(state==='over'||state==='win'){resetGame(false);}
}
```

resetGame(false) sets demoMode=false state 'playing'.

Now — potential bug: In updateBalls loop iterating balls downward with splice; inside we `break` after splice to exit substep loop. Then continue outer loop (bi--). OK.

But careful: after balls.splice(bi,1), the `lostAny` handling later uses balls.length===0. Remove lostAny var entirely; the `if(balls.length===0...)` after loop suffices.

Also when game over (state='over'), update returns early next frames; balls array empty; fine.

Also on win, balls remain (frozen). Render draws them. Fine.

Let me also handle: capsule caught during 'demo' → applyPower works same.

Also floats inside shake — I'll include draws of floats within the shake save block.

Alright — HUD textBaseline 'middle' set; overlay uses 'middle' too; after overlays set nothing else. Each draw sets its own align/baseline where needed. Particles/bricks don't use text.

Font strings: use `ctx.font='800 78px system-ui, -apple-system, "Segoe UI", Arial, sans-serif'` — quotes inside string: fine with single-quoted JS string containing double quotes.

letterSpacing helper:

```
function setLS(v){ try{ ctx.letterSpacing=v; }catch(err){} }
```
Assigning a string to ctx.letterSpacing in Chrome works; in others it's ignored property assignment (no throw). Actually assignment to non-existent property on the context object never throws. So try/catch unnecessary but harmless. Keep simple: set ctx.letterSpacing where wanted and reset. If unsupported, no effect — no error. But wait: in strict mode assigning to object property is fine. OK.

Hmm, `ctx.letterSpacing='3px'` — Chrome ≥ 99 supports. Safe.

Edge: letterSpacing affects fillText measurements for centered text — fine.

Now, verify the crack generation: pts from py=2 down to b.h-1: increments (22-4)/3=6 → 2,8,14,20 ✓ within.

Also cracks drawn only when hp===1 && maxHp===2 ✓ "visibly crack after first hit".

Icon on 2-hp brick with power: after crack it still shows icon ✓.

Now, multiball rotation: atan2(vy,vx) for upward ball ~ -90°; ±0.45 → -45°/-135°... wait -90-0.45=-0.45rad? atan2 gives ≈ -1.571; +0.45 → -1.121 rad = -64° (up-right), -0.45 → -2.021 = -116° (up-left). Good.

But first clone i=0 gets -0.45 (left-up), i=1 gets +0.45. Both relative to src direction ✓.

Check applyPower wide when already wide: wideT=10 reset, tw set ✓.

Ball trail pushed only in non-stuck branch each frame before substeps ✓.

Let me now also think about `resize()` being called inside render before setTransform — yes.

Also initial `resize()` before first frame ✓ (render does it anyway).

devicePixelRatio changes → rect.width CSS same but dpr change → bw changes → resize triggers ✓ (checked each frame).

One perf nit: getBoundingClientRect every frame — fine.

Now write the HTML fully.

Also title of demo overlay maybe animate bricks behind — it's live game behind translucent overlay ✓ visible within 3s screenshot ✓ (screenshot at ~3s shows demo playing with overlay + bricks + ball).

Let me make overlay slightly lighter (0.45) so demo gameplay clearly visible.

Also, small: draw "P1" nothing.

Write final code:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Neon Breakout</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#05070f}
body{display:flex;align-items:center;justify-content:center;background:radial-gradient(1100px 750px at 50% 38%,#0c1130 0%,#05070f 72%)}
#game{width:min(96vw,136vh);aspect-ratio:3/2;display:block;border-radius:14px;background:#0a0d1f;touch-action:none;cursor:crosshair;
box-shadow:0 0 50px rgba(60,110,255,.22),0 0 110px rgba(130,70,255,.10),0 0 0 1px rgba(130,160,255,.16)}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
'use strict';
(function(){
const W=960,H=640,HUD_H=54;
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
let scaleF=1;
function resize(){
  const r=canvas.getBoundingClientRect();
  const dpr=window.devicePixelRatio||1;
  const bw=Math.max(1,Math.round(r.width*dpr)),bh=Math.max(1,Math.round(r.height*dpr));
  if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
  scaleF=bw/W;
}
...
```

Wait width min(96vw,136vh): 136vh at 800 = 1088 → w 1088 h 725 fits in 800 ✓. At 1280×800 ok. At 375×667 phone: 96vw=360 h=240 ✓.

Full script below. Let me write it end-to-end mentally checking syntax.

I need to be careful about function order (function declarations hoisted — fine).

Also `bricks.every(bb=>bb.hp<=0)` inside hitBrick each break — 96 iterations OK.

Let me count bricks cleared for over screen: computed at render time via filter — fine.

Another detail: during 'over'/'win', particles continue; update returns early after handling particles/floats — yes I structured update to always update particles & floats first, then early-return for over/win (plus demo auto-restart). Also decay shake before return? shake decay happens before early return ✓ (I put shake decay with time at top).

But overDelay increments only in over/win branch ✓.

Now, in 'over'/'win' we skip paddle tween & capsule updates — capsules frozen mid-air. Slightly odd but overlay covers; acceptable. Actually clear capsules on gameOver/win: capsules.length=0 in gameOver() and winGame(). Cleaner.

Also on win, maybe let balls keep... frozen fine.

Write update:

```
function update(dt){
  time+=dt;
  shake*=Math.exp(-7*dt); if(shake<0.03)shake=0;
  // particles
  for(let i=parts.length-1;i>=0;i--){const p=parts[i];p.t+=dt;
    if(p.t>=p.life){parts.splice(i,1);continue;}
    p.vy+=880*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=Math.exp(-0.9*dt);}
  for(let i=floats.length-1;i>=0;i--){const f=floats[i];f.t+=dt;f.y-=44*dt;if(f.t>=f.life)floats.splice(i,1);}
  if(paddle.flash>0)paddle.flash-=dt;

  if(state==='over'||state==='win'){
    overDelay+=dt;
    if(demoMode&&overDelay>1.8)resetGame(true);
    return;
  }

  if(state==='demo')aiControl(dt);else playerControl(dt);
  paddle.w+=(paddle.tw-paddle.w)*Math.min(1,dt*12);
  paddle.x=clamp(paddle.x,paddle.w/2+4,W-paddle.w/2-4);
  if(wideT>0){wideT-=dt;if(wideT<=0){wideT=0;paddle.tw=P_BASE_W;}}

  updateBalls(dt);

  for(let i=capsules.length-1;i>=0;i--){
    const c=capsules[i];c.y+=c.vy*dt;c.ph+=dt*3.2;
    if(c.y-12>H){capsules.splice(i,1);continue;}
    if(c.y+11>=paddle.y&&c.y-11<=paddle.y+paddle.h&&Math.abs(c.x-paddle.x)<=paddle.w/2+16){
      capsules.splice(i,1);applyPower(c.type);
    }
  }
}
```

paddle.flash init 0.

updateBalls final:

```
function updateBalls(dt){
  for(let bi=balls.length-1;bi>=0;bi--){
    const b=balls[bi];
    if(b.stuck){
      b.x=paddle.x+b.off;b.y=paddle.y-BALL_R-1;b.stick+=dt;
      if(state==='demo'&&b.stick>0.55)launchBall(b);
      continue;
    }
    b.trail.push({x:b.x,y:b.y});
    if(b.trail.length>11)b.trail.shift();
    const sp=Math.hypot(b.vx,b.vy);
    const steps=Math.max(1,Math.ceil(sp*dt/6));
    const sdt=dt/steps;
    let dead=false;
    for(let s=0;s<steps&&!dead;s++){
      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);sfx.wall();sparks(b.x,b.y,'#7dd3fc',3);}
      else if(b.x>W-BALL_R){b.x=W-BALL_R;b.vx=-Math.abs(b.vx);sfx.wall();sparks(b.x,b.y,'#7dd3fc',3);}
      if(b.y<HUD_H+BALL_R){b.y=HUD_H+BALL_R;b.vy=Math.abs(b.vy);sfx.wall();sparks(b.x,b.y,'#7dd3fc',3);}
      if(b.vy>0&&b.y+BALL_R>=paddle.y&&b.y-BALL_R<=paddle.y+paddle.h&&Math.abs(b.x-paddle.x)<=paddle.w/2+BALL_R){
        bouncePaddle(b);
      }
      brickCollision(b);
      if(b.y-BALL_R>H+20){dead=true;}
    }
    if(dead){loseBall(b.x);balls.splice(bi,1);}
  }
  if(balls.length===0){
    lives--;
    wideT=0;paddle.tw=P_BASE_W;capsules.length=0;
    if(lives<=0){gameOver();}
    else{spawnStuckBall();addFloat(lives+' LIVES LEFT',paddle.x,paddle.y-40,'#f87171',16);}
  }
}
```

Hmm `if(balls.length===0)` also triggers in 'playing'/'demo' only — state can't be other here since early return. But careful: gameOver() sets state='over'; but we're mid-update; subsequent capsule loop still runs this frame — capsules cleared anyway. Fine.

Wait: sfx.wall() every wall hit might be spammy but okay.

sparks helper:

```
function sparks(x,y,color,n){
  for(let i=0;i<n;i++){
    const a=rand(0,TAU),s=rand(30,140);
    parts.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:rand(0.15,0.35),t:0,color,size:rand(1.5,3)});
  }
}
```

bouncePaddle sets paddle.flash=0.18.

drawPaddle flash: after gradient fill, if(paddle.flash>0){ctx.globalAlpha=Math.min(0.8,paddle.flash*4);ctx.fillStyle='#fff';rr(...);ctx.fill();ctx.globalAlpha=1;}

loseBall(x): sfx.lose(); burst(x,H-10,'#f87171',16,220); shake=Math.min(shake+3,8);

gameOver(): state='over';overDelay=0;capsules.length=0;sfx.over();

winGame(): guard state; winBonus=lives*100; score+=winBonus; state='win'; overDelay=0; capsules.length=0; sfx.win(); burst(W/2,H*0.45,'#fbbf24',50,340); shake=4;

Now brickCollision per sub-step loops over ALL 96 bricks each step — steps ≤ ~4, balls ≤3 → ≤ 1200 AABB checks/frame worst case. Trivial.

Optimization possible but unnecessary.

render() full:

```
function render(){
  resize();
  ctx.setTransform(scaleF,0,0,scaleF,0,0);
  ctx.textBaseline='alphabetic';
  drawBackground();
  ctx.save();
  if(shake>0)ctx.translate(rand(-shake,shake),rand(-shake,shake));
  drawBricks();
  drawCapsules();
  drawParticles();
  drawPaddle();
  drawBalls();
  drawFloats();
  ctx.restore();
  drawHUD();
  if(state==='playing')drawPlayHints();
  drawOverlay();
}
```

drawPlayHints(): stuck hint; only if balls.some(stuck):

```
function drawPlayHints(){
  if(!balls.some(b=>b.stuck))return;
  const a=0.55+0.45*Math.sin(time*5);
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.globalAlpha=a;
  ctx.fillStyle='#dbeafe';
  ctx.font='700 17px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText('CLICK OR PRESS SPACE TO LAUNCH',W/2,372);
  ctx.globalAlpha=a*0.6;
  ctx.font='600 13px system-ui,"Segoe UI",Arial,sans-serif';
  ctx.fillText('MOVE — mouse or ← → keys',W/2,396);
  ctx.globalAlpha=1;
}
```

drawOverlay():

```
function drawOverlay(){
  ctx.textAlign='center';ctx.textBaseline='middle';
  if(state==='demo'){
    ctx.fillStyle='rgba(4,6,16,0.5)';ctx.fillRect(0,0,W,H);
    titleGradient('BREAKOUT',236,80);
    ls('6px');ctx.fillStyle='rgba(158,183,255,0.8)';ctx.font='700 15px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('NEON BRICK BREAKER',W/2,296);ls('0px');
    ctx.fillStyle='rgba(196,208,240,0.85)';ctx.font='600 15px system-ui,"Segoe UI",Arial,sans-serif';
    ctx.fillText('MOVE — mouse or ← →  ·  LAUNCH — click or space',W/2,346);
    const a=0.6+0.4*Math.sin(time*4.5);
    ctx.globalAlpha=a;
    ctx.fillStyle='#ffffff';
    ctx.shadowColor='rgba(125,211,252,0.9)';ctx.shadowBlur=18;
    ctx.font='800 21px system-ui,"Segoe UI",Arial,sans-serif';
    ctx.fillText('CLICK OR PRESS ANY KEY TO START',W/2,412);
    ctx.shadowBlur=0;ctx.globalAlpha=1;
    ctx.fillStyle='rgba(140,160,205,0.65)';ctx.font='500 13px system-ui,"Segoe UI",Arial,sans-serif';
    ctx.fillText('Demo mode — the AI is playing. Take over whenever you like!',W/2,466);
  } else if(state==='over'){
    ctx.fillStyle='rgba(4,6,16,0.68)';ctx.fillRect(0,0,W,H);
    ctx.fillStyle='#f87171';ctx.shadowColor='rgba(248,113,113,0.7)';ctx.shadowBlur=28;
    ctx.font='800 64px system-ui,"Segoe UI",Arial,sans-serif';
    ctx.fillText('GAME OVER',W/2,238);ctx.shadowBlur=0;
    ctx.fillStyle='#e8f0ff';ctx.font='700 30px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('SCORE  '+score,W/2,312);
    const cleared=bricks.filter(b=>b.hp<=0).length;
    ctx.fillStyle='rgba(160,178,220,0.8)';ctx.font='600 16px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('BRICKS  '+cleared+' / '+(COLS*ROWS),W/2,348);
    blinkText('CLICK OR PRESS ANY KEY TO RESTART',414);
  } else if(state==='win'){
    ctx.fillStyle='rgba(4,6,16,0.62)';ctx.fillRect(0,0,W,H);
    // gold title
    ctx.font='800 72px system-ui,"Segoe UI",Arial,sans-serif';
    const g=ctx.createLinearGradient(0,200,0,272);
    g.addColorStop(0,'#fef3c7');g.addColorStop(0.5,'#fbbf24');g.addColorStop(1,'#f59e0b');
    ctx.fillStyle=g;ctx.shadowColor='rgba(251,191,36,0.65)';ctx.shadowBlur=30;
    ctx.fillText('YOU WIN!',W/2,238);ctx.shadowBlur=0;
    ctx.fillStyle='#e8f0ff';ctx.font='700 30px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('SCORE  '+score,W/2,312);
    if(winBonus>0){ctx.fillStyle='#4ade80';ctx.font='700 17px ui-monospace,Menlo,Consolas,monospace';
      ctx.fillText('+'+winBonus+' LIFE BONUS',W/2,346);}
    blinkText('CLICK OR PRESS ANY KEY TO PLAY AGAIN',414);
  }
}
function blinkText(text,y){
  ctx.globalAlpha=0.6+0.4*Math.sin(time*4.5);
  ctx.fillStyle='#ffffff';
  ctx.font='800 20px system-ui,"Segoe UI",Arial,sans-serif';
  ctx.fillText(text,W/2,y);
  ctx.globalAlpha=1;
}
function titleGradient(text,y,size){
  ctx.font='800 '+size+'px system-ui,"Segoe UI",Arial,sans-serif';
  const g=ctx.createLinearGradient(0,y-size*0.55,0,y+size*0.55);
  g.addColorStop(0,'#9be7ff');g.addColorStop(0.55,'#6d9dff');g.addColorStop(1,'#b57cff');
  ctx.fillStyle=g;
  ctx.shadowColor='rgba(105,140,255,0.55)';ctx.shadowBlur=26;
  ctx.fillText(text,W/2,y);
  ctx.shadowBlur=0;
}
function ls(v){ if('letterSpacing' in ctx) ctx.letterSpacing=v; }
```

drawHUD:

```
function drawHUD(){
  ctx.fillStyle='rgba(7,9,22,0.94)';ctx.fillRect(0,0,W,HUD_H);
  ctx.fillStyle='rgba(110,150,255,0.35)';ctx.fillRect(0,HUD_H-2,W,2);
  ctx.textBaseline='middle';
  ctx.textAlign='left';
  ls('3px');
  ctx.fillStyle='rgba(150,172,225,0.75)';ctx.font='700 11px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText('SCORE',22,17);ls('0px');
  ctx.fillStyle='#eef4ff';ctx.font='700 23px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText(String(score),22,38);
  ctx.textAlign='right';
  ls('3px');
  ctx.fillStyle='rgba(150,172,225,0.75)';ctx.font='700 11px ui-monospace,Menlo,Consolas,monospace';
  ctx.fillText('LIVES',W-22,17);ls('0px');
  for(let i=0;i<3;i++){
    const x0=W-48-26-i*34;
    if(i<lives){ctx.fillStyle='#38bdf8';rr(x0,26,26,7,3.5);ctx.fill();}
    else{ctx.strokeStyle='rgba(120,140,180,0.45)';ctx.lineWidth=1.5;rr(x0,26,26,7,3.5);ctx.stroke();}
  }
  if(wideT>0){
    ctx.textAlign='center';
    ctx.fillStyle='#4ade80';ctx.font='700 12px ui-monospace,Menlo,Consolas,monospace';
    ctx.fillText('WIDE PADDLE',W/2,16);
    ctx.fillStyle='rgba(74,222,128,0.25)';ctx.fillRect(W/2-52,26,104,5);
    ctx.fillStyle='#4ade80';ctx.fillRect(W/2-52,26,104*(wideT/10),5);
  } else if(demoMode){
    ctx.textAlign='center';
    ctx.fillStyle='rgba(150,172,225,0.55)';ctx.font='700 11px ui-monospace,Menlo,Consolas,monospace';
    ls('2px');ctx.fillText('DEMO · AI PLAYING',W/2,30);ls('0px');
  }
}
```

Lives icons positions: rightmost x0 for i=0: W-48-26=886 → spans 886..912; right margin 48 vs label right at W-22=938. Label 'LIVES' right edge 938. Icons start below at y 26. i=1: 852..878, i=2: 818..844. Good.

Now background:

```
function drawBackground(){
  ctx.fillStyle='#0a0d1f';ctx.fillRect(0,0,W,H);
  let g=ctx.createRadialGradient(W*0.22,H*0.15,0,W*0.22,H*0.15,430);
  g.addColorStop(0,'rgba(58,92,220,0.10)');g.addColorStop(1,'rgba(58,92,220,0)');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  g=ctx.createRadialGradient(W*0.82,H*0.8,0,W*0.82,H*0.8,470);
  g.addColorStop(0,'rgba(146,64,220,0.09)');g.addColorStop(1,'rgba(146,64,220,0)');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  ctx.strokeStyle='rgba(120,150,255,0.05)';ctx.lineWidth=1;
  ctx.beginPath();
  for(let x=48;x<W;x+=48){ctx.moveTo(x+0.5,HUD_H);ctx.lineTo(x+0.5,H);}
  for(let y=HUD_H+48;y<H;y+=48){ctx.moveTo(0,y+0.5);ctx.lineTo(W,y+0.5);}
  ctx.stroke();
  ctx.strokeStyle='rgba(112,150,255,0.4)';ctx.lineWidth=2;
  ctx.beginPath();ctx.moveTo(1,H);ctx.lineTo(1,HUD_H+1);ctx.lineTo(W-1,HUD_H+1);ctx.lineTo(W-1,H);ctx.stroke();
}
```

Also small glow along top border: skip.

drawBricks final (as planned). Note shadow reset inside loop save/restore. I used ctx.save/restore per brick — 96 saves fine.

Actually simplify: only set shadow for power bricks; wrap.

drawParticles as planned.

Check hitBrick float text: uses addFloat('+'+pts). Also on crack '+10'.

One more scoring nicety: catching capsule +50? Wide/multi are the reward; skip extra score... add +25 float 'POWER +25'? Keep: score+=25 on catch with float '+25'. Fine, small.

Actually applyPower already shows WIDE/MULTIBALL float. Add score+=25 silently? Skip to avoid clutter. No.

Init: buildBricks etc. via resetGame(true) then loop.

Also handle visibility change (tab switch) — dt clamped, fine.

Double-check demo overlay covering HUD? Overlay draws after HUD covering full canvas incl. HUD — dims HUD too; that's okay since HUD shows demo info... HUD drawn before overlay → dimmed. Hmm, HUD readability under 0.5 alpha dark — acceptable. Alternatively draw overlay before HUD. Better: draw overlay before HUD so HUD stays crisp. Reorder: drawOverlay() then drawHUD()? But overlay dims game world only then. Let me reorder render: world → overlay → HUD → hints. Then HUD text sits above overlay ✓. And hints (playing) drawn after HUD — fine anywhere.

But 'over'/'win' overlays with HUD on top: HUD shows score — fine.

Reorder render:
```
drawBackground(); save/shake: bricks,capsules,particles,paddle,balls,floats restore; drawOverlay(); drawHUD(); if playing drawPlayHints();
```
Good.

In demo the overlay text region y 236-466 over dimmed field; bricks at 96-307 visible above title? Title at y=236 overlaps brick rows (96..307) — with 0.5 dim it's fine aesthetically (title over bricks). Maybe shift title to 250. OK.

Hmm actually demo overlay dim 0.5 — bricks visible through, gameplay visible. 

Now — the screenshot at 3s: shows demo game + overlay + HUD. 

Potential issue: **autopl AI in demo never launches if stuck ball overlapped by paddle clamp?** launchBall sets velocities; fine.

**AI prediction when ball stuck**: skipped (continue) → target=W/2 default; paddle centers; then launch. Good.

**AI noise causing paddle to hold at capsule x while ball approaches**: acceptable.

Also ensure aiControl clamps paddle after move — update() clamps after control ✓.

Now the crack visual: also dim color overlay. ✓.

One more requirement reading: "Some bricks visibly carry a power-up icon; when broken they drop it". ✓ capsule falls from brick center.

"catching it with the paddle gives either a wider paddle (10 seconds) or multi-ball (3 balls)" ✓.

Edge: multiball when 2 balls exist → need=1 → spawn 1 → total 3 ✓.

Now finalize code text. Also add `e.preventDefault()` for arrow keys keydown to avoid iframe scroll (no scroll anyway, overflow hidden). Keep.

Also prevent context menu on long touch? minor skip.

Double-check rr() with r > half dims? r=3.5 on 26×7 fine; r=11 on 34×22: r=h/2=11 → arcTo fine (capsule). rr(-17,-11,34,22,11) ✓.

Paddle rr r=7 with h=14 → r=h/2 capsule-ish ✓.

Bricks r=5 with h=22 ✓.

Ball gradient radial each ball per frame — fine.

Trail draws before ball ✓.

Now assemble everything. Also add `document.addEventListener('visibilitychange')`? not needed.

Let me also ensure `applyPower` for 'wide' when in state playing vs demo identical ✓.

Write out; roughly 520 lines. Also ensure no stray references (loseBall defined, sparks defined, burst defined).

Also: In brickCollision, after hitBrick the brick may be dead; break ✓. But ball may still overlap a *second* brick this substep — next substep handles.

Also I should handle ball colliding with two bricks simultaneously in tight corner: penetration resolution picks least axis per brick sequentially — could jitter but rare; sub-steps keep it stable enough.

Potential subtle bug: In substep loop, after bouncePaddle sets vy negative, brickCollision still runs — fine.

Another: after wall reflect at left, `else if` for right — fine.

Check launchBall random angle ±0.35 rad ≈ ±20° ✓.

Check multiball clones' trail=[] ✓.

drawBalls: stuck ball trail empty ✓.

Score font: 'SCORE' label then value; value string via String(score) ✓.

HUD demo tag conflicts with wideT: shows wide widget instead — fine.

Final check on `keys` handling 'Space' code — e.code==='Space' ✓.

Also block repeated keydown (auto-repeat) for Space launching: harmless (launchAll only affects stuck balls).

Also Escape → primaryAction? In playing state, pressing Escape does nothing. fine.

Ensure no use of alert/etc ✓. No external requests ✓.

Write the final file now.

One more thought on canvas CSS `width:min(96vw,136vh)`: when viewport height small (e.g., 500), 136vh=680 → h=453 fits. Good.

Also add `image-rendering` nothing needed.

Also `body` fallback centering with flex; canvas aspect-ratio supported Chrome 88+ ✓.

Let me now also double check `resize()` when canvas CSS size is 0 (hidden?) — not our case.

Potential divide-by-zero rect.width if display none — no.

Now write it all out carefully.

Additional check — `hitBrick` increments score, floats; also `ballSpeed` rescale loop: `for(const ob of balls){const sp=Math.hypot(ob.vx,ob.vy); if(sp>0){const k=ballSpeed/sp; ob.vx*=k; ob.vy*=k;}}` — careful: stuck balls have sp 0 → skipped ✓.

Also after rescale, current ball b's velocity changed magnitude mid-substep — fine.

Demo AI: also make it slightly imperfect but catch most: maxV=480.

Also, during demo, mouseControl may flip true from pointermove — but aiControl used regardless of mouseControl in demo ✓.

When player starts (resetGame(false)), keys state may have stale arrow keys if user held key — fine.

Let me write `playerControl`:

```
function playerControl(dt){
  const L=keys['ArrowLeft']||keys['KeyA'],R=keys['ArrowRight']||keys['KeyD'];
  if(L)paddle.x-=640*dt;
  if(R)paddle.x+=640*dt;
  if(!L&&!R&&mouseControl)paddle.x+=(mouseX-paddle.x)*Math.min(1,dt*24);
}
```

Alright. Also add small paddle "tilt"? skip.

Compose final answer as single code block, no surrounding text (per output rules).

Let me write the complete final code:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Neon Breakout</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#05070f}
  body{display:flex;align-items:center;justify-content:center;
       background:radial-gradient(1100px 750px at 50% 38%,#0c1130 0%,#05070f 72%)}
  #game{width:min(96vw,136vh);aspect-ratio:3/2;display:block;border-radius:14px;
       background:#0a0d1f;touch-action:none;cursor:crosshair;
       box-shadow:0 0 50px rgba(60,110,255,.22),0 0 110px rgba(130,70,255,.10),0 0 0 1px rgba(130,160,255,.16)}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
'use strict';
(function(){
/* ===== constants ===== */
const W=960,H=640,HUD_H=54;
const COLS=12,ROWS=8,B_TOP=96,B_H=22,B_GAP=5,MARGIN=22;
const B_W=(W-MARGIN*2-(COLS-1)*B_GAP)/COLS;
const P_BASE_W=112,P_H=14,P_Y=H-34;
const BALL_R=7,SPEED0=430,SPEED_MAX=650;
const ROWS_DEF=[
  {hp:2,color:'#c084fc',pts:30},
  {hp:2,color:'#a78bfa',pts:30},
  {hp:2,color:'#818cf8',pts:30},
  {hp:1,color:'#38bdf8',pts:20},
  {hp:1,color:'#2dd4bf',pts:20},
  {hp:1,color:'#4ade80',pts:20},
  {hp:1,color:'#facc15',pts:20},
  {hp:1,color:'#fb923c',pts:20}
];
const TAU=Math.PI*2;
const rand=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
...
```

Proceed with full code as designed. Also careful: `ls` function name; also note `ctx.letterSpacing` needs reset — I call ls('0px').

In drawOverlay demo section I call ls('6px') before font set? letterSpacing applies at fillText time regardless of set order relative to font. Set before fillText ✓.

Also one more: overlay drawn each frame with shadowBlur resets ✓.

Also 'win' title font set before gradient — I set font then gradient using y extents constant — fine.

Now the final code, assembled:

Also don't forget paddle.flash property init: paddle={x:W/2,y:P_Y,w:P_BASE_W,tw:P_BASE_W,h:P_H,flash:0}; resetGame resets flash=0.

updateBalls stuck-ball launch in demo: launchBall(b) — b.stick already >0.55.

launchAll():
```
function launchAll(){for(const b of balls)if(b.stuck)launchBall(b);}
```

primaryAction as defined.

Pointer events: also 'pointerdown' preventDefault? Not necessary; but to avoid text selection/dragging—canvas only, fine.

Also add `canvas.addEventListener('pointermove')` instead of window? window catches even outside canvas — good for dragging beyond edges. Use window for both move and down? pointerdown on window: clicking anywhere on page (only canvas visible) — fine.

Edge: pointermove before any pointerdown sets mouseControl=true and mouseX — during demo irrelevant.

OK writing final.

Also last look at possible NaN: Math.atan2(0,0) if src velocity zero (stuck src chosen only if no moving ball; src=balls.find(!stuck) may be undefined → fallback else branch) ✓. In clone branch src exists non-stuck with speed>0 (always, since moving balls always have full speed). ✓.

`balls.find(b=>!b.stuck)||balls[0]` — I wrote earlier; in multiball: `let src=balls.find(b=>!b.stuck);` then if(src) clone else spawn-from-paddle fallback ✓.

Also winGame during multiball clone hitting last brick — state check in winGame guard `if(state!=='playing'&&state!=='demo')return;` ✓.

I think the design is solid. Also small feature: show "BRICKS x/96" in HUD? skip.

Time to write the definitive file. I'll be careful with braces.

Also, add subtle motion to background? Static fine.

Let me also double check drawBricks inner highlight rects don't cover crack strokes: order — base fill, highlight, shade, crack overlay (dark tint + strokes), icon, border ✓.

For hp===1 tough, the dark tint covers highlight giving "damaged" look ✓.

Also power brick cracked keeps icon on top ✓.

Final code:

(Also, one thing: `ctx.font='700 23px ui-monospace,Menlo,Consolas,monospace'` — ui-monospace works in Chrome; fallback Menlo/Consolas/monospace fine.)

Counting particles cap: many breaks → parts array grows; each break 16 particles life <1s; fine.

Write it.

Also verify: `if(paddle.flash>0)paddle.flash-=dt;` flash could go slightly negative — guard in draw with Math.max(0,...).

drawPaddle uses wideT>0 for colors ✓.

Also I realize the HUD WIDE widget draws at center where in demo the 'DEMO · AI PLAYING' also wants center — priority to wide ✓.

Now truly write out. I'll aim for clean, commented code.

Also: add `oncontextmenu` prevention? Right-click shows menu — irrelevant.

One more: keyboard 'any key starts' — includes modifier keys, F12? keydown for F12 fires but preventDefault not called; still starts game — acceptable ("press any key").

But avoid starting on Tab? whatever, fine.

Also, must ensure that when demo auto-restarts (resetGame(true)), state='demo' and overDelay=0 ✓.

Also after player's game over → user presses key → resetGame(false) fresh ✓.

Final answer code below.

Let me write drawParticles with 'lighter' composite and restore ✓.

Also floats drawn inside shake block ✓ (they're world-anchored).

Writing now — full file:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Neon Breakout</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#05070f}
  body{display:flex;align-items:center;justify-content:center;
    background:radial-gradient(1100px 750px at 50% 38%,#0c1130 0%,#05070f 72%)}
  #game{width:min(96vw,136vh);aspect-ratio:3/2;display:block;border-radius:14px;
    background:#0a0d1f;touch-action:none;cursor:crosshair;
    box-shadow:0 0 50px rgba(60,110,255,.22),0 0 110px rgba(130,70,255,.10),0 0 0 1px rgba(130,160,255,.16)}
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
'use strict';
(function(){
/* ---------- constants ---------- */
const W=960,H=640,HUD_H=54;
const COLS=12,ROWS=8,B_TOP=96,B_H=22,B_GAP=5,MARGIN=22;
const B_W=(W-MARGIN*2-(COLS-1)*B_GAP)/COLS;
const P_BASE_W=112,P_H=14,P_Y=H-34;
const BALL_R=7,SPEED0=430,SPEED_MAX=650;
const ROWS_DEF=[
  {hp:2,color:'#c084fc',pts:30},
  {hp:2,color:'#a78bfa',pts:30},
  {hp:2,color:'#818cf8',pts:30},
  {hp:1,color:'#38bdf8',pts:20},
  {hp:1,color:'#2dd4bf',pts:20},
  {hp:1,color:'#4ade80',pts:20},
  {hp:1,color:'#facc15',pts:20},
  {hp:1,color:'#fb923c',pts:20}
];
const TAU=Math.PI*2;
const rand=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);

/* ---------- canvas ---------- */
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
let scaleF=1;
function resize(){
  const r=canvas.getBoundingClientRect();
  const dpr=window.devicePixelRatio||1;
  const bw=Math.max(1,Math.round(r.width*dpr)),bh=Math.max(1,Math.round(r.height*dpr));
  if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
  scaleF=bw/W;
}
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();
}
function ls(v){ if('letterSpacing' in ctx) ctx.letterSpacing=v; }

/* ---------- audio ---------- */
let AC=null;
function ensureAudio(){
  try{
    if(!AC){const C=window.AudioContext||window.webkitAudioContext;if(C)AC=new C();}
    if(AC&&AC.state==='suspended')AC.resume().catch(function(){});
  }catch(e){}
}
function tone(freq,dur,type,vol,delay){
  if(!AC||AC.state!=='running')return;
  try{
    const t0=AC.currentTime+(delay||0);
    const o=AC.createOscillator(),g=AC.createGain();
    o.type=type||'square';
    o.frequency.setValueAtTime(freq,t0);
    g.gain.setValueAtTime(0.0001,t0);
    g.gain.exponentialRampToValueAtTime(vol||0.05,t0+0.01);
    g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
    o.connect(g);g.connect(AC.destination);
    o.start(t0);o.stop(t0+dur+0.03);
  }catch(e){}
}
const sfx={
  brick:function(){tone(520,0.07,'square',0.045);tone(260,0.09,'square',0.04);},
  crack:function(){tone(240,0.05,'square',0.035);},
  paddle:function(){tone(330,0.06,'triangle',0.06);},
  wall:function(){tone(190,0.035,'triangle',0.028);},
  power:function(){tone(523,0.09,'square',0.05);tone(659,0.09,'square',0.05,0.08);tone(784,0.14,'square',0.05,0.16);},
  lose:function(){tone(300,0.16,'sawtooth',0.06);tone(150,0.26,'sawtooth',0.06,0.13);},
  win:function(){[523,659,784,1046].forEach(function(f,i){tone(f,0.15,'square',0.05,i*0.11);});},
  over:function(){[330,262,196,131].forEach(function(f,i){tone(f,0.22,'sawtooth',0.05,i*0.15);});},
  launch:function(){tone(440,0.06,'triangle',0.045);}
};

/* ---------- state ---------- */
let state='demo',demoMode=true;
let score=0,lives=3,ballSpeed=SPEED0,winBonus=0;
let bricks=[],balls=[],capsules=[],parts=[],floats=[];
let paddle={x:W/2,y:P_Y,w:P_BASE_W,tw:P_BASE_W,h:P_H,flash:0};
let wideT=0,shake=0,time=0,overDelay=0;
let mouseX=W/2,mouseControl=true;
const keys={};
let aiNoiseX=0,aiNoiseT=0;
```

Then builders, etc. (as planned).

I'll now produce the final complete code block. Estimate ~470 lines. Watch syntax: arrow funcs, template? avoid backticks-with-${} fine to use, allowed. I'll use plain concatenation mostly.

Also careful with `ls` before font set for labels — order fine.

drawFloats: set textAlign center, baseline middle.

Also drawParticles resets composite.

drawCapsules with save/translate/rotate per capsule ✓.

One more check: paddle drawn even in over/win states (frozen) ✓ — with overlay dim on top ✓.

drawOverlay in 'over' shows cleared bricks count using bricks (still populated) ✓.

Edge: bricks.every on empty? bricks always 96 ✓.

Also `hitBrick` guard: if state over/win (can't happen since update early-returns)... skip.

Also make sure `update` doesn't run ball physics in 'over' — early return before paddle/balls ✓.

Alright, final write-out.

function drawBricks(){ for(const b of bricks){ if(b.hp<=0)continue; ... } }

Also I want a subtle "bricks left" glow on last few bricks? skip.

Compose the whole thing now. Also remember meta viewport.

After writing, mentally re-run for syntax slips (unmatched parens, etc.). I'll be methodical.

Also add restart hint also accepts click anywhere ✓ pointerdown.

Also the demo overlay note says take over anytime.

Let me finalize multiball code:

```
function multiball(){
  const need=3-balls.length;
  if(need<=0){
    score+=150;
    addFloat('+150 MAX BALLS',paddle.x,paddle.y-30,'#f472b6',15);
    return;
  }
  const src=balls.find(function(b){return !b.stuck;});
  for(let i=0;i<need;i++){
    if(src){
      const sp=Math.hypot(src.vx,src.vy)||ballSpeed;
      const a=Math.atan2(src.vy,src.vx)+(i===0?-0.45:0.45);
      balls.push({x:src.x,y:src.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,stuck:false,off:0,stick:0,trail:[]});
    }else{
      const a=(-90+(i-(need-1)/2)*55)*Math.PI/180;
      balls.push({x:paddle.x,y:paddle.y-BALL_R-2,vx:Math.cos(a)*ballSpeed,vy:Math.sin(a)*ballSpeed,stuck:false,off:0,stick:0,trail:[]});
    }
  }
  addFloat('MULTIBALL!',paddle.x,paddle.y-34,'#f472b6',18);
  burst(paddle.x,paddle.y,'#f472b6',20,260);
}
function applyPower(type){
  sfx.power();
  if(type==='wide'){
    wideT=10;paddle.tw=P_BASE_W*1.6;
    addFloat('WIDE PADDLE!',paddle.x,paddle.y-30,'#4ade80',18);
    burst(paddle.x,paddle.y,'#4ade80',18,240);
  }else multiball();
}
```

Note angle for fallback: -90°±: cos(-90°)=0, sin=-1 → straight up ✓ with 55° spread.

OK. Final code below. Let me also double check `drawHUD` after overlay: HUD covers top 54px; overlay dim covers everything including behind HUD; HUD re-drawn opaque on top ✓.

In demo, hint suppressed because state==='demo' (hints only for 'playing') ✓.

drawPlayHints only when state playing ✓.

Also stuck ball in playing: player may use mouse click to launch ✓.

Alright, writing the final file in full now. I'll double check every brace as I go.

Additionally: `window.addEventListener('keydown',...)` — also ignore if e.repeat for starting actions? primaryAction on repeat would re-reset repeatedly while held — but once state becomes 'playing', primaryAction no-ops ✓.

Space in playing with e.repeat: launchAll harmless.

One more possible pitfall: In demo, if the user's pointer sits over canvas, mouseControl irrelevant ✓.

After resetGame(false), mouseControl stays whatever; paddle lerps to mouseX ✓.

Ready.

Also quick sanity: B_W = (960-44-55)/12 = 861/12 = 71.75 ✓.

Row y: r=7 → 96+7*27=285, +22 = 307 ✓.

AI predict step=1/90, loop 600 → sim up to 6.6s ✓.

Final: ensure `requestAnimationFrame(frame)` starts after resetGame(true) so first frame has bricks ✓. Also call resize() once before loop (render also calls) ✓.

Also note: `render()` calls `resize()` first each frame — includes getBoundingClientRect; ok.

Write answer.