Models 44

Mistral Large 4

by MistralReasoningClean
Open in new window

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

How it went

Total time
11 min 44 s
Cost
$0.110
Output tokens
52,180
Spent thinking
42,754 tokens
Speed
74.3 tok/s
Console errors
0
First token after
1.2 s
First visible text after
9 min 52 s
Prompt tokens
528
Code size
24.4 KB
Finish reason
stop
Served by
Mistral
Model id
mistralai/mistral-large-4-0
Reasoning sent
{"effort":"high"}
Max output
262,144 tokens
Generated
Nov 12, 2026

What it was asked

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

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

Code run-1.html · 633 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Neon Breakout</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;height:100%}
  body{
    background:radial-gradient(1100px 700px at 50% 18%, #111b3d 0%, #080d1d 55%, #04060d 100%);
    display:flex;align-items:center;justify-content:center;
    font-family:"Segoe UI",system-ui,-apple-system,Arial,sans-serif;
    overflow:hidden;color:#dfe9ff;
    -webkit-user-select:none;user-select:none;
  }
  #wrap{position:relative;width:min(96vw,calc((100vh - 28px)*1.33333),1060px)}
  @supports (width:100dvh){#wrap{width:min(96vw,calc((100dvh - 28px)*1.33333),1060px)}}
  canvas{
    display:block;width:100%;height:auto;aspect-ratio:800/600;
    background:#070b18;border-radius:12px;touch-action:none;
    box-shadow:0 0 0 1px rgba(100,170,255,.16),0 22px 70px rgba(0,0,0,.65),0 0 90px rgba(40,120,255,.12);
  }
  #overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:10;pointer-events:none;border-radius:12px;overflow:hidden}
  #overlay.hidden{display:none}
  #overlay.demo{align-items:flex-start;padding-top:14px;background:linear-gradient(to bottom,rgba(4,8,18,.72),rgba(4,8,18,0) 36%)}
  #overlay.over,#overlay.win{background:rgba(4,8,18,.5)}
  .pill{display:flex;align-items:center;gap:10px;max-width:92%;flex-wrap:wrap;justify-content:center;
    background:rgba(8,14,30,.88);border:1px solid rgba(96,205,255,.45);color:#cfeaff;
    padding:10px 18px;border-radius:999px;font-size:14px;letter-spacing:.4px;text-align:center;
    box-shadow:0 0 26px rgba(60,170,255,.28),inset 0 0 14px rgba(80,180,255,.08)}
  .pill b{color:#7fe3ff}
  .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#ff4d6d;box-shadow:0 0 10px #ff4d6d;animation:blink 1s infinite}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
  .panel{pointer-events:auto;text-align:center;background:rgba(8,13,28,.85);border:1px solid rgba(110,200,255,.35);
    border-radius:16px;padding:30px 48px;box-shadow:0 0 55px rgba(50,150,255,.28),inset 0 0 30px rgba(60,150,255,.06);
    animation:pop .3s ease}
  @keyframes pop{from{transform:scale(.85);opacity:0}}
  .panel h1{margin:0 0 8px;font-size:44px;letter-spacing:5px;font-weight:800}
  .panel h1.red{background:linear-gradient(180deg,#ffb199,#ff2d55);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 16px rgba(255,45,85,.45))}
  .panel h1.gold{background:linear-gradient(180deg,#fff3b0,#ffb700);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 16px rgba(255,190,40,.45))}
  .panel .score{font-size:20px;font-weight:700;letter-spacing:2px;color:#ffe08a;margin:4px 0 20px}
  .panel button{cursor:pointer;font:inherit;font-weight:700;font-size:16px;letter-spacing:2px;color:#06121f;
    background:linear-gradient(180deg,#8fe9ff,#38b6ff);border:none;border-radius:10px;padding:13px 28px;
    box-shadow:0 0 24px rgba(80,190,255,.55),0 4px 0 #1a6ea8;transition:transform .08s,box-shadow .2s}
  .panel button:hover{box-shadow:0 0 36px rgba(130,225,255,.85),0 4px 0 #1a6ea8;transform:translateY(-1px)}
  .panel button:active{transform:translateY(2px);box-shadow:0 0 12px rgba(80,190,255,.5),0 1px 0 #1a6ea8}
  .panel .hint{margin:14px 0 0;font-size:12px;letter-spacing:1.5px;color:rgba(190,220,255,.55)}
  @media (max-width:520px){
    .panel{padding:20px 24px}
    .panel h1{font-size:30px;letter-spacing:3px}
    .pill{font-size:12px;padding:8px 14px}
  }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="800" height="600"></canvas>
  <div id="overlay" class="hidden"></div>
</div>
<script>
'use strict';
/* ---------- setup ---------- */
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const overlay=document.getElementById('overlay');
const W=800,H=600,HUD_H=46,WALL=10;
const FIELD_L=WALL,FIELD_R=W-WALL,FIELD_TOP=HUD_H+WALL;
const DPR=Math.min(window.devicePixelRatio||1,2);
canvas.width=W*DPR;canvas.height=H*DPR;
ctx.setTransform(DPR,0,0,DPR,0,0);

const ROWS=8,COLS=10,BGAP=6,BH=22,BM=14;
const brickW=(FIELD_R-FIELD_L-BM*2-BGAP*(COLS-1))/COLS;
const BTOP=FIELD_TOP+16;
const BASE_SPEED=400,MAX_SPEED=660;

const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function shade(hex,amt){const n=parseInt(hex.slice(1),16);let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
  if(amt>=0){r+=(255-r)*amt;g+=(255-g)*amt;b+=(255-b)*amt;}else{r*=1+amt;g*=1+amt;b*=1+amt;}
  return 'rgb('+(r|0)+','+(g|0)+','+(b|0)+')';}
function rr(x,y,w,h,r){r=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}

/* ---------- state ---------- */
let state='demo';           // demo | playing | over | win
let score=0,lives=3,ballSpeed=BASE_SPEED;
let bricks=[],balls=[],powerups=[],particles=[],floaters=[];
const paddle={x:W/2,y:H-38,w:112,h:14,expandTimer:0};
let mouseX=W/2,time=0,shake=0;
const keys=new Set();
const rowColors=['#ff4757','#ff7f50','#ffa502','#eccc68','#2ed573','#1e90ff','#3742fa','#a55eea'];
const stars=[];
for(let i=0;i<70;i++)stars.push({x:Math.random()*W,y:Math.random()*H,s:0.6+Math.random()*1.4,sp:6+Math.random()*18,ph:Math.random()*6.28});
const bgGrad=ctx.createLinearGradient(0,0,0,H);
bgGrad.addColorStop(0,'#0a1128');bgGrad.addColorStop(0.55,'#070b18');bgGrad.addColorStop(1,'#04060d');
const wallGrad=ctx.createLinearGradient(0,HUD_H,0,H);
wallGrad.addColorStop(0,'#1b2b4d');wallGrad.addColorStop(1,'#101a33');

/* ---------- level ---------- */
function makeCracks(rng){
  const w=brickW,h=BH,lines=[];
  for(let i=0;i<3;i++){
    let x=w*(0.2+0.6*rng()),y=h*(0.15+0.7*rng());
    const pts=[[x,y]];let ang=rng()*6.28;
    for(let s=0;s<4;s++){ang+=(rng()-0.5)*1.8;
      x=clamp(x+Math.cos(ang)*w*0.18,2,w-2);y=clamp(y+Math.sin(ang)*h*0.34,2,h-2);
      pts.push([x,y]);}
    lines.push(pts);
  }
  return lines;
}
function buildLevel(){
  bricks=[];
  const rng=mulberry32((Math.random()*1e9)>>>0);
  for(let r=0;r<ROWS;r++){
    for(let c=0;c<COLS;c++){
      const x=FIELD_L+BM+c*(brickW+BGAP),y=BTOP+r*(BH+BGAP);
      let type='normal';
      if(r===0||r===1)type='tough';
      else if(r===3&&c%4===1)type='tough';
      else if(r===5&&c%3===0)type='tough';
      const g=ctx.createLinearGradient(0,y,0,y+BH);
      if(type==='tough'){g.addColorStop(0,'#d6dee9');g.addColorStop(0.5,'#8b98ab');g.addColorStop(1,'#57637a');}
      else{const col=rowColors[r];g.addColorStop(0,shade(col,0.38));g.addColorStop(0.55,col);g.addColorStop(1,shade(col,-0.4));}
      bricks.push({x,y,w:brickW,h:BH,type,row:r,hp:type==='tough'?2:1,alive:true,cracked:false,power:null,grad:g,cracks:makeCracks(rng)});
    }
  }
  const order=bricks.map((_,i)=>i);
  for(let i=order.length-1;i>0;i--){const j=(rng()*(i+1))|0;const t=order[i];order[i]=order[j];order[j]=t;}
  for(let k=0;k<10;k++)bricks[order[k]].power=(k%2===0)?'expand':'multiball';
}

/* ---------- flow ---------- */
function respawnBall(){
  balls=[{x:paddle.x,y:paddle.y-9,r:7,vx:0,vy:0,speed:ballSpeed,stuck:true,timer:state==='demo'?0.7:1.0,trail:[]}];
  paddle.expandTimer=0;
}
function startGame(){
  if(state==='playing')return;
  state='playing';
  score=0;lives=3;ballSpeed=BASE_SPEED;
  paddle.x=W/2;paddle.w=112;paddle.expandTimer=0;mouseX=W/2;
  powerups.length=0;particles.length=0;floaters.length=0;
  buildLevel();respawnBall();
  showOverlay('hidden');
}
function showOverlay(kind){
  if(kind==='demo'){
    overlay.className='demo';
    overlay.innerHTML='<div class="pill"><span class="dot"></span><span><b>DEMO</b> &mdash; the AI is playing &nbsp;&middot;&nbsp; <b>Click or press any key to start</b></span></div>';
  }else if(kind==='over'){
    overlay.className='over';
    overlay.innerHTML='<div class="panel"><h1 class="red">GAME OVER</h1><div class="score">SCORE&nbsp; '+score+'</div><button type="button">&#8635;&nbsp; PLAY AGAIN</button><div class="hint">or press any key</div></div>';
  }else if(kind==='win'){
    overlay.className='win';
    overlay.innerHTML='<div class="panel"><h1 class="gold">&#9733; YOU WIN! &#9733;</h1><div class="score">SCORE&nbsp; '+score+'</div><button type="button">&#8635;&nbsp; PLAY AGAIN</button><div class="hint">or press any key</div></div>';
  }else{
    overlay.className='hidden';
  }
}

/* ---------- audio (lazy, gesture-safe) ---------- */
let AC=null;
function ensureAudio(){
  if(!AC){try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){AC=null;}}
  if(AC&&AC.state==='suspended'){AC.resume().catch(()=>{});}
}
function beep(freq,dur,type,vol,when){
  if(!AC)return;
  try{
    const t=AC.currentTime+(when||0);
    const o=AC.createOscillator(),g=AC.createGain();
    o.type=type||'square';o.frequency.setValueAtTime(freq,t);
    g.gain.setValueAtTime(vol||0.05,t);
    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    o.connect(g);g.connect(AC.destination);
    o.start(t);o.stop(t+dur+0.03);
  }catch(e){}
}

/* ---------- input ---------- */
window.addEventListener('pointermove',e=>{
  const r=canvas.getBoundingClientRect();
  mouseX=(e.clientX-r.left)*(W/r.width);
});
window.addEventListener('pointerdown',e=>{
  ensureAudio();
  const r=canvas.getBoundingClientRect();
  mouseX=(e.clientX-r.left)*(W/r.width);
  if(state!=='playing')startGame();
  else{for(const b of balls)if(b.stuck)b.timer=0;}
});
window.addEventListener('keydown',e=>{
  if(e.key.indexOf('Arrow')===0||e.key===' ')e.preventDefault();
  ensureAudio();
  keys.add(e.key);
  if(state!=='playing')startGame();
});
window.addEventListener('keyup',e=>keys.delete(e.key));
window.addEventListener('blur',()=>keys.clear());

/* ---------- demo AI ---------- */
function aiControl(dt){
  let target=W/2,lowest=null;
  for(const b of balls){if(!b.stuck&&(!lowest||b.y>lowest.y))lowest=b;}
  if(lowest)target=lowest.x+lowest.vx*0.08+Math.sin(time*2.3)*6;
  if(powerups.length&&powerups[0].y>240)target=powerups[0].x;
  const maxSp=520,d=target-mouseX;
  mouseX+=clamp(d,-maxSp*dt,maxSp*dt);
}

/* ---------- gameplay helpers ---------- */
function setSpeed(b,s){const m=Math.hypot(b.vx,b.vy)||1;b.vx=b.vx/m*s;b.vy=b.vy/m*s;b.speed=s;}
function fixAngle(b){
  const minVy=b.speed*0.22;
  if(Math.abs(b.vy)<minVy){
    const dir=b.vy<0?-1:1;
    const vx=Math.sqrt(Math.max(b.speed*b.speed-minVy*minVy,0));
    b.vy=dir*minVy;b.vx=(b.vx<0?-1:1)*vx;
  }
}
function burst(x,y,color,n,pow){
  for(let i=0;i<n;i++){
    const a=Math.random()*6.283,sp=(90+Math.random()*240)*pow;
    const max=0.45+Math.random()*0.5;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-120,size:1.4+Math.random()*2.6,life:max,max,color:Math.random()<0.25?'#ffffff':color});
  }
}
function addFloater(x,y,text,color){floaters.push({x,y,text,color,vy:-46,life:0.9,max:0.9});}
function dropPower(br){powerups.push({x:br.x+br.w/2,y:br.y+br.h/2,vy:150,w:40,h:18,type:br.power,bob:Math.random()*6.28});}
function hitBrick(br){
  br.hp--;
  if(br.hp<=0){
    br.alive=false;
    const pts=(br.type==='tough'?150:100)+(ROWS-1-br.row)*10;
    score+=pts;
    addFloater(br.x+br.w/2,br.y+br.h/2,'+'+pts,br.type==='tough'?'#ffd32a':'#eaf6ff');
    burst(br.x+br.w/2,br.y+br.h/2,br.type==='tough'?'#9fb2c8':rowColors[br.row],18,1);
    shake=Math.min(shake+3,5);
    if(br.power)dropPower(br);
    ballSpeed=Math.min(MAX_SPEED,ballSpeed+5);
    for(const b of balls)if(!b.stuck)setSpeed(b,ballSpeed);
    beep(500+Math.random()*180,0.08,'square',0.045);
  }else{
    br.cracked=true;
    score+=25;
    addFloater(br.x+br.w/2,br.y+br.h/2,'+25','#9aa7bd');
    burst(br.x+br.w/2,br.y+br.h/2,'#8b98ab',7,0.6);
    beep(280,0.07,'sawtooth',0.05);
  }
}
function applyPower(p){
  score+=25;
  if(p.type==='expand'){
    paddle.expandTimer=10;
    addFloater(paddle.x,paddle.y-26,'WIDE PADDLE 10s','#3dff9c');
    beep(620,0.09,'sine',0.06);beep(930,0.12,'sine',0.06,0.08);
  }else{
    if(balls.length<3){
      const q=[-0.55,0.55,0];
      while(balls.length<3){
        const a=q.shift();
        balls.push({x:paddle.x,y:paddle.y-10,r:7,speed:ballSpeed,stuck:false,timer:0,trail:[],
          vx:ballSpeed*Math.sin(a),vy:-ballSpeed*Math.cos(a)});
      }
      addFloater(paddle.x,paddle.y-26,'MULTI-BALL x3','#f45df0');
      beep(520,0.08,'triangle',0.06);beep(780,0.08,'triangle',0.06,0.07);beep(1040,0.12,'triangle',0.06,0.14);
    }else{
      score+=150;
      addFloater(paddle.x,paddle.y-26,'+150  MAX BALLS','#ffd32a');
      beep(880,0.1,'triangle',0.06);
    }
  }
}

/* ---------- collisions (sub-stepped: no tunneling) ---------- */
function collideBricks(b,px,py){
  for(let i=0;i<bricks.length;i++){
    const br=bricks[i];
    if(!br.alive)continue;
    if(b.x+b.r<br.x||b.x-b.r>br.x+br.w||b.y+b.r<br.y||b.y-b.r>br.y+br.h)continue;
    const cx=clamp(b.x,br.x,br.x+br.w),cy=clamp(b.y,br.y,br.y+br.h);
    const dx=b.x-cx,dy=b.y-cy;
    if(dx*dx+dy*dy>b.r*b.r)continue;
    let nx=0,ny=0;
    if(py+b.r<=br.y+0.01)ny=-1;
    else if(py-b.r>=br.y+br.h-0.01)ny=1;
    else if(px+b.r<=br.x+0.01)nx=-1;
    else if(px-b.r>=br.x+br.w-0.01)nx=1;
    else{
      const ol=(b.x+b.r)-br.x,orr=(br.x+br.w)-(b.x-b.r);
      const ot=(b.y+b.r)-br.y,ob=(br.y+br.h)-(b.y-b.r);
      if(Math.min(ol,orr)<Math.min(ot,ob))nx=ol<orr?-1:1;else ny=ot<ob?-1:1;
    }
    if(nx!==0){b.vx=(nx<0?-1:1)*Math.abs(b.vx);b.x=nx<0?br.x-b.r:br.x+br.w+b.r;}
    else{b.vy=(ny<0?-1:1)*Math.abs(b.vy);b.y=ny<0?br.y-b.r:br.y+br.h+b.r;}
    fixAngle(b);
    hitBrick(br);
    return;
  }
}
function collidePaddle(b,px,py){
  const half=paddle.w/2;
  if(b.y+b.r<paddle.y||b.y-b.r>paddle.y+paddle.h)return;
  if(Math.abs(b.x-paddle.x)>half+b.r)return;
  if(b.vy>0&&py+b.r<=paddle.y+0.01){
    const rel=clamp((b.x-paddle.x)/half,-1,1);   // -1 left edge .. 0 center .. 1 right edge
    const ang=rel*Math.PI*0.36;                   // center = straight up, edges = sharp
    b.vx=b.speed*Math.sin(ang);
    b.vy=-b.speed*Math.cos(ang);
    b.y=paddle.y-b.r;
    burst(b.x,paddle.y,'#8fd8ff',4,0.35);
    beep(200+Math.abs(rel)*120,0.05,'square',0.05);
  }else if(b.vy>0){
    const side=px<paddle.x?-1:1;
    b.vx=side*Math.abs(b.vx);
    b.x=paddle.x+side*(half+b.r);
  }else{
    b.y=paddle.y-b.r;
    b.vy=-Math.abs(b.vy);
  }
}

/* ---------- update ---------- */
function update(dt){
  time+=dt;
  for(const s of stars){s.y+=s.sp*dt;if(s.y>H){s.y=-2;s.x=Math.random()*W;}}

  if(state==='demo')aiControl(dt);
  else{
    let dir=0;
    if(keys.has('ArrowLeft')||keys.has('a')||keys.has('A'))dir--;
    if(keys.has('ArrowRight')||keys.has('d')||keys.has('D'))dir++;
    if(dir)mouseX+=dir*760*dt;
  }

  if(paddle.expandTimer>0)paddle.expandTimer=Math.max(0,paddle.expandTimer-dt);
  const targetW=paddle.expandTimer>0?172:112;
  paddle.w+=(targetW-paddle.w)*Math.min(1,dt*12);
  const minX=FIELD_L+paddle.w/2+1,maxX=FIELD_R-paddle.w/2-1;
  mouseX=clamp(mouseX,minX,maxX);
  paddle.x+=(mouseX-paddle.x)*Math.min(1,dt*40);
  paddle.x=clamp(paddle.x,minX,maxX);

  for(let i=powerups.length-1;i>=0;i--){
    const p=powerups[i];
    p.y+=p.vy*dt;p.bob+=dt*5;
    if(p.y+p.h/2>=paddle.y&&p.y-p.h/2<=paddle.y+paddle.h&&Math.abs(p.x-paddle.x)<=paddle.w/2+p.w/2){
      applyPower(p);powerups.splice(i,1);continue;
    }
    if(p.y-p.h>H)powerups.splice(i,1);
  }

  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    if(b.stuck){
      b.x=paddle.x;b.y=paddle.y-b.r-1;b.trail.length=0;
      b.timer-=dt;
      if(b.timer<=0){b.stuck=false;const a=(Math.random()-0.5)*0.5;b.vx=b.speed*Math.sin(a);b.vy=-b.speed*Math.cos(a);}
    }else{
      const dist=b.speed*dt;
      const steps=Math.max(1,Math.ceil(dist/(b.r*0.8)));
      const sdt=dt/steps;
      for(let s=0;s<steps;s++){
        const px=b.x,py=b.y;
        b.x+=b.vx*sdt;b.y+=b.vy*sdt;
        if(b.x<FIELD_L+b.r){b.x=FIELD_L+b.r;b.vx=Math.abs(b.vx);fixAngle(b);}
        else if(b.x>FIELD_R-b.r){b.x=FIELD_R-b.r;b.vx=-Math.abs(b.vx);fixAngle(b);}
        if(b.y<FIELD_TOP+b.r){b.y=FIELD_TOP+b.r;b.vy=Math.abs(b.vy);}
        collideBricks(b,px,py);
        collidePaddle(b,px,py);
      }
      b.trail.push({x:b.x,y:b.y});
      if(b.trail.length>10)b.trail.shift();
    }
    if(b.y-b.r>H)balls.splice(i,1);
  }

  if(balls.length===0&&(state==='playing'||state==='demo')){
    if(state==='demo')respawnBall();
    else{
      lives--;
      if(lives<=0){
        state='over';showOverlay('over');
        beep(320,0.15,'sawtooth',0.06);beep(210,0.2,'sawtooth',0.06,0.13);beep(120,0.4,'sawtooth',0.06,0.28);
      }else{respawnBall();beep(180,0.18,'sawtooth',0.06);}
    }
  }

  if(bricks.length>0&&bricks.every(b=>!b.alive)){
    if(state==='demo')buildLevel();
    else if(state==='playing'){
      state='win';showOverlay('win');
      beep(523,0.1,'triangle',0.06);beep(659,0.1,'triangle',0.06,0.1);beep(784,0.1,'triangle',0.06,0.2);beep(1047,0.25,'triangle',0.07,0.3);
    }
  }

  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];p.life-=dt;
    if(p.life<=0){particles.splice(i,1);continue;}
    p.vy+=700*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;
  }
  for(let i=floaters.length-1;i>=0;i--){
    const f=floaters[i];f.life-=dt;f.y+=f.vy*dt;
    if(f.life<=0)floaters.splice(i,1);
  }
  shake=Math.max(0,shake-dt*16);
}

/* ---------- drawing ---------- */
function drawIcon(type,cx,cy,s,color){
  ctx.save();
  ctx.strokeStyle=color;ctx.fillStyle=color;
  ctx.lineWidth=Math.max(1.5,s*0.28);ctx.lineCap='round';ctx.lineJoin='round';
  if(type==='expand'){
    const y=cy,x0=cx-s,x1=cx+s;
    ctx.beginPath();
    ctx.moveTo(x0,y);ctx.lineTo(x1,y);
    ctx.moveTo(x0,y);ctx.lineTo(x0+s*0.55,y-s*0.55);
    ctx.moveTo(x0,y);ctx.lineTo(x0+s*0.55,y+s*0.55);
    ctx.moveTo(x1,y);ctx.lineTo(x1-s*0.55,y-s*0.55);
    ctx.moveTo(x1,y);ctx.lineTo(x1-s*0.55,y+s*0.55);
    ctx.stroke();
  }else{
    const r=s*0.34;
    ctx.beginPath();
    ctx.arc(cx,cy-s*0.5,r,0,6.29);
    ctx.arc(cx-s*0.62,cy+s*0.42,r,0,6.29);
    ctx.arc(cx+s*0.62,cy+s*0.42,r,0,6.29);
    ctx.fill();
  }
  ctx.restore();
}
function drawStars(){
  ctx.save();
  for(const s of stars){
    ctx.globalAlpha=0.12+0.18*(0.5+0.5*Math.sin(time*2+s.ph));
    ctx.fillStyle='#8fc7ff';
    ctx.fillRect(s.x,s.y,s.s,s.s);
  }
  ctx.restore();
}
function drawGrid(){
  ctx.strokeStyle='rgba(90,140,255,0.05)';ctx.lineWidth=1;
  ctx.beginPath();
  for(let x=FIELD_L;x<=FIELD_R;x+=40){ctx.moveTo(x,FIELD_TOP);ctx.lineTo(x,H);}
  for(let y=FIELD_TOP;y<H;y+=40){ctx.moveTo(0,y);ctx.lineTo(W,y);}
  ctx.stroke();
}
function drawWalls(){
  ctx.fillStyle=wallGrad;
  ctx.fillRect(0,HUD_H,WALL,H-HUD_H);
  ctx.fillRect(W-WALL,HUD_H,WALL,H-HUD_H);
  ctx.fillRect(0,HUD_H,W,WALL);
  ctx.save();
  ctx.shadowColor='rgba(90,200,255,0.9)';ctx.shadowBlur=8;
  ctx.fillStyle='#7fe3ff';
  ctx.fillRect(WALL-2,HUD_H,2,H-HUD_H);
  ctx.fillRect(W-WALL,HUD_H,2,H-HUD_H);
  ctx.fillRect(0,HUD_H+WALL-2,W,2);
  ctx.restore();
}
function drawBrick(br){
  ctx.save();
  ctx.shadowColor='rgba(0,0,0,0.45)';ctx.shadowBlur=3;ctx.shadowOffsetY=2;
  rr(br.x,br.y,br.w,br.h,5);
  ctx.fillStyle=br.grad;ctx.fill();
  ctx.shadowColor='rgba(0,0,0,0)';ctx.shadowBlur=0;ctx.shadowOffsetY=0;
  ctx.lineWidth=1;
  ctx.strokeStyle=br.type==='tough'?'rgba(28,36,50,0.9)':'rgba(0,0,0,0.3)';
  rr(br.x+0.5,br.y+0.5,br.w-1,br.h-1,5);ctx.stroke();
  if(br.type==='tough'){
    ctx.strokeStyle='rgba(255,255,255,0.3)';
    rr(br.x+2.5,br.y+2.5,br.w-5,br.h-5,3);ctx.stroke();
    ctx.fillStyle='rgba(22,28,40,0.85)';
    const o=5;
    ctx.beginPath();
    ctx.arc(br.x+o,br.y+o,1.7,0,6.29);ctx.arc(br.x+br.w-o,br.y+o,1.7,0,6.29);
    ctx.arc(br.x+o,br.y+br.h-o,1.7,0,6.29);ctx.arc(br.x+br.w-o,br.y+br.h-o,1.7,0,6.29);
    ctx.fill();
    if(br.cracked){
      ctx.fillStyle='rgba(8,12,20,0.3)';
      rr(br.x,br.y,br.w,br.h,5);ctx.fill();
      ctx.strokeStyle='rgba(12,16,24,0.9)';ctx.lineWidth=1.4;
      for(const line of br.cracks){
        ctx.beginPath();
        ctx.moveTo(br.x+line[0][0],br.y+line[0][1]);
        for(let i=1;i<line.length;i++)ctx.lineTo(br.x+line[i][0],br.y+line[i][1]);
        ctx.stroke();
      }
    }
  }else{
    ctx.fillStyle='rgba(255,255,255,0.3)';
    rr(br.x+2,br.y+2,br.w-4,4.5,2.2);ctx.fill();
  }
  if(br.power){
    ctx.save();
    ctx.shadowColor='rgba(255,255,255,0.95)';ctx.shadowBlur=7;
    drawIcon(br.power,br.x+br.w/2,br.y+br.h/2,6.5,'rgba(255,255,255,0.95)');
    ctx.restore();
  }
  ctx.restore();
}
function drawPower(p){
  const w=p.w,h=p.h,x=p.x-w/2,y=p.y-h/2;
  const col=p.type==='expand'?'#35f0a0':'#f45df0';
  ctx.save();
  ctx.shadowColor=col;ctx.shadowBlur=13;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  g.addColorStop(0,'#ffffff');g.addColorStop(0.35,col);g.addColorStop(1,shade(col,-0.45));
  rr(x,y,w,h,h/2);ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  ctx.strokeStyle='rgba(255,255,255,0.8)';ctx.lineWidth=1.2;
  rr(x+0.6,y+0.6,w-1.2,h-1.2,h/2-0.6);ctx.stroke();
  drawIcon(p.type,p.x,p.y+Math.sin(p.bob)*1.2,6.2,'#0a1020');
  ctx.restore();
}
function drawParticles(){
  for(const p of particles){
    ctx.globalAlpha=Math.max(p.life/p.max,0);
    ctx.fillStyle=p.color;
    ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,6.29);ctx.fill();
  }
  ctx.globalAlpha=1;
}
function drawPaddle(){
  const w=paddle.w,h=paddle.h,x=paddle.x-w/2,y=paddle.y;
  const ex=paddle.expandTimer>0;
  ctx.save();
  ctx.shadowColor=ex?'rgba(61,255,156,0.9)':'rgba(77,201,255,0.9)';
  ctx.shadowBlur=16;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  if(ex){g.addColorStop(0,'#7dffc0');g.addColorStop(1,'#17b862');}
  else{g.addColorStop(0,'#8fe9ff');g.addColorStop(1,'#1e90ff');}
  rr(x,y,w,h,h/2);ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='rgba(255,255,255,0.55)';
  rr(x+3,y+2,w-6,3.5,1.8);ctx.fill();
  ctx.restore();
}
function drawBall(b){
  for(let i=0;i<b.trail.length;i++){
    const t=b.trail[i],f=(i+1)/b.trail.length;
    ctx.globalAlpha=f*0.22;
    ctx.fillStyle='#8fd8ff';
    ctx.beginPath();ctx.arc(t.x,t.y,b.r*(0.35+0.65*f),0,6.29);ctx.fill();
  }
  ctx.globalAlpha=1;
  ctx.save();
  ctx.shadowColor='#bfeaff';ctx.shadowBlur=14;
  const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r);
  g.addColorStop(0,'#ffffff');g.addColorStop(1,'#9fdcff');
  ctx.fillStyle=g;
  ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,6.29);ctx.fill();
  ctx.restore();
}
function drawFloaters(){
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='700 13px "Segoe UI",system-ui,Arial,sans-serif';
  for(const f of floaters){
    ctx.globalAlpha=Math.max(f.life/f.max,0);
    ctx.fillStyle=f.color;
    ctx.fillText(f.text,f.x,f.y);
  }
  ctx.globalAlpha=1;
}
function drawHeart(cx,cy,s,filled){
  ctx.beginPath();
  ctx.moveTo(cx,cy+s*0.9);
  ctx.bezierCurveTo(cx-s*1.1,cy+s*0.1,cx-s*0.55,cy-s*0.85,cx,cy-s*0.25);
  ctx.bezierCurveTo(cx+s*0.55,cy-s*0.85,cx+s*1.1,cy+s*0.1,cx,cy+s*0.9);
  ctx.closePath();
  if(filled){ctx.save();ctx.shadowColor='#ff4d6d';ctx.shadowBlur=8;ctx.fillStyle='#ff4d6d';ctx.fill();ctx.restore();}
  else{ctx.strokeStyle='rgba(255,77,109,0.3)';ctx.lineWidth=1.4;ctx.stroke();}
}
function drawHUD(){
  const g=ctx.createLinearGradient(0,0,0,HUD_H);
  g.addColorStop(0,'#0d1530');g.addColorStop(1,'#0a0f22');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,HUD_H);
  ctx.fillStyle='rgba(96,200,255,0.28)';ctx.fillRect(0,HUD_H-1,W,1);
  ctx.textBaseline='middle';
  ctx.textAlign='left';
  ctx.font='700 15px "Segoe UI",system-ui,Arial,sans-serif';
  ctx.fillStyle='#9fdcff';
  ctx.fillText('SCORE '+String(score).padStart(6,'0'),16,HUD_H/2+1);
  if(paddle.expandTimer>0){
    ctx.fillStyle='#3dff9c';
    ctx.fillText('WIDE '+paddle.expandTimer.toFixed(1)+'s',180,HUD_H/2+1);
  }
  ctx.textAlign='center';
  ctx.font='600 10px "Segoe UI",system-ui,Arial,sans-serif';
  ctx.fillStyle='rgba(150,200,255,0.45)';
  ctx.fillText('N E O N   B R E A K O U T',W/2,HUD_H/2+1);
  for(let i=0;i<3;i++)drawHeart(W-22-i*26,HUD_H/2+1,11,i<lives);
}
function render(){
  ctx.save();
  if(shake>0.15)ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
  ctx.fillStyle=bgGrad;ctx.fillRect(-8,-8,W+16,H+16);
  drawStars();drawGrid();drawWalls();
  for(const br of bricks)if(br.alive)drawBrick(br);
  for(const p of powerups)drawPower(p);
  drawParticles();
  drawPaddle();
  for(const b of balls)drawBall(b);
  drawFloaters();
  ctx.restore();
  drawHUD();
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='11px "Segoe UI",system-ui,Arial,sans-serif';
  ctx.fillStyle='rgba(150,190,255,0.32)';
  ctx.fillText('move: mouse or \u2190 \u2192 arrow keys',W/2,H-9);
}

/* ---------- main loop ---------- */
let last=performance.now();
function frame(now){
  let dt=(now-last)/1000;last=now;
  if(dt>0.05)dt=0.05;
  if(dt>0)update(dt);
  render();
  requestAnimationFrame(frame);
}

buildLevel();
respawnBall();
showOverlay('demo');
requestAnimationFrame(frame);
</script>
</body>
</html>
Reasoning trace (42,754 tokens)
The user wants a polished Breakout game in a single HTML file with vanilla JS and canvas. Let me build a complete, self-contained game.

Key requirements:
- Paddle controlled with mouse AND left/right arrow keys
- Bounce angle depends on hit position on paddle (center = straight up, edges = sharp)
- One level with at least 6 rows of bricks, mixing normal and tough (2-hit) bricks that crack visibly after first hit
- Some bricks carry power-up icons; when broken they drop, catching with paddle gives wider paddle (10s) or multi-ball (3 balls)
- Ball never passes through bricks or paddle (proper collision)
- Particles when brick breaks
- 3 lives, score displayed
- Game over and victory screens with restart
- Self-playing demo mode (AI plays) behind "Click or press a key to start" message
- Must look good at 1280x800 and adapt to 375px wide, no horizontal scrollbar
- No external resources (no CDN needed — vanilla only)
- Zero console errors
- Visible on first paint (screenshot at ~3s)

Design plan:
- Canvas centered, fixed logical size like 800x600 (or scale to fit). Use CSS to scale canvas to fit viewport while preserving aspect ratio. Logical game size 800x600. At 375px wide, canvas scales down via CSS (width: min(100vw, ...)). Use devicePixelRatio for crispness? Simpler: set canvas width/height to logical size and scale with CSS. For crispness on retina, could scale by dpr but keep logical coordinates via ctx.scale. Let's do: canvas logical 800x600; backing store = 800*dpr x 600*dpr; ctx.setTransform(dpr,0,0,dpr,0,0). CSS width computed to fit container.

- Game states: 'menu' (demo mode with AI playing + overlay "Click or press a key to start"), 'playing', 'gameover', 'victory'. In menu, an AI controls the paddle and the game auto-plays with lives refilled etc. Actually simplest: run the same game loop in 'demo' state where AI moves paddle; when user clicks/presses key, switch to 'playing' and reset the game fresh with 3 lives.

- Paddle: mouse move sets target x; keys move paddle. In demo, AI sets target.

- Ball-paddle collision: compute relative hit position (-1..1), angle = rel * maxAngle (e.g., 60° from vertical). Set velocity accordingly, ensure upward. Also add slight speed-up over time maybe. Keep ball speed constant-ish but increase slightly with brick hits.

- Ball-brick collision: swept or sub-stepped. To guarantee no tunneling, move ball in small substeps (e.g., step size <= min(ball radius, brick half sizes)). Or do proper AABB-circle collision with previous position to determine side. I'll implement: for each frame, compute movement, then check collision against each brick using expanded AABB (circle vs rect) and determine penetration side by comparing overlap on x and y using previous position. A robust approach: substep the ball movement into steps of at most ball radius (or 4px), checking collision each substep; on collision, reflect based on which side was hit (compute closest point). With substeps small enough, tunneling is impossible.

Simplest robust: per substep, for each brick alive, check circle-rect overlap; if overlap, determine collision normal by comparing ball center to rect: compute closest point on rect to circle center; if distance < radius, we have collision. Determine side: dx = ball.x - closest.x, dy = ball.y - closest.y; if |dx| > |dy| reflect vx else reflect vy. But corner cases: better to use previous position to decide: if ball was above brick (prevY + r <= brick.y) reflect vy, etc. Combine: check prev position relative to brick bounds to determine normal. That's standard and reliable with substeps.

I'll do: substeps = ceil(dist / (ball.r * 0.5)) maybe. For each substep, move ball, then for bricks, if circle intersects rect: determine side using previous substep position (before this substep move): if prevBottom <= brick.top and now overlapping → hit from top, vy = -|vy|; elif prevTop >= brick.bottom → hit from bottom; elif prevRight <= brick.left → vx = -|vx|; elif prevLeft >= brick.right → vx = |vx|; else corner — pick axis with least penetration. Then break out and handle brick hit (damage, particles, score, maybe powerup drop). Only one brick per substep (break after first collision) — fine.

- Paddle collision similarly with substeps: if ball moving down and circle intersects paddle rect → compute hit = (ball.x - paddle.x) / (paddle.w/2) clamped to [-1,1]; angle = hit * maxBounce (e.g., 65°); speed = current speed; vx = speed * sin(angle), vy = -speed * cos(angle). Ensure minimum vy magnitude. Also if ball hits paddle side, reflect vx. Use previous position check for sides.

- Bricks: 6+ rows. Let's do 8 rows for richness. Layout: cols = 10, brick width computed with margins. Rows: mix of normal (1 hit) and tough (2 hits, cracked appearance after first hit). Assign tough bricks in a pattern (e.g., rows 1-2 tough? or checkerboard). Power-ups on random ~20% of bricks, shown as icon (lightning bolt for multi-ball, wide arrows for expand — draw with canvas paths). Colors per row.

Brick types:
- normal: 1 HP, color by row.
- tough: 2 HP, drawn with metallic/darker color + rivets; after first hit, draw crack lines (precomputed crack pattern per brick, seeded random).

Cracked rendering: when hp goes 2→1, set cracked = true; draw crack polylines.

- Power-ups: when a brick with powerup is destroyed (hp reaches 0), spawn falling capsule with icon. Types: 'expand' (wider paddle 10s) and 'multiball' (3 balls total — spawn 2 extra balls with angles). Catching: capsule intersects paddle rect → apply. Capsule falls at constant speed; if falls below bottom, remove.

Multi-ball: keep array of balls. When multiball triggered, for each existing ball spawn 2 more (total cap maybe 3 balls as spec says 3 balls — "multi-ball (3 balls)" so exactly 3 balls). If already 3, maybe just refresh? Simplest: on catch, if balls.length < 3, add balls until 3 with spread angles. If already 3, give score bonus instead. Extra balls launched from paddle position upward with angles.

- Lives: 3. Ball lost (all balls below bottom): lives--, if lives > 0 reset ball on paddle (stuck until serve?) — simpler: immediately launch new ball upward from paddle; brief pause maybe. If lives == 0 → game over screen with score + restart button (click or key).

- Score: +10 per normal brick hit? Let's do: normal brick destroyed +100, tough first hit +50, tough destroyed +100? Or classic: points per brick by row. Let's do: hit points: normal destroy 50, tough hit 25 and destroy 75? Keep simple: destroying brick gives points = row-based (top rows more). Also powerup catch +25? Keep it simple and display score top.

- Particles: on brick break, spawn ~14 particles with brick color, gravity, fade. Also small particle burst on paddle hit maybe.

- Screens: overlay divs over canvas (HTML) for menu/game over/victory with restart button. Buttons clickable; also key/click anywhere restarts. Requirement: "Game over and victory screens with a restart option" — HTML overlay with button is fine and polished. Also "Click or press a key to start" message in demo mode — overlay text.

But note: iframe receives keyboard events only after user clicks. So listen on window for keydown; also canvas click. The overlay message covers that.

- Demo mode: state 'demo': AI paddle follows ball x with some lag/error; game runs; when ball lost, auto re-serve; lives effectively infinite in demo (reset lives to 3 when 0? or just respawn ball). Score accumulates. When user clicks or presses key → start real game: reset everything, state 'playing'.

Also pause? Not required. Maybe P to pause — optional, skip to keep robust. Could add but not necessary.

- HUD: score and lives drawn in canvas top area, or HTML above canvas. Draw in canvas: top bar with score left, lives right (hearts). Since bricks start below top bar. Let's design: canvas 800x600. Top HUD area height 40. Play area from y=40 to 600. Walls at x=0..8 and x=792..800? Let's have side walls with padding 8px. Actually classic breakout: ball bounces off left/right/top walls; bottom = death. Play field: x from 8 to 792 (wall thickness 8 drawn), top wall at y=40 (below HUD). Let me define: HUD height 44; field top = 44; walls: left x< WALL, right x > W-WALL, top y < TOP. WALL=8.

- Bricks: area from y = TOP + 24 = 68, rows 8, brick height 22, gap 6. cols 10: brick width = (W - 2*WALL - margin*2 - gap*(cols-1)) / cols. margin 16 each side. (800-16-32 - 9*6)/10 = (800-16-16-54)/10 = 714/10 = 71.4. Fine.

Rows colors: rainbow-ish gradient palette.

Pattern: rows 0..7 (top to bottom). Tough bricks: e.g., rows 0 and 1 fully tough? Requirement: mixing normal and tougher. Let's make rows 0-1 tough (top two rows), rows 2-5 normal with some tough mixed (checkerboard in row 3?), rows 6-7 normal. Or nicer: tough bricks scattered: every row has some tough. Let's do: row index r; tough if (r < 2) or (r===4 && c%3===1) or (r===6 && c%2===0)? Hmm keep visible mixing: top 2 rows tough (metal), then rows 2-7 normal but rows 3 and 5 have tough at alternating columns. Fine.

Power-ups: choose ~18% of bricks randomly (seeded at level build) to carry powerup, ensure at least, say, 6 bricks have them, with at least 2 of each type. I'll assign: iterate bricks, random chance; then force first N. Simpler: pick indices deterministically: every brick where (r*cols + c) % 5 === 2 gets a powerup, type alternating expand/multiball by ((r+c)%2). That guarantees plenty. But "some bricks visibly carry a power-up icon" — deterministic pattern is fine and guarantees availability. Maybe mix random with seeded RNG for variety. I'll use a seeded PRNG (mulberry32) so layout is consistent.

- Ball: white circle with glow (shadowBlur). Speed: start 5.2 px/frame at 60fps... Use delta-time based movement for smoothness: speed in px/sec, e.g., 380 px/s, increasing slightly per brick hit up to max 620. Use requestAnimationFrame with dt clamp (max 0.033). Substep: step distance = min( speed*dt / n ). n = ceil(dist / 4). Each substep move & collide.

Actually simpler and robust: compute per-frame movement; substeps = ceil(totalDist / (ball.r*0.75)); for each substep, move by totalDist/substeps, then run collision checks (bricks then paddle then walls). Walls are easy: if x < WALL + r → x = WALL + r, vx = |vx|; etc. Bottom: if y - r > H → ball lost.

- Paddle: width base 110, height 14, y = H - 34. Rounded rect with gradient. Expand powerup: width 170 for 10s; show timer bar. Draw paddle with slight glow.

- Mouse control: on mousemove over canvas (or window), compute canvas-relative x via getBoundingClientRect scaling; set paddle.x target. Paddle moves toward target instantly (mouse) or with keyboard velocity. For keyboard: left/right arrows set velocity 620 px/s. Combine: if keys pressed, keyboard takes precedence (or add). I'll track keysDown set; in update, if left/right pressed, move paddle by speed*dt and set mouseTarget = paddle.x (so mouse doesn't fight). Else paddle.x lerps to mouseTarget quickly (or set directly). Direct set is fine for mouse: paddle.x = clamp(mouseTarget).

Demo AI: target = ball.x (the lowest/nearest ball) + small offset noise; move paddle toward it at limited speed (e.g., 520 px/s) so it's beatable-ish; also add slight prediction. When no balls (shouldn't happen), center.

- Serve: ball starts stuck to paddle; in demo auto-launch after 0.8s; in playing, ball launches automatically after 1s or on click/key? Simpler: auto-launch after short delay always. But "Click or press a key to start" refers to starting the game from menu. After losing a life, auto re-serve after 0.9s with ball stuck to paddle — show "Get ready" maybe. Keep auto.

Actually let's have ball stuck to paddle at start (state ball.stuck = true), and a serve timer; when timer expires, launch upward with slight random angle. In demo mode same.

- Victory: when all bricks destroyed → state 'victory', overlay with score, best? No localStorage allowed. Just score + restart.

- Game over: overlay with score and restart.

- Restart: button click or press Enter/Space/click canvas → reset to 'playing' fresh (from gameover/victory), or from menu start playing. Implement startGame() that resets all.

- Also allow pressing any key during play? "Click or press a key to start" — any keydown in menu starts. During gameover/victory, Enter/Space/click restarts (or any key — but careful: arrow keys shouldn't restart accidentally? It's fine: any key restarts). Hmm, during 'playing', keydown shouldn't do anything except arrows. I'll check state first.

- Rendering polish: background with subtle gradient + grid or starfield; bricks with rounded corners, gradient, highlight; tough bricks metallic gray-blue with rivets, crack lines when damaged; powerup icons drawn inside bricks (small glyph): expand = horizontal double arrow; multiball = three dots / or "×3"? Draw icons with paths: expand: `<->` arrows; multi: small triangle of 3 circles. Capsules falling: rounded rect with icon, glow, slight bobbing.

- HUD: Score left "SCORE 001250", lives as hearts right, plus active powerup indicator (expand timer bar). Draw text with canvas font (system fonts: 'Segoe UI', Arial). Fine.

- Also show level name? "LEVEL 1" center top maybe.

- Canvas scaling: container centers canvas; canvas CSS size computed in JS on resize: scale = min((vw - 24)/800, (vh - 24)/600, maybe max 1.15?) Actually allow upscale a bit on big screens? At 1280x800, 800x600 fits fine at scale 1 (with margins). Let scale = min(vw/840, vh/640) capped at, say, 1.4 so it doesn't get huge? At 1280x800: min(1280/840=1.52, 800/640=1.25) → 1.25, capped 1.4 → 1.25, canvas CSS 1000x750. That fits in 800 height? 750 + margins ok. Hmm 800 viewport height, 750 canvas + padding fine. But maybe cap at 1.15 to be safe: 920x690. Let's cap scale at 1.25. Also ensure at 375px wide: scale = 375/840 = 0.446 → canvas ~357x268. Playable. No horizontal scrollbar: body overflow hidden? Requirement says no horizontal scrollbar; set body margin 0, overflow-x hidden, and canvas max-width 100%. I'll compute in JS and set style width/height; also CSS fallback width:100%; height:auto; aspect-ratio. Use ResizeObserver/window resize.

Simplest: CSS: #game { width: min(96vw, calc((100vh - 48px) * (800/600)), 1100px); aspect-ratio: 800/600; height: auto; } Then canvas fills container (width:100%; height:100%). Backing store fixed at 800*dpr. That avoids JS resize. max-width 1100px so on very wide screens it doesn't get gigantic. At 1280x800: (100vh-48)=752 * 1.333 = 1002px; min(96vw=1228, 1002, 1100) → 1002px wide, height 752. Good. At 375x667: min(360, (667-48)*1.333=825, 1100) → 360 wide, 270 high. 

But mouse coordinates: use rect = canvas.getBoundingClientRect(); x = (e.clientX - rect.left) * (800 / rect.width). Works.

- DPR: set canvas.width = 800 * dpr etc., ctx.setTransform(dpr,0,0,dpr,0,0). Cap dpr at 2 for perf.

- Fonts: use system font stack; no external fonts.

- Zero console errors: careful with code.

Let me now write the code carefully.

Structure:

```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> ... </style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="800" height="600"></canvas>
  <div id="overlay" class="show"> ... dynamic content ... </div>
</div>
<script> ... </script>
</body>
</html>
```

Overlay: absolutely positioned over canvas, semi-transparent, with title, message, button. Content set by JS per state. pointer-events: none except button? For "click anywhere to start", listen on window click. Button click also triggers window click (bubbles) — careful not to double-start; startGame idempotent via state check.

Let me define states: 'demo', 'playing', 'over', 'win'. Overlay visible for 'demo' (with "Click or press any key to start" + demo hint), hidden during 'playing', visible for 'over'/'win' with restart button.

Actually maybe show a small persistent hint during play? Not needed.

Game constants:

```js
const W = 800, H = 600;
const HUD = 46;
const WALL = 10;
const TOP = HUD; // top wall at y=HUD
const FIELD_L = WALL, FIELD_R = W - WALL;
```

Bricks:

```js
const ROWS = 8, COLS = 10;
const BRICK_TOP = TOP + 26;
const BRICK_H = 22, GAP = 6;
const BRICK_MARGIN = 18;
const brickW = (W - WALL*2 - BRICK_MARGIN*2 - GAP*(COLS-1)) / COLS;
```

Compute: W - 20 - 36 - 54 = 690; /10 = 69. Good.

Brick object: {x, y, w, h, hp, maxHp, type: 'normal'|'tough', color, cracked, power: null|'expand'|'multiball', alive: true, seed}

Build level:

```js
function buildLevel(){
  bricks = [];
  const rng = mulberry32(1337);
  const palette = ['#ff5d5d','#ff9f43','#ffd32a','#2ed573','#1dd1a1','#54a0ff','#5f6df0','#c56cf0']; // per row
  for (let r=0;r<ROWS;r++){
    for(let c=0;c<COLS;c++){
      const x = WALL + BRICK_MARGIN + c*(brickW+GAP);
      const y = BRICK_TOP + r*(BRICK_H+GAP);
      let type = 'normal';
      if (r < 2 || (r===4 && c%3===1) || (r===6 && c%2===1)) type='tough';
      let power = null;
      const roll = rng();
      if (roll < 0.16) power = (rng() < 0.5 ? 'expand' : 'multiball');
      // guarantee at least a few of each:
      ...
      bricks.push({...});
    }
  }
  // ensure at least 3 expand and 3 multiball:
  count and assign to random normal bricks if lacking.
}
```

Tough bricks with powerup: when first hit, brick stays (cracked) — powerup drops only when destroyed. Good per spec ("when broken they drop it").

Colors: normal bricks row color; tough: steel gradient (#9aa7b8 → #6b7686) with darker border; cracked draws dark jagged lines.

Crack drawing: per brick generate crack segments from rng at build: 3 polylines from center-ish outward. Draw when hp < maxHp.

Power-up icon inside brick: draw small white glyph centered:
- expand: double-headed horizontal arrow: line with arrowheads both ends.
- multiball: three small circles arranged triangle, or a "2→3"? I'll draw 3 dots: one center-left... Actually multiball icon: draw one circle splitting: simpler draw three circles in triangle formation.

Also tint brick slightly? Add a subtle white icon with glow.

Balls:

```js
ball = {x, y, r: 7, vx, vy, speed, stuck: true}
```

speed base 400 px/s; on each brick destroyed speed += 6 up to 640; normalize velocity to speed.

Paddle:

```js
paddle = {x: W/2, y: H-36, w: 110, h: 14, baseW: 110, expandTimer: 0, speed: 640}
```

Expand: when caught, expandTimer = 10; paddle.w = 168 while timer>0 else 110. Smooth width lerp.

Powerups (falling):

```js
{ x, y, vy: 130, type, w: 34, h: 16, rot/bob }
```

Catch: if powerup.y + h/2 >= paddle.y and powerup.y - h/2 <= paddle.y + paddle.h and |x - paddle.x| <= paddle.w/2 + w/2 → apply.

Apply expand: expandTimer = 10. Apply multiball: spawn extra balls: while balls.length < 3: create ball at paddle with angle spread (-40°, 0°, 40° from up?) Actually if 1 ball exists, add 2 more at angles ±35° with same speed. If balls.length >= 3 already, award +150 score instead.

Particles:

```js
{ x, y, vx, vy, life, maxLife, color, size }
```

Update: vy += gravity*dt (e.g., 500), pos += v*dt, life -= dt. Draw as squares/circles with alpha = life/maxLife.

Also brick-hit spark particles (small) when tough brick hit but not destroyed.

Floating score text? Could add "+100" floaters — nice polish. Add floaters array {x,y,text,life}. Sure, small.

Collision details:

Ball update per substep:

```js
function moveBall(b, dt){
  const dist = b.speed * dt; // per substep dt already divided
  ...
}
```

Let me structure update:

```js
function update(dt){
  // paddle movement (input or AI)
  // expand timer
  // powerups fall & catch
  // balls: substeps
  for each ball:
    if stuck: position on paddle; if serveTimer<=0 launch
    else:
      const steps = Math.max(1, Math.ceil((b.speed*dt) / (b.r*0.9)));
      const sdt = dt/steps;
      for (s=0; s<steps; s++){
        b.x += b.vx*sdt; b.y += b.vy*sdt;
        collideWalls(b);
        collideBricks(b);
        collidePaddle(b);
        if (b.dead) break;
      }
    remove if y - r > H
}
```

collideWalls: 
```js
if (b.x < FIELD_L + b.r){ b.x = FIELD_L + b.r; b.vx = Math.abs(b.vx); }
if (b.x > FIELD_R - b.r){ b.x = FIELD_R - b.r; b.vx = -Math.abs(b.vx); }
if (b.y < TOP + b.r){ b.y = TOP + b.r; b.vy = Math.abs(b.vy); }
```
Bottom handled outside (loss).

collideBricks(b): loop bricks alive; circle-rect overlap test:

```js
const closestX = clamp(b.x, br.x, br.x+br.w);
const closestY = clamp(b.y, br.y, br.y+br.h);
const dx = b.x - closestX, dy = b.y - closestY;
if (dx*dx + dy*dy <= b.r*b.r) { hit }
```

But need previous position to determine normal. Keep px, py before substep move. On hit:

```js
// determine side from previous position
let nx=0, ny=0;
const prevBottom = py + b.r, prevTop = py - b.r, prevRight = px + b.r, prevLeft = px - b.r;
if (prevBottom <= br.y + 0.001 && b.vy > 0) { ny = -1; }
else if (prevTop >= br.y + br.h - 0.001 && b.vy < 0) { ny = 1; }
else if (prevRight <= br.x + 0.001 && b.vx > 0) { nx = -1; }
else if (prevLeft >= br.x + br.w - 0.001 && b.vx < 0) { nx = 1; }
else {
  // corner/overlap: choose axis of least penetration
  const overlapX = ... 
}
```

Simpler robust fallback: compute overlap on each axis from current position:
```js
const overlapLeft = (b.x + b.r) - br.x;
const overlapRight = (br.x + br.w) - (b.x - b.r);
const overlapTop = (b.y + b.r) - br.y;
const overlapBottom = (br.y + br.h) - (b.y - b.r);
const minX = Math.min(overlapLeft, overlapRight);
const minY = Math.min(overlapTop, overlapBottom);
if (minX < minY) nx = (overlapLeft < overlapRight) ? -1 : 1; else ny = (overlapTop < overlapBottom) ? -1 : 1;
```

Combine: prefer previous-position test; if none matched (deep overlap), use least-penetration. Then reflect: if nx: b.vx = nx * Math.abs(b.vx)... careful: if nx = -1 means ball hit left side of brick moving right → vx should become negative: b.vx = -Math.abs(b.vx) when nx=-1? Let's define nx as direction to push ball out: if ball hit brick's left side, ball should move left: vx = -|vx|. So b.vx = (nx<0) ? -Math.abs(b.vx) : Math.abs(b.vx) where nx=-1 for left side. In my code above, prevRight <= br.x means ball was left of brick → hit left side → nx = -1 → vx = -|vx|. Yes: `if (nx !== 0) b.vx = (nx < 0 ? -1 : 1) * Math.abs(b.vx); else b.vy = (ny < 0 ? -1 : 1) * Math.abs(b.vy);` with ny=-1 for top side (ball above moving down → bounce up). Check: prevBottom <= br.y (ball above brick) and vy>0 (moving down) → hit top → ny=-1 → vy = -|vy| (up). Correct.

Also reposition ball out of brick: b.x += nx * (penetration) — optional; with substeps small, setting position to boundary is good: e.g., if ny=-1: b.y = br.y - b.r; if ny=1: b.y = br.y+br.h+b.r; etc. Do that.

Then damage brick:
```js
function hitBrick(br, b){
  br.hp--;
  if (br.hp <= 0){
    br.alive = false;
    score += br.type==='tough' ? 150 : (50 + br.row*10);
    spawnParticles(br);
    if (br.power) dropPowerup(br);
    speedUp();
  } else {
    br.cracked = true;
    score += 25;
    small spark particles;
  }
  floater text;
}
```

Only one brick per substep: after handling a brick hit, break out of brick loop (return true).

collidePaddle(b): only if b.vy > 0 (moving down):
```js
if (b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h && b.x >= paddle.x - paddle.w/2 - b.r && b.x <= paddle.x + paddle.w/2 + b.r){
  // side hit check using prev pos: if prevBottom <= paddle.y → top hit
  if (py + b.r <= paddle.y + 0.001){
    const rel = clamp((b.x - paddle.x) / (paddle.w/2), -1, 1);
    const maxAngle = Math.PI * 0.36; // ~65°
    const ang = rel * maxAngle;
    b.vx = b.speed * Math.sin(ang);
    b.vy = -b.speed * Math.cos(ang);
    b.y = paddle.y - b.r;
  } else {
    // hit side: reflect vx, push out
    if (px + b.r <= paddle.x - paddle.w/2) { b.x = paddle.x - paddle.w/2 - b.r; b.vx = -Math.abs(b.vx); }
    else if (px - b.r >= paddle.x + paddle.w/2) { b.x = paddle.x + paddle.w/2 + b.r; b.vx = Math.abs(b.vx); }
    else { b.y = paddle.y - b.r; b.vy = -Math.abs(b.vy); }
  }
}
```

Edge case: ball moving down but rel computed — with min cos(angle) at 65°, cos=0.42 → vy ≈ -0.42*speed, still upward. Good; center → straight up (vx=0). 

Also ensure ball never gets stuck moving horizontally: if |vy| < 0.15*speed after paddle bounce... cos max angle ensures min. For brick bounces vy could become tiny if ball moving mostly horizontally hits top/bottom? Reflection preserves magnitude of component; if vy was tiny before, remains tiny — could cause infinite horizontal loop between side walls and bricks. Add safeguard: after any reflection, if |vy| < speed*0.18, adjust: set vy = ±speed*0.18 and renormalize vx to keep speed. Implement helper `ensureMinVy(b)` called after brick/paddle collisions:

```js
function ensureMinVy(b){
  const minVy = b.speed * 0.22;
  if (Math.abs(b.vy) < minVy){
    b.vy = (b.vy >= 0 ? 1 : -1) * minVy;
    const s = Math.hypot(b.vx, b.vy);
    b.vx = b.vx / s * b.speed; // vy already at min... 
  }
}
```

Hmm, careful: after setting vy, renormalize whole vector to speed: 
```js
const mag = Math.hypot(b.vx, b.vy); if (mag>0){ b.vx = b.vx/mag*b.speed; b.vy = b.vy/mag*b.speed; }
```
But that could reduce |vy| below min again if vx large. Compute: set sign, vy = minVy, then vx = sqrt(speed² - minVy²) with sign of current vx (or keep). 

```js
function fixAngle(b){
  const minVy = b.speed*0.22;
  if (Math.abs(b.vy) < minVy){
    const dir = b.vy < 0 ? -1 : 1;
    const vxMag = Math.sqrt(Math.max(b.speed*b.speed - minVy*minVy, 0));
    b.vy = dir*minVy;
    b.vx = (b.vx < 0 ? -1 : 1) * vxMag;
  }
}
```

Good.

Also paddle moving into ball: if paddle moves up into ball? Paddle y fixed. If ball below paddle top and moving up... The check `b.vy > 0` for paddle top hit; side hits handled. Also if ball is beside paddle and paddle moves horizontally into ball — could push ball through? Ball beside paddle at same y, paddle moves into it: overlap detected with prevBottom > paddle.y → side branch: prevRight <= paddleLeft? Not necessarily. The else branch: reflect vy... hmm. Let's handle: if not top-hit (i.e., prevBottom > paddle.y), treat as side: determine side by comparing px to paddle center: if px < paddle.x → left side → vx = -|vx|, x = paddle.x - w/2 - r; else right side. That covers paddle moving into ball. Good, simpler:

```js
function collidePaddle(b, px, py){
  const half = paddle.w/2;
  if (b.vy > 0 && b.y + b.r >= paddle.y && b.y - b.r <= paddle.y + paddle.h && Math.abs(b.x - paddle.x) <= half + b.r){
    if (py + b.r <= paddle.y + 0.01){ ...top bounce... }
    else {
      const side = (px < paddle.x) ? -1 : 1;
      b.vx = side * Math.abs(b.vx); // hmm if ball moving left and hits left side... 
      b.x = paddle.x + side*(half + b.r);
    }
  }
}
```

Wait if px < paddle.x (ball left of center) and overlapping side, ball should bounce left: vx = -|vx|. side=-1 → vx = -|vx|. But what if ball was moving right and got overlapped from top region... covered by top branch. If ball moving left and paddle moves right into ball from left side: px < paddle.x, side=-1, vx = -|vx| — ball moving left already, stays left, fine. OK.

Also allow "english": add slight vx based on paddle velocity when top hit? Spec says angle depends on hit position only. Keep position-based; maybe add tiny paddle-velocity influence (clamped) for feel — spec says depends on where ball hits; adding paddle velocity might violate "center = straight up". Keep pure position-based to match spec exactly.

Ball stuck serve: ball.stuck = true; each frame ball.x = paddle.x, ball.y = paddle.y - r - 1; serveTimer -= dt; when <= 0: launch: angle = (rng-0.5)*0.6 rad; vx = speed*sin, vy = -speed*cos. In demo also auto.

Lives handling: when balls array becomes empty (all lost): lives--; if lives <= 0 → state 'over', show overlay; else respawn: create stuck ball, serveTimer = 1.0. In demo: lives = 3 always reset (don't game over); just respawn ball.

Win check: bricks.every(!alive) → state 'win'.

Score display: pad? Just number. Also maybe show "best" — can't persist. Skip.

HUD draw: top bar: rect from 0,0 to W,HUD with dark gradient; text "SCORE 0001234" left at x=16; center "NEON BREAKOUT" small? maybe level; right hearts: draw heart shapes or just "♥♥♥" text — use path hearts for polish. Draw 3 hearts, filled for remaining lives, dim for lost. Also expand timer: if expandTimer>0, draw small bar under paddle or in HUD: "WIDE PADDLE 7.3s" with bar. Put in HUD center-right.

Also draw walls: left/right/top wall strips with neon gradient. Background: dark gradient + subtle grid lines + vignette. Maybe animated starfield particles in background for polish (slow drifting dots) — cheap. Add ~40 stars drifting down slowly, alpha twinkle. Nice.

Rendering bricks: rounded rect (use roundRect if available — Chrome supports ctx.roundRect; but to be safe implement helper rr(ctx,x,y,w,h,r) path manually). Fill vertical gradient (create per brick per frame? gradients each frame for 80 bricks is okay but could cache. 80 gradients/frame fine on modern machines, but let's cache gradient per brick at build time since colors static — but canvas gradient coordinates are in user space, static, so cache works. However with DPR transform, gradient defined in logical coords works fine. Cache: br.grad = ctx.createLinearGradient(0, br.y, 0, br.y+br.h). Do at build. Tough bricks: metallic gradient + inner bevel + rivets; cracked: overlay crack lines (precomputed points relative to brick).

Brick draw:
```js
function drawBrick(br){
  ctx.save();
  rr(br.x, br.y, br.w, br.h, 5);
  if (br.type==='tough'){
    ctx.fillStyle = br.cracked ? gradToughCracked : gradTough; // or same + cracks
    fill; stroke darker;
    // rivets at corners
    // cracks if cracked
  } else {
    ctx.fillStyle = br.grad; fill;
    // top highlight: lighter strip
    ctx.fillStyle='rgba(255,255,255,0.25)'; rr(br.x+2, br.y+2, br.w-4, 5, 3) fill;
    stroke rgba(0,0,0,0.25)
  }
  if (br.power) drawPowerIcon(br);
  ctx.restore();
}
```

Power icon in brick: white glyph with shadowBlur glow:
- expand: arrows: line from x1 to x2 with arrowheads: draw path.
- multi: three circles.

Let me write drawIcon(type, cx, cy, s, color):
```js
function drawIcon(type, cx, cy, s, color){
  ctx.save();
  ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 2; ctx.lineCap='round';
  if (type==='expand'){
    // horizontal double arrow
    ctx.beginPath();
    ctx.moveTo(cx - s, cy); ctx.lineTo(cx + s, cy);
    ctx.moveTo(cx - s, cy); ctx.lineTo(cx - s + s*0.6, cy - s*0.6);
    ctx.moveTo(cx - s, cy); ctx.lineTo(cx - s + s*0.6, cy + s*0.6);
    ctx.moveTo(cx + s, cy); ctx.lineTo(cx + s - s*0.6, cy - s*0.6);
    ctx.moveTo(cx + s, cy); ctx.lineTo(cx + s - s*0.6, cy + s*0.6);
    ctx.stroke();
  } else {
    // three balls triangle
    const r = s*0.32;
    circle(cx, cy - s*0.45, r); circle(cx - s*0.5, cy + s*0.4, r); circle(cx + s*0.5, cy + s*0.4, r); fill;
  }
  ctx.restore();
}
```

Falling capsule: rounded rect 36x18, colored: expand = cyan/green (#2ed573), multi = magenta/purple (#c56cf0) or gold. Icon white inside. Glow shadowBlur 12. Bob: slight rotation? Keep simple: y falls, x maybe slight sine drift.

Particles on break: 16 particles, velocities random directions biased upward, colors from brick (use row color + white), gravity 600, life 0.5-0.9s, size 2-4, draw as filled rects rotated? Just circles.

Paddle hit particles: 4 tiny white sparks. Wall hit: tiny spark maybe. Keep minimal for perf.

Floaters: "+150" rising text fading.

Ball draw: radial gradient white core, glow shadowBlur 16 shadowColor white; trail: store last N positions, draw fading circles — nice polish, cheap. Keep trail array per ball, push each frame, cap 10.

Paddle draw: rounded rect gradient (cyan → blue), glow shadowBlur 14 shadowColor '#4dd0ff'; when expanded, color shifts green. Slight highlight line on top.

Overlay HTML/CSS: 

```html
<div id="overlay">
  <div class="panel">
    <h1 id="ovTitle">NEON BREAKOUT</h1>
    <p id="ovMsg">...</p>
    <button id="ovBtn">Start Game</button>
    <p class="hint" id="ovHint">Click or press any key to start</p>
  </div>
</div>
```

CSS: overlay absolute inset 0 within #wrap (wrap position relative, sized to canvas). display flex center; background rgba(4,8,20,0.55) + backdrop blur maybe. Panel: border 1px rgba(120,200,255,.35), border-radius 14px, padding, background rgba(10,16,34,0.8), box-shadow glow, text center. h1 with gradient text + letter-spacing. Button: neon style, hover.

For demo state: title "NEON BREAKOUT", msg "Demo — AI is playing", hint "Click or press any key to start", button "▶ Start". For over: "GAME OVER", msg "Score: X", button "↻ Restart". For win: "YOU WIN!", msg, button "Play again".

Show/hide via class.

Input handling:

```js
window.addEventListener('keydown', e => {
  if (['ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
  keys.add(e.key);
  if (state==='demo' || state==='over' || state==='win'){ startGame(); }
});
window.addEventListener('keyup', e => keys.delete(e.key));
canvas/window mousemove: update mouseX.
window click / pointerdown: if state!=='playing' → startGame(); (also if playing, maybe serve early? not needed)
```

Wait: during 'playing', clicking shouldn't restart. Only demo/over/win. But also requirement: "The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear message" — we display "Click or press any key to start". Good. Our game in demo doesn't need keyboard; paddle AI plays. After click, keyboard works (since iframe focused after click). Also keydown on window works after click. Good.

Mouse: listen mousemove on window; compute relative to canvas rect. Also touchmove for mobile (375px) — add touch support: touchmove → mouseX; prevent default to avoid scroll. Add pointermove instead — pointer events cover mouse+touch. Use `window.addEventListener('pointermove', ...)` and `pointerdown` for click. pointerdown also for start. Set touch-action: none on canvas/wrap to prevent scrolling.

Keyboard movement: in update, if keys has ArrowLeft → paddle.x -= paddleSpeed*dt; ArrowRight += . Also 'a'/'d'? Add for good measure. Then clamp. And set mouseX target = paddle.x so when releasing keys mouse doesn't yank back. Actually simpler: paddle target = mouseX always; keyboard adds delta to mouseX: mouseX += dir*speed*dt, then paddle.x = clamp(mouseX). That unifies: paddle.x follows mouseX; keyboard modifies mouseX. AI modifies mouseX toward ball. 

paddle.x = clamp(mouseX, FIELD_L + w/2 + 2, FIELD_R - w/2 - 2). Smooth: paddle.x += (target - paddle.x) * min(1, dt*30) for slight smoothing? For mouse direct is more responsive; use lerp factor dt*40 (fast). For AI also fine. Let's do direct set for responsiveness but AI needs limited speed: AI sets mouseX = approach(ball.x, maxSpeed*dt). Then paddle.x = lerp toward mouseX quickly. Hmm, if paddle lerps toward mouseX and AI moves mouseX at limited speed, paddle speed limited accordingly — good. For mouse, mouseX jumps instantly, lerp dt*40 ≈ nearly instant. OK: `paddle.x += (clampTarget - paddle.x) * Math.min(1, dt*40)`.

Clamp target accounting paddle width: target clamped to [FIELD_L + w/2, FIELD_R - w/2].

Demo AI: pick ball with largest y (closest to bottom) among non-stuck; else center. desired = ball.x + sin(time*3)*8 (wobble); move mouseX toward desired by maxAiSpeed*dt (maxAiSpeed ~ 470 < ball speeds sometimes, creating misses). Also when ball stuck, center under ball.

Lives in demo: when balls empty, just respawn ball (lives stays 3). Score accumulates in demo; when real game starts, reset score.

Also demo should maybe not drop below... fine.

Restart from over/win: startGame() resets score=0, lives=3, buildLevel, etc., state='playing', overlay hidden.

Also add pause when tab hidden? rAF handles dt clamp; fine.

Now, "Something meaningful must be visible on first paint" — demo runs immediately with bricks, ball bouncing, particles — good. Overlay panel semi-transparent so game visible behind; maybe make demo overlay a slim banner at top instead of full panel so game more visible? The requirement: self-playing demo behind a "Click or press a key to start" message. A bottom banner might be nicer: overlay panel positioned at bottom? Let's make demo overlay a compact centered panel but with lower opacity background (rgba(5,10,25,0.35)) so game visible; or position panel at top. I'll do: demo → small pill at top center: "▶ DEMO — click or press any key to start" plus title. Implementation: overlay container with .banner class for demo (top-centered pill), .panel for over/win (centered modal). Let's implement two elements or toggle classes. Simpler: one overlay div; JS sets innerHTML/class. I'll have:

```html
<div id="overlay" class="hidden">
  <div id="ovBox"></div>
</div>
```

JS builds content per state:
- demo: `<div class="pill"><span class="live-dot"></span> DEMO MODE — AI playing <b>·</b> click or press any key to start</div>` positioned top.
- over: panel with h1 GAME OVER, score, button.
- win: panel YOU WIN.

CSS classes: #overlay.demo {align-items:flex-start; padding-top:14px; background:transparent; pointer-events:none} etc.

Button needs pointer-events auto. Since we also start on any window pointerdown, button click will bubble → startGame called once (guard: if state changes, subsequent calls no-op). Use `if(state!=='playing') startGame()` in handler — after first call state==='playing', second call no-op. But during 'playing', pointerdown does nothing. Good. Also keydown any key: same guard. But careful: keydown during 'playing' with ArrowLeft shouldn't restart — guard handles.

Edge: user clicks during demo → startGame resets → overlay hidden. Good.

Now write update loop:

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

update(dt):
```js
function update(dt){
  // input → mouseX
  if (state==='demo'){ aiControl(dt); }
  else {
    let dir = 0;
    if (keys.has('ArrowLeft')||keys.has('a')||keys.has('A')) dir -= 1;
    if (keys.has('ArrowRight')||keys.has('d')||keys.has('D')) dir += 1;
    if (dir) mouseX += dir * 720 * dt;
  }
  // paddle width (expand)
  if (paddle.expandTimer > 0){ paddle.expandTimer -= dt; if (paddle.expandTimer<=0) paddle.expandTimer = 0; }
  const targetW = paddle.expandTimer>0 ? 170 : 110;
  paddle.w += (targetW - paddle.w) * Math.min(1, dt*10);
  // move paddle toward mouseX
  const minX = FIELD_L + paddle.w/2 + 1, maxX = FIELD_R - paddle.w/2 - 1;
  mouseX = clamp(mouseX, minX, maxX);
  paddle.x += (mouseX - paddle.x) * Math.min(1, dt*40);
  paddle.x = clamp(paddle.x, minX, maxX);
  
  // powerups
  for (let i=powerups.length-1;i>=0;i--){ p=powerups[i]; p.y += p.vy*dt; p.bob += dt*6;
    if (catch test){ applyPower(p); powerups.splice(i,1); continue; }
    if (p.y - p.h > H) powerups.splice(i,1);
  }
  
  // serve timers & balls
  serveTimer -= dt; // global? per stuck ball
  for balls: if stuck → follow paddle; if serveTimer<=0 launch. Use per-ball stuck with global serveTimer only for the initial? Simpler: each stuck ball has own timer. When respawning after life loss, create stuck ball with timer 1.1.
  
  for (let i=balls.length-1;i>=0;i--){ b=balls[i];
    if (b.stuck){ b.x = paddle.x; b.y = paddle.y - b.r - 0.5; b.timer -= dt; if (b.timer<=0){ launch(b); } continue; }
    substep collisions...
    if (b.y - b.r > H){ balls.splice(i,1); }
  }
  if (balls.length===0){
    if (state==='demo'){ respawnBall(); }
    else { lives--; if (lives<=0){ state='over'; showOverlay('over'); } else respawnBall(); }
  }
  // win
  if (state==='playing' && bricks.every(b=>!b.alive)){ state='win'; showOverlay('win'); }
  // particles, floaters update
}
```

Wait win check should also apply in demo? If demo clears all bricks... unlikely but possible; then bricks all dead and no more bricks — demo would have nothing to hit; ball just bounces. Handle: in demo, if all bricks dead → rebuild level (endless demo). In playing → win. Let's do: if all dead: if state==='demo' → buildLevel() (continue) else state='win'.

Also score/lives in demo: keep showing.

respawnBall(): balls = [{stuck ball at paddle, timer 1.0, speed = baseSpeed}]; Also reset expand? Keep expand timer running (fine) or reset — reset to 0 for fairness on new life. I'll reset expandTimer=0 on life loss.

launch(b): angle = (Math.random()-0.5)*0.5; b.vx = b.speed*Math.sin(angle); b.vy = -b.speed*Math.cos(angle); b.stuck=false.

speed management: base speed 400; on brick destroy: all balls speed = min(speed+8, 660)? Store global ballSpeed; each ball has speed property = ballSpeed. On destroy: ballSpeed = min(640, ballSpeed + 6); then normalize each ball velocity to ballSpeed. Also when multiball spawns new balls use ballSpeed.

Normalize helper: setSpeed(b, s){ const m=Math.hypot(b.vx,b.vy)||1; b.vx=b.vx/m*s; b.vy=b.vy/m*s; b.speed=s; }

collideBricks returns after first hit. Also add tiny random angle jitter? No — deterministic reflection is fine and spec-compliant.

Brick hit scoring: row-based: points = (ROWS - r) * 10 + 40? Top row (r=0) = 80+... let's do normal: 60 + (ROWS-1-r)*10 → bottom row 60, top 130. Tough destroy: +150; tough first hit (cracked): +30. Keep simple: normal destroy 100, tough crack 25, tough destroy 150? Row factor adds depth. I'll do: destroy: (type==='tough'?150:100) + (ROWS-1-r)*10; crack: 25.

Floater text at brick center: "+110".

Powerup catch: expand → paddle.expandTimer = 10; floater "WIDE PADDLE!" at paddle; multiball → if balls.length < 3: add (3 - balls.length) balls at paddle.x with angles: existing balls keep; new balls: angles [-0.6, 0.6] rad from vertical plus maybe [0]? If 1 ball → add 2: angles ±0.55. If 2 balls → add 1: angle 0? or random ±. Implement: while balls.length<3: create ball at (paddle.x, paddle.y - r), speed=ballSpeed, angle = spread based on index: use angles [-0.55, 0, 0.55] pick unused... simpler: new balls launched immediately (not stuck) with angles from set [-0.6, 0.6] then [0] if needed. If balls.length>=3: score += 150, floater "MAX BALLS +150".

Capsule catch test: 
```js
if (p.y + p.h/2 >= paddle.y && p.y - p.h/2 <= paddle.y + paddle.h && Math.abs(p.x - paddle.x) <= paddle.w/2 + p.w/2)
```

dropPowerup(br): powerups.push({x: br.x+br.w/2, y: br.y+br.h/2, vy: 140, w: 38, h: 18, type: br.power, bob: 0}).

Particles spawn:
```js
function burst(x,y,color,n=16,power=1){
  for(i<n){ a=Math.random()*Math.PI*2; sp=(120+Math.random()*260)*power; particles.push({x,y,vx:cos(a)*sp, vy:sin(a)*sp - 80, life:0.5+Math.random()*0.45, max:..., size:1.5+Math.random()*2.5, color: Math.random()<0.3?'#ffffff':color}); }
}
```

Update particles: vy += 700*dt; x+=vx*dt; y+=vy*dt; life-=dt; remove when <=0. Draw: alpha=life/max; fillRect or circle.

Also bounce off floor for particles? They can fall past bottom; fine (clip by canvas).

Stars background: 50 stars {x,y,r,vy,tw}; update in render or update: y += vy*dt; wrap. Draw alpha 0.2+0.3*sin(time*tw+phase).

Render:

```js
function render(){
  ctx.clearRect? fill background gradient (cached).
  draw stars;
  draw walls (left, right, top strips) with neon gradient + glow line at inner edge;
  draw HUD (bar, score, hearts, expand timer);
  draw bricks;
  draw powerups;
  draw particles;
  draw paddle (with glow); 
  draw balls (trail + core);
  draw floaters;
  if state==='demo' maybe dim? no.
}
```

Order: background, stars, walls, bricks, powerups, particles, paddle, balls, floaters, HUD last (HUD on top). HUD bar drawn first actually so bricks don't overlap it — bricks start below TOP anyway. Draw HUD after background before bricks? HUD is at top region, no overlap. Draw HUD last is fine.

Walls: left wall rect x 0..WALL, y 0..H? Top wall y 0..TOP covers HUD area? HUD bar is separate (y 0..HUD). Let's make HUD bar y 0..HUD with its own bg, and play walls: left x0..WALL from y=TOP..H; right similar; top wall y TOP..TOP+WALL? Hmm simpler: top wall at y = TOP - WALL? Let's define play area: x in [WALL, W-WALL], y in [TOP+WALL? ...]. Let me just say: ball bounds: left = WALL, right = W-WALL, top = TOP (ball bounces at y < TOP + r? but HUD occupies 0..TOP). Make top wall visual: strip from y=TOP-? Actually put top wall strip at y in [TOP, TOP+8]? Then bricks start below TOP+8+gap. Alternatively HUD height 46, and wall top at y=46 with strip drawn 46..54, bricks from 70. Ball top bound = 54? Let's define:

- HUD_H = 46 (0..46)
- WALL = 10; top wall occupies y 46..56 (strip), ball top bound TOP_B = 56.
- side walls x 0..10 and 790..800 full height below 46? Draw from y=46 to H.
- FIELD_L = 10, FIELD_R = 790, FIELD_TOP = 56.
- BRICK_TOP = 56 + 18 = 74.

Brick area: 8 rows * (22+6) = 224 - 6 = 218 → ends at 74+218=292. Mid-field. Fine, plenty of room.

Bottom: paddle y = H - 40 = 560. Ball lost when y - r > H (600).

HUD: score at left x=18, baseline center y=23+? font 16px bold. Lives hearts right: at x from W-18 backward, heart size 12, spacing 22. Expand indicator: if expandTimer>0 draw text "WIDE × 7.2s" center with small bar. Also maybe show ball speed? no.

Hearts drawing: path heart:
```js
function heart(x,y,s,color){ ctx.save(); ctx.translate(x,y); ctx.scale(s/16,s/16); ctx.beginPath(); ctx.moveTo(0,5); ctx.bezierCurveTo(-8,-3,-16,3,0,14); ... }
```
Standard heart path: 
`M0,4 C0,1 -3,-2 -6,-2 C-9,-2 -11,0 -11,3 C-11,7 -6,10 0,15 C6,10 11,7 11,3 C11,0 9,-2 6,-2 C3,-2 0,1 0,4 Z` roughly. I'll use a known simple heart:

```js
ctx.beginPath();
ctx.moveTo(x, y + s*0.3);
ctx.bezierCurveTo(x, y, x - s*0.5, y, x - s*0.5, y + s*0.3);
ctx.bezierCurveTo(x - s*0.5, y + s*0.55, x, y + s*0.75, x, y + s);
ctx.bezierCurveTo(x, y + s*0.75, x + s*0.5, y + s*0.55, x + s*0.5, y + s*0.3);
ctx.bezierCurveTo(x + s*0.5, y, x, y, x, y + s*0.3);
```
Hmm that's a rough heart. Let me use a cleaner parametric heart:

```js
function drawHeart(cx, cy, s, fill){
  ctx.beginPath();
  ctx.moveTo(cx, cy + s*0.35);
  ctx.bezierCurveTo(cx + s*0.6, cy - s*0.15, cx + s*0.35, cy - s*0.6, cx, cy - s*0.25);
  ctx.bezierCurveTo(cx - s*0.35, cy - s*0.6, cx - s*0.6, cy - s*0.15, cx, cy + s*0.35);
  ctx.closePath();
  if (fill){ ctx.fillStyle = '#ff4d6d'; ctx.fill(); } else { ctx.strokeStyle='rgba(255,77,109,0.35)'; ctx.lineWidth=1.5; ctx.stroke(); }
}
```

Hmm heart shape: top dip at center. The above: start bottom point (cx, cy+0.35s), curve right up to top-right lobe, to center dip (cx, cy-0.25s), then mirror. That approximates a heart. Good enough at 12px.

Alternatively use text '♥' with fillText — simpler and reliable: ctx.font='18px system-ui'; fillText('❤', x, y) colored. Emoji rendering varies but fine. I'll draw path hearts to be safe (no emoji font dependency). Actually '♥' (U+2665) is widely supported in text fonts. But color control fine. I'll use path hearts — deterministic.

Score text: `SCORE ${score}` and maybe `BEST` skip.

Title in HUD center: "NEON BREAKOUT" tiny letterspaced, alpha 0.5. Optional.

Now overlay content functions:

```js
function showOverlay(kind){
  const ov = document.getElementById('overlay');
  const box = document.getElementById('ovBox');
  ov.className = kind; // 'demo' | 'over' | 'win' | 'hidden'
  if (kind==='demo'){
    box.innerHTML = `<div class="pill"><span class="dot"></span><b>DEMO</b> — the AI is playing &nbsp;·&nbsp; <b>Click or press any key to start</b></div>`;
  } else if (kind==='over'){
    box.innerHTML = `<div class="panel"><h1 class="red">GAME OVER</h1><p class="score">SCORE ${score}</p><button id="btn">↻ &nbsp;PLAY AGAIN</button><p class="hint">or press any key</p></div>`;
  } else if (kind==='win'){
    box.innerHTML = `<div class="panel"><h1 class="gold">★ YOU WIN! ★</h1><p class="score">SCORE ${score}</p><button id="btn">↻ &nbsp;PLAY AGAIN</button><p class="hint">or press any key</p></div>`;
  }
}
```

Button listener: since innerHTML re-created, attach via event delegation on overlay: ov.addEventListener('click', e => { if(state!=='playing') startGame(); }). And window pointerdown also triggers. Careful double-call: guard in startGame: `if (state==='playing') return;`. But when state==='demo' and user clicks button: window pointerdown fires startGame → state='playing'; then click event fires on button → handler checks state!=='playing' → false → no-op. Good.

Hide overlay: ov.className='hidden'.

CSS for overlay: 
```css
#overlay{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; pointer-events:none; z-index:5;}
#overlay.hidden{display:none;}
#overlay.demo{align-items:flex-start; padding-top:12px; background:linear-gradient(rgba(3,6,16,0.55), transparent 40%);}
```
Hmm demo: pill at top; background gradient subtle at top so pill readable. pointer-events none so clicks pass to window anyway (window listener catches). Button needs pointer-events:auto:
```css
#overlay button{pointer-events:auto; ...}
```
Panel styling with glow border.

Pill: inline-flex, gap, background rgba(8,14,30,0.85), border 1px solid rgba(90,200,255,0.4), border-radius 999px, padding 10px 18px, color #cfe9ff, font 14px, letter-spacing .5px, box-shadow 0 0 24px rgba(60,180,255,0.25). Blinking dot: .dot{width:8px;height:8px;border-radius:50%;background:#ff4d6d; box-shadow:0 0 8px #ff4d6d; animation: blink 1s infinite}.

h1: font-size 44px; gradient text via background-clip. .red → red/orange gradient; .gold → gold.

Button: neon: background transparent, border 1px solid #57d4ff, color #aef0ff, padding 12px 26px, border-radius 10px, font 16px bold, letter-spacing 1px, cursor pointer, box-shadow inset + outer glow; hover: background rgba(87,212,255,0.15).

Body: background radial gradient dark navy; display flex center; height 100vh; margin 0; overflow hidden (prevents scrollbars). #wrap{position:relative;} canvas{display:block; width: min(...); ...}. Let me compute canvas CSS:

```css
#wrap{position:relative; width:min(96vw, calc((100dvh - 32px) * 1.3333), 1080px); }
canvas{width:100%; height:auto; aspect-ratio: 4/3; display:block; border-radius:10px; box-shadow: 0 0 0 1px rgba(90,160,255,.18), 0 18px 60px rgba(0,0,0,.6), 0 0 80px rgba(40,120,255,.12); background:#070b18; touch-action:none;}
```

aspect-ratio 800/600 = 4/3. width min(96vw, (100dvh-32)*4/3, 1080). At 1280×800: (800-32)*1.333=1024; min(1228,1024,1080)=1024 → height 768 ≤ 800 ok. At 375×667: min(360, 846, 1080)=360 → height 270. Good. dvh fallback: use vh with fallback: `calc((100vh - 32px)*1.3333)` then dvh override. I'll just use 100vh (fine) plus min-height considerations. Actually mobile URL bars — use svh? Use `100dvh` with fallback line before. I'll write both.

Overlay inset 0 matches wrap size = canvas size. Good.

Font: `font-family: "Segoe UI", system-ui, -apple-system, Arial, sans-serif;` on body.

Canvas text font: `'700 16px "Segoe UI", system-ui, Arial, sans-serif'`.

Now the demo AI details:

```js
function aiControl(dt){
  let target = W/2;
  // choose lowest ball
  let lowest = null;
  for (const b of balls){ if (!b.stuck && (!lowest || b.y > lowest.y)) lowest = b; }
  if (lowest){ target = lowest.x + Math.sin(time*2.1)*10; }
  else if (balls.length && balls[0].stuck){ target = paddle.x; }
  const maxSp = 500;
  const d = target - mouseX;
  mouseX += clamp(d, -maxSp*dt, maxSp*dt);
}
```

Also AI could try to catch powerups: if a powerup falling and below? target = powerup.x when powerup y > H*0.4? Add: if any powerup with y > 200, target its x sometimes. Let's add: pick nearest falling powerup; if exists and its y > paddle.y - 220, target = p.x. This makes demo catch powerups — showcases features. But might cause misses; that's okay (demo). Add slight preference: if powerup falling and above paddle by > 150px, aim at it; else aim at ball. Actually aiming at ball is safer; powerups are bonus. Demo catching them shows the feature. I'll aim at powerup if one is falling and y > 300 (plenty of time), else ball. Hmm when powerup near bottom (y>450) still catchable. Condition: powerups.some(p => p.y > 250). Let's just: if powerups.length && powerups[0].y > 260 → target powerups[0].x else ball. Fine.

Ball speed 400 vs AI max 500 — AI mostly catches. Brick hits speed up to 640 > 500 → misses sometimes → lives lost in demo but demo respawns. Good, dynamic.

Serve timer in demo: 0.6s.

Now the "ball never passes through bricks or paddle" — substep size: dist per substep = speed*dt/steps where steps = ceil(dist/(r*0.8)) → per-substep movement ≤ 0.8*r ≈ 5.6px < brick height 22 and brick width 69 — with circle-rect test, no tunneling. Paddle height 14 > 5.6. Good.

Also handle ball hitting brick corner exactly — least penetration fallback.

One more: after brick hit, ball position set to boundary — prevents re-hit same substep; we break after one brick.

Multiple balls hitting same brick same substep: each ball processed sequentially; brick may die on first ball, second ball sees dead brick (alive check) — fine.

Paddle collision when ball stuck: skip (stuck handled separately).

Walls collision inside substep loop.

Let me also add slight screen shake on brick break? Small: shake = 3 on brick destroy, decay; render translate random*shake. Nice juice. Implement: shake magnitude var, in render: ctx.save(); if shake>0.1 translate((rand-0.5)*shake, ...). Apply before drawing world (not HUD). Keep small (max 4). Particles + shake + floaters = juicy.

Sound? No external; could use WebAudio beeps — allowed (no network). Adds polish. But autoplay policies: audio context requires user gesture; in demo no gesture → can't start audio. Could init on first pointer/key. Add simple WebAudio blips on events, created lazily on first user interaction. Risk: none if guarded. I'll add tiny synth: function beep(freq, dur, type, vol) with try/catch. Init AudioContext on first pointerdown/keydown. During demo (no gesture) no sound — fine. Keep code small. This is optional; include but carefully guarded so no console errors. `let AC=null; function audio(){ if(!AC){ try{ AC = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC=null; } } if(AC && AC.state==='suspended'){ AC.resume().catch(()=>{}); } return AC; }` beep: create oscillator+gain, exponential ramp. Call audio() only inside user-gesture handlers (pointerdown/keydown) to be safe; event beeps call beep() which checks AC exists. Good.

Sounds: paddle hit (220Hz square 0.05), wall (180), brick (440+rand), tough crack (300), powerup (880 arpeggio), lose life (descending), win jingle. Keep minimal: paddle, brick, power, lose. 

Now code size is getting big but fine.

Let me also handle visibility of "press any key" hint — done in overlay.

Edge: prevent arrow keys scrolling page — page doesn't scroll (overflow hidden) but preventDefault anyway.

Also handle window blur → clear keys (avoid stuck keys): window.addEventListener('blur', ()=>keys.clear()).

Now write render functions concretely.

Background: cached gradient:
```js
const bgGrad = ctx.createLinearGradient(0,0,0,H);
bgGrad.addColorStop(0,'#0a1024'); bgGrad.addColorStop(0.6,'#070b18'); bgGrad.addColorStop(1,'#05070f');
```
Plus subtle grid: draw vertical/horizontal lines every 40px alpha 0.05 — precompute path? Draw each frame with loop 20 lines — fine.

Vignette: radial gradient overlay cached.

Walls: 
```js
function drawWalls(){
  // left, right, top strips below HUD
  const g = ctx.createLinearGradient(0,TOP,0,H); // cache
  ctx.fillStyle = wallGrad; 
  ctx.fillRect(0, HUD_H, WALL, H-HUD_H);
  ctx.fillRect(W-WALL, HUD_H, WALL, H-HUD_H);
  ctx.fillRect(0, HUD_H, W, WALL); // top wall strip 46..56
  // inner neon line
  ctx.fillStyle = 'rgba(90,200,255,0.8)';
  ctx.fillRect(WALL, HUD_H, 2, H-HUD_H);
  ctx.fillRect(W-WALL-2, HUD_H, 2, H-HUD_H);
  ctx.fillRect(0, HUD_H+WALL-2, W, 2);
}
```
With glow: ctx.shadowColor='rgba(80,190,255,0.9)'; shadowBlur=8; draw lines; reset.

Ball bounds: left = WALL + r? If wall strip is 0..10, ball should bounce at x < 10 + r. FIELD_L=10. Top: strip 46..56, bounce at y < 56 + r → FIELD_TOP = 56 = HUD_H + WALL. OK consistent.

HUD bar: fillRect(0,0,W,HUD_H) with gradient dark; bottom border line rgba(90,200,255,0.25). Text: score left; center title small; right hearts; expand bar maybe under center.

Let me draw expand timer as a bar in HUD center: if expandTimer>0: text `WIDE PADDLE ${expandTimer.toFixed(1)}s` at center, plus thin bar width proportional. 

Bricks draw with slight drop shadow: ctx.shadowColor='rgba(0,0,0,0.4)'; shadowBlur=4; shadowOffsetY=2 — per brick might be heavy but 80 bricks fine.

Tough brick style: base gradient steel: createLinearGradient vertical '#b8c4d4' → '#7e8b9e' → '#5d6a7d'. Border stroke '#39424f'. Inner bevel: stroke inset lighter. Rivets: 4 small circles at corners inset 5px, fill 'rgba(40,48,60,0.9)'. When cracked: draw 3 jagged dark polylines: precompute per brick: br.cracks = array of point arrays relative to brick, generated at build with rng: start near center, random walk 4 segments. Draw stroke 'rgba(20,24,32,0.85)' lineWidth 1.5. Also overlay darker tint rgba(0,0,0,0.25) when cracked.

Normal brick: gradient row color light→dark: create two stops: lighten(color, 0.25) → color → darken? Use helper shade(hex, amt). I'll precompute per-row gradients at build: rowGrad[r] = createLinearGradient(0, y0, 0, y0+BRICK_H) but bricks in same row share y — gradient depends on y; create per brick (80 gradients at build, cached — fine, done once).

Actually simpler: per brick create gradient at build: br.grad.

Power icon in brick: draw white icon at center with glow: ctx.shadowColor='#fff'; shadowBlur=6.

Ball render:
```js
function drawBall(b){
  // trail
  for (let i=0;i<b.trail.length;i++){ const t=b.trail[i]; const a=(i/b.trail.length)*0.25; ctx.fillStyle=`rgba(140,220,255,${a})`; circle(t.x,t.y,b.r*(0.4+0.6*i/b.trail.length)); }
  ctx.save(); ctx.shadowColor='#bfeaff'; ctx.shadowBlur=14;
  const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r);
  g.addColorStop(0,'#ffffff'); g.addColorStop(1,'#9fdcff');
  ctx.fillStyle=g; circle(b.x,b.y,b.r); fill; ctx.restore();
}
```
Trail update in update: b.trail.push({x,y}); if length>12 shift. When stuck, clear trail? push anyway fine.

Paddle render:
```js
function drawPaddle(){
  const w=paddle.w, h=paddle.h, x=paddle.x-w/2, y=paddle.y;
  ctx.save();
  ctx.shadowColor = paddle.expandTimer>0 ? '#4dff9d' : '#4dc9ff'; ctx.shadowBlur=16;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  if (expand) g stops '#5cffb0','#1fbf6e' else '#7fe3ff','#1e90ff';
  rr(x,y,w,h,7); fill;
  // top highlight
  ctx.shadowBlur=0; ctx.fillStyle='rgba(255,255,255,0.5)'; rr(x+3,y+2,w-6,3,2); fill;
  ctx.restore();
}
```

Powerup capsule render:
```js
function drawPower(p){
  const w=p.w,h=p.h,x=p.x-w/2,y=p.y-h/2;
  ctx.save();
  const col = p.type==='expand' ? '#3dff9c' : '#ff5df2';
  ctx.shadowColor=col; ctx.shadowBlur=12;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  g.addColorStop(0, lighten); g.addColorStop(1, col);
  rr(x,y,w,h,9); fill;
  ctx.shadowBlur=0; ctx.strokeStyle='rgba(255,255,255,0.7)'; lineWidth=1; rr stroke;
  drawIcon(p.type, p.x, p.y + Math.sin(p.bob)*1.5, 6, '#0b1020'); // dark icon on bright capsule? or white
  ctx.restore();
}
```
Icon color: white looks good on colored capsule: '#fff' with slight dark stroke? Use '#ffffff'.

Brick power icon: white on colored brick with glow.

Floaters:
```js
{ x, y, vy:-40, life:0.9, max, text, color }
draw: alpha, font bold 13px, fillText centered.
```

Particles draw: fillRect small or circle; use circle for roundness; alpha.

Shake: let shake=0; on brick destroy shake=Math.min(shake+3,6); update shake*=Math.pow(0.001, dt)? simpler shake = Math.max(0, shake - dt*14). render: if shake>0.2 ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake) after save, restore after world draw (before HUD? HUD can shake too, fine—apply to whole canvas).

Now level build with guaranteed powerups:

```js
function buildLevel(){
  bricks = [];
  const rng = mulberry32((Math.random()*1e9)|0);
  const rowColors = ['#ff4757','#ff7f50','#ffa502','#eccc68','#2ed573','#1e90ff','#3742fa','#a55eea'];
  for (let r=0;r<ROWS;r++){
    for (let c=0;c<COLS;c++){
      const x = FIELD_L + 14 + c*(brickW+GAP);
      const y = FIELD_TOP + 16 + r*(BRICK_H+GAP);
      let type='normal';
      if (r===0 || r===1) type='tough';
      else if (r===3 && (c%4===1)) type='tough';
      else if (r===5 && (c%3===0)) type='tough';
      const hp = type==='tough'?2:1;
      const br = {x,y,w:brickW,h:BRICK_H,type,hp,maxHp:hp,row:r,col:c,alive:true,cracked:false,power:null,rng};
      // gradient
      if (type==='tough'){ br.grad = toughGrad(y); } else { br.grad = rowGrad(rowColors[r], y); }
      // cracks precomputed
      br.cracks = makeCracks(rng, brickW, BRICK_H);
      bricks.push(br);
    }
  }
  // assign powerups: ~16 bricks
  const candidates = bricks.slice();
  shuffle(candidates, rng);
  let i=0;
  const wantExpand = 4, wantMulti = 4;
  // assign alternating
  let n=0;
  for (const br of candidates){
    if (n>=8) break;
    if (rng()<0.9){ br.power = (n%2===0)?'expand':'multiball'; n++; }
  }
  // ensure at least 3 each
  ...
}
```

Simpler deterministic-ish: iterate bricks, if rng()<0.18 assign power alternating type starting random. With 80 bricks, ~14 powerups, roughly half each. Ensure min 3 each: count; if expand<3, convert some random power bricks or normal bricks. I'll write a small ensure loop.

Actually simpler: choose exactly: indices where (r*COLS+c) % 7 === 3 → power, type = ((r+c)%2? 'multiball':'expand'). That's ~11 bricks, alternating. Plus maybe random extra. Deterministic pattern might look patterned but icons visible — fine, spec just wants some bricks with icons. I'll do rng-based with ensure-minimum; it's fine.

makeCracks(rng,w,h): 3 polylines: each starts at random point, 4 segments random angle ±, length w*0.15. Store relative coords.

rowGrad(color,y): 
```js
function hexToRgb... 
```
Write shade helper:
```js
function shade(hex, amt){ // amt -1..1
  const n=parseInt(hex.slice(1),16);
  let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
  if(amt>=0){ r+= (255-r)*amt; g+=(255-g)*amt; b+=(255-b)*amt; } else { r*=1+amt; g*=1+amt; b*=1+amt; }
  return `rgb(${r|0},${g|0},${b|0})`;
}
```

Brick grad: createLinearGradient(0,y,0,y+BRICK_H): stop0 shade(color,0.35), stop 0.5 color, stop1 shade(color,-0.35). Tough: '#cfd8e3','#8b98ab','#5a6678'.

Now collision code with prev positions. In substep loop:

```js
for (let s=0;s<steps;s++){
  const px=b.x, py=b.y;
  b.x += b.vx*sdt; b.y += b.vy*sdt;
  // walls
  if (b.x < FIELD_L + b.r){ b.x = FIELD_L + b.r; b.vx = Math.abs(b.vx); spark... }
  else if (b.x > FIELD_R - b.r){ b.x = FIELD_R - b.r; b.vx = -Math.abs(b.vx); }
  if (b.y < FIELD_TOP + b.r){ b.y = FIELD_TOP + b.r; b.vy = Math.abs(b.vy); }
  // bricks
  if (collideBricks(b, px, py)) { /* handled */ }
  // paddle
  collidePaddle(b, px, py);
}
```

collideBricks:
```js
function collideBricks(b, px, py){
  for (const br of bricks){
    if (!br.alive) continue;
    // quick reject
    if (b.x + b.r < br.x || b.x - b.r > br.x+br.w || b.y + b.r < br.y || b.y - b.r > br.y+br.h) continue;
    // circle-rect
    const cx = clamp(b.x, br.x, br.x+br.w);
    const cy = clamp(b.y, br.y, br.y+br.h);
    const dx = b.x-cx, dy = b.y-cy;
    if (dx*dx+dy*dy > b.r*b.r) continue;
    // determine normal
    let nx=0, ny=0;
    if (py + b.r <= br.y + 0.01) ny = -1;
    else if (py - b.r >= br.y + br.h - 0.01) ny = 1;
    else if (px + b.r <= br.x + 0.01) nx = -1;
    else if (px - b.r >= br.x + br.w - 0.01) nx = 1;
    else {
      // overlap-based
      const ol = (b.x+b.r)-br.x, orr=(br.x+br.w)-(b.x-b.r);
      const ot=(b.y+b.r)-br.y, ob=(br.y+br.h)-(b.y-b.r);
      const mx=Math.min(ol,orr), my=Math.min(ot,ob);
      if (mx<my) nx = ol<orr ? -1 : 1; else ny = ot<ob ? -1 : 1;
    }
    if (nx!==0){ b.vx = (nx<0?-1:1)*Math.abs(b.vx); b.x = nx<0 ? br.x - b.r : br.x+br.w + b.r; }
    else { b.vy = (ny<0?-1:1)*Math.abs(b.vy); b.y = ny<0 ? br.y - b.r : br.y+br.h + b.r; }
    fixAngle(b);
    hitBrick(br);
    return true;
  }
  return false;
}
```

Wait: when nx determined from px tests but ball also overlapping vertically... e.g., ball approaches from left: px + r <= br.x → nx=-1, vx reversed, x set to left of brick. Good. If ball comes from top-left corner diagonally: px+r <= br.x might hold while py+r <= br.y also holds — first branch (top) wins → bounce up only, ball still overlapping left side next substep → next substep px+r <= br.x still → nx bounce. With substeps small it resolves in 1-2 substeps. Fine.

hitBrick:
```js
function hitBrick(br){
  br.hp--;
  beep(...)
  if (br.hp<=0){
    br.alive=false;
    const pts = (br.type==='tough'?150:100) + (ROWS-1-br.row)*10;
    score += pts;
    addFloater(br.x+br.w/2, br.y+br.h/2, '+'+pts, br.type==='tough'?'#ffd32a':'#ffffff');
    burst(br.x+br.w/2, br.y+br.h/2, br.type==='tough'?'#9fb2c8':rowColors[br.row], 18, 1);
    shake = Math.min(shake+3, 5);
    if (br.power) dropPower(br);
    ballSpeed = Math.min(660, ballSpeed + 5);
    for (const b of balls) if(!b.stuck) setSpeed(b, ballSpeed);
    beep(520+Math.random()*160, 0.07, 'square', 0.05);
  } else {
    br.cracked = true;
    score += 25;
    addFloater(..., '+25', '#aab');
    burst small 6 particles gray;
    beep(300,0.06,'sawtooth',0.05);
  }
}
```

collidePaddle:
```js
function collidePaddle(b, px, py){
  const half = paddle.w/2;
  if (b.y + b.r < paddle.y || b.y - b.r > paddle.y + paddle.h) return;
  if (Math.abs(b.x - paddle.x) > half + b.r) return;
  if (b.vy <= 0 && py + b.r <= paddle.y) { /* moving up and above? skip */ }
  if (py + b.r <= paddle.y + 0.01 && b.vy > 0){
    const rel = clamp((b.x - paddle.x)/half, -1, 1);
    const maxAng = Math.PI*0.36;
    const ang = rel*maxAng;
    b.vx = b.speed*Math.sin(ang);
    b.vy = -b.speed*Math.cos(ang);
    b.y = paddle.y - b.r;
    spark at contact; beep(200,0.05,'square',0.05);
  } else if (b.vy > 0 || py + b.r > paddle.y) {
    // side hit
    const side = (px < paddle.x) ? -1 : 1;
    b.vx = side*Math.abs(b.vx);
    b.x = paddle.x + side*(half + b.r);
  }
}
```

Hmm the condition structure: if ball overlaps paddle and (prev bottom above paddle top) → top bounce. Else (ball beside/below) → side push. If ball moving up and overlapping paddle from below (shouldn't happen since paddle near bottom and ball above)... if ball below paddle moving up through? Ball below paddle can only get there by passing paddle — prevented. Edge: ball moving up, overlapping paddle side region while rising — side branch would flip vx — acceptable.

Simplify:
```js
if (py + b.r <= paddle.y + 0.01) { top bounce (only if vy>0) } else { side }
```
If py+r <= paddle.y but vy<0 (moving up away) — ball above paddle moving up, overlapping? Can't overlap if above... could overlap corner. Then top bounce with vy>0 check fails; do side? Let's just: if vy>0 && py+b.r <= paddle.y+0.01 → top bounce; else if vy>0 → side bounce; else (vy<=0) → push ball up: b.y = paddle.y - b.r; b.vy = -Math.abs(b.vy). That last case: ball moving up into paddle from below — reflect. OK.

fixAngle after top bounce not needed (cos(65°)=0.42 > 0.22 min). Fine but call anyway harmless? fixAngle only adjusts if |vy| < minVy — after paddle bounce |vy|≥0.42*speed > min. Skip.

Now multiball spawn:
```js
function applyPower(p){
  if (p.type==='expand'){
    paddle.expandTimer = 10;
    addFloater(paddle.x, paddle.y - 24, 'WIDE PADDLE 10s', '#3dff9c');
    beep(660,0.09,'sine',0.06); setTimeout? no—do quick arpeggio via multiple beeps scheduled: beep at delays using AC time. Keep single beep + maybe second: beep(880,0.12,'sine',0.06) with delay — implement beep(freq,dur,type,vol,when=0).
  } else {
    if (balls.length < 3){
      const angles = [-0.55, 0.55, 0];
      // add until 3
      while (balls.length < 3){
        const b = {x:paddle.x, y:paddle.y - 10, r:7, speed:ballSpeed, stuck:false, trail:[], timer:0};
        const a = angles[balls.length] ?? (Math.random()-0.5);
        b.vx = ballSpeed*Math.sin(a); b.vy = -ballSpeed*Math.cos(a);
        balls.push(b);
      }
      addFloater(paddle.x, paddle.y-24, 'MULTI-BALL ×3', '#ff5df2');
    } else {
      score += 150; addFloater(paddle.x, paddle.y-24, '+150 MAX BALLS', '#ffd32a');
    }
    beep...
  }
  score += 25? maybe +25 for catching any. Add score+=25 floater? Keep: catch gives +25.
}
```

Wait: balls.length could be 2 when caught (one lost) → add 1 → 3. angles index: balls.length before push: 0→-0.55? If only one ball exists (the one in play), new ones should be at ±0.55. balls.length===1 → angles[1]=0.55? Hmm indexing: I want new balls at angles not duplicating existing. Simple: new ball angle = [-0.6, 0.6, 0][balls.length % 3]... if balls.length===1, we add 2 balls: first gets angles[1]? Let me just: while balls.length<3: a = (balls.length===1 ? (balls.length... 

Cleaner: 
```js
const spread = [-0.6, 0, 0.6];
let k = 0;
while (balls.length < 3){
  const a = spread[k++];
  ...
}
```
If 1 ball → add 2: angles -0.6, 0 — the 0 overlaps existing direction; visually fine (three balls fanning). If 2 balls → add 1: angle -0.6. Eh, asymmetric but fine. Alternatively random angle each: a=(Math.random()-0.5)*1.2. I'll use spread with offset: startIdx = balls.length; for n from balls.length to 2: a = spread[n]... spread[1]=0, spread[2]=0.6 for 1 existing → adds 0 and 0.6. OK whatever — use: `const a = (balls.length - 1) * 0.6 - 0.6;` when adding: if length 1 → a=-0.6; then length 2 → a=0. Hmm.

Simplest good-looking: new balls at angles ±0.55 relative: 
```js
while (balls.length < 3){
  const a = balls.length === 1 ? (Math.random()<0.5?-0.55:0.55) : (balls.length===2 ? (Math.random()<0.5?-0.55:0.55) : 0);
```
Overcomplicating. Do: angles queue [-0.55, 0.55, 0]; shift from queue while adding:
```js
const q=[-0.55,0.55,0];
while(balls.length<3){ const a=q.shift(); ... }
```
1 existing → adds -0.55, 0.55. 2 existing → adds -0.55. 

Also new balls shouldn't be stuck; launched immediately. Also ensure they don't immediately collide paddle: spawn at paddle.y - r - 2 with vy negative. Good.

Also cap: if multiball caught with 3 balls already → bonus.

dropPower:
```js
function dropPower(br){
  powerups.push({x: br.x+br.w/2, y: br.y+br.h/2, vy: 150, w: 40, h: 18, type: br.power, bob: Math.random()*6});
}
```

Catch check in update loop as described.

Serve/respawn:
```js
function respawnBall(){
  balls = [{x:paddle.x, y:paddle.y-8, r:7, vx:0, vy:0, speed:ballSpeed, stuck:true, timer: 1.0, trail:[]}];
  paddle.expandTimer = 0;
}
```

startGame():
```js
function startGame(){
  if (state==='playing') return;
  state='playing';
  score=0; lives=3; ballSpeed=400;
  paddle.w=110; paddle.x=W/2; mouseX=W/2; paddle.expandTimer=0;
  powerups=[]; particles=[]; floaters=[];
  buildLevel();
  respawnBall();
  hideOverlay();
}
```

Init demo:
```js
state='demo'; score=0; lives=3; ballSpeed=400; buildLevel(); respawnBall(); showOverlay('demo');
```
Demo ball timer 0.8.

In demo when balls empty → respawnBall() (lives unchanged). Also if all bricks cleared in demo → buildLevel() again.

Win check only when state==='playing'.

Game over: state='over'; showOverlay('over'); beep descending.

Now the overlay button: also allow clicking during demo anywhere → startGame via window pointerdown. Note: pointerdown on window fires even on overlay (pointer-events none except button; button pointer-events auto → click works, bubbles to window pointerdown? pointerdown on button bubbles to window — yes). Good.

But careful: during 'playing', pointerdown shouldn't do anything — guard `if(state!=='playing') startGame()`. During playing, maybe pointerdown serves stuck ball early: if balls[0].stuck → timer=0. Nice touch: click to launch. Implement in pointerdown handler: if state==='playing'{ for stuck balls timer=0 }.

Keyboard: any key when not playing → startGame. During playing, keys tracked for arrows.

Also 'Enter'/' ' restart from over/win — covered by any-key.

Prevent default for arrows/space to avoid page scroll: in keydown, if e.key startsWith 'Arrow' or ' ' → preventDefault.

Now DPR setup:
```js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const DPR = Math.min(window.devicePixelRatio||1, 2);
canvas.width = W*DPR; canvas.height = H*DPR;
ctx.setTransform(DPR,0,0,DPR,0,0);
```
Canvas element has width/height attrs set in JS; CSS controls display size. Set attrs initially 800/600 in HTML then override.

Mouse mapping:
```js
function toGameX(e){
  const r = canvas.getBoundingClientRect();
  return (e.clientX - r.left) * (W / r.width);
}
window.addEventListener('pointermove', e => { mouseX = toGameX(e); });
```

Touch: pointermove covers touch drag; add touch-action:none on canvas (CSS) to prevent scroll; also pointerdown.

Now HUD text drawing:

```js
function drawHUD(){
  // bar
  const g = hudGrad; ctx.fillStyle=g; ctx.fillRect(0,0,W,HUD_H);
  ctx.fillStyle='rgba(90,200,255,0.25)'; ctx.fillRect(0,HUD_H-1,W,1);
  ctx.textBaseline='middle';
  ctx.textAlign='left';
  ctx.font='700 15px "Segoe UI", system-ui, Arial, sans-serif';
  ctx.fillStyle='#9fdcff';
  ctx.fillText('SCORE  ' + score, 16, HUD_H/2+1);
  // center title
  ctx.textAlign='center';
  ctx.fillStyle='rgba(160,210,255,0.5)';
  ctx.font='600 11px ...';
  ctx.fillText('N E O N   B R E A K O U T', W/2, HUD_H/2);
  // expand timer
  if (paddle.expandTimer>0){
    const t=paddle.expandTimer;
    ctx.textAlign='center';
    ctx.fillStyle='#3dff9c';
    ctx.font='700 12px ...';
    ctx.fillText(`WIDE PADDLE ${t.toFixed(1)}s`, W/2, HUD_H/2 + 14); // might collide with title; move title? 
  }
```
Hmm center has title; put expand indicator left of hearts or below score. Let's put expand timer at center only when active and skip title then? Simpler: draw title at center always; expand bar: draw at center bottom of HUD: small bar: x from W/2-40..W/2+40 at y HUD_H-6, height 3, fill proportion. Plus label right of score: `WIDE 7.2s` in green at x=140. I'll draw text after score: if expandTimer>0: fillStyle '#3dff9c', fillText(`WIDE ${t.toFixed(1)}s`, 150, HUD_H/2).

Hearts right: for i in 0..2: x = W - 20 - i*24, y=HUD_H/2; filled if i<lives: drawHeart filled '#ff4d6d' with glow; else outline dim.

Also show lives label? hearts suffice.

render() full:

```js
function render(){
  ctx.save();
  if (shake>0.2) ctx.translate((Math.random()-0.5)*shake, (Math.random()-0.5)*shake);
  ctx.fillStyle = bgGrad; ctx.fillRect(-10,-10,W+20,H+20);
  drawStars();
  drawGrid();
  drawWalls();
  for (const br of bricks) if (br.alive) drawBrick(br);
  for (const p of powerups) drawPower(p);
  drawParticles();
  drawPaddle();
  for (const b of balls) drawBall(b);
  drawFloaters();
  ctx.restore();
  drawHUD();
}
```

Grid: lines every 40: strokeStyle 'rgba(90,140,255,0.05)'.

Stars: array of 60 {x,y in field, s, sp, ph}; update in update(): y+=sp*dt; if y>H → y=FIELD_TOP... draw alpha 0.15+0.2*sin(time*2+ph), color '#8fc7ff', size 1-2. Draw only within field? Whole canvas fine (behind walls).

drawBrick:
```js
function drawBrick(br){
  ctx.save();
  ctx.shadowColor='rgba(0,0,0,0.45)'; ctx.shadowBlur=4; ctx.shadowOffsetY=2;
  rr(br.x, br.y, br.w, br.h, 5);
  ctx.fillStyle = br.grad; ctx.fill();
  ctx.shadowColor='transparent'; ctx.shadowBlur=0; ctx.shadowOffsetY=0;
  ctx.strokeStyle = br.type==='tough' ? 'rgba(30,38,50,0.9)' : 'rgba(0,0,0,0.28)';
  ctx.lineWidth=1; ctx.stroke();
  if (br.type==='tough'){
    // bevel highlight
    ctx.strokeStyle='rgba(255,255,255,0.35)';
    rr(br.x+2, br.y+2, br.w-4, br.h-4, 4); ctx.stroke();
    // rivets
    ctx.fillStyle='rgba(25,32,44,0.9)';
    const o=5;
    [[br.x+o,br.y+o],[br.x+br.w-o,br.y+o],[br.x+o,br.y+br.h-o],[br.x+br.w-o,br.y+br.h-o]].forEach(([x,y])=>{ctx.beginPath();ctx.arc(x,y,1.8,0,7);ctx.fill();});
    if (br.cracked){
      ctx.fillStyle='rgba(10,14,20,0.28)'; rr(br.x,br.y,br.w,br.h,5); ctx.fill();
      ctx.strokeStyle='rgba(15,20,28,0.9)'; ctx.lineWidth=1.4;
      for (const line of br.cracks){
        ctx.beginPath(); ctx.moveTo(br.x+line[0][0], br.y+line[0][1]);
        for (let i=1;i<line.length;i++) ctx.lineTo(br.x+line[i][0], br.y+line[i][1]);
        ctx.stroke();
      }
    }
  } else {
    // top highlight
    ctx.fillStyle='rgba(255,255,255,0.28)';
    rr(br.x+2, br.y+2, br.w-4, 5, 3); ctx.fill();
  }
  if (br.power){
    ctx.save();
    ctx.shadowColor='#ffffff'; ctx.shadowBlur=8;
    drawIcon(br.power, br.x+br.w/2, br.y+br.h/2, 7, 'rgba(255,255,255,0.95)');
    ctx.restore();
  }
  ctx.restore();
}
```

rr helper:
```js
function rr(x,y,w,h,r){
  r = Math.min(r, w/2, h/2);
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,x+w,y+h,r);
  ctx.arcTo(x+w,y+h,x,y+h,r);
  ctx.arcTo(x,y+h,x,y,r);
  ctx.arcTo(x,y,x+w,y,r);
  ctx.closePath();
}
```

makeCracks:
```js
function makeCracks(rng,w,h){
  const lines=[];
  for (let i=0;i<3;i++){
    let x=w*(0.25+0.5*rng()), y=h*(0.2+0.6*rng());
    const pts=[[x,y]];
    let ang=rng()*Math.PI*2;
    for (let s=0;s<4;s++){
      ang += (rng()-0.5)*1.6;
      x += Math.cos(ang)*w*0.16; y += Math.sin(ang)*h*0.3;
      x=clamp(x,2,w-2); y=clamp(y,2,h-2);
      pts.push([x,y]);
    }
    lines.push(pts);
  }
  return lines;
}
```

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

Power assignment:
```js
// after bricks built
const idx=[];
for (let i=0;i<bricks.length;i++) if (rng()<0.2) idx.push(i);
// ensure >= 3 each type
let ne=0, nm=0;
const assign=[];
for (const i of idx){ const t = (ne<=nm)?'expand':'multiball'; ... }
```
Simpler: shuffle indices of all bricks, take first 10, alternate types expand/multiball; plus ensure at least 3 each by construction (5 each). Let's do exactly 10 powerup bricks: 5 expand, 5 multiball, random positions (not on... any brick fine including tough — power drops when destroyed, tough needs 2 hits — that's fine and even interesting). Shuffle:

```js
const order=[];
for(let i=0;i<bricks.length;i++) order.push(i);
for(let i=order.length-1;i>0;i--){const j=(rng()*(i+1))|0;[order[i],order[j]]=[order[j],order[i]];}
for(let k=0;k<10;k++){ bricks[order[k]].power = k%2===0?'expand':'multiball'; }
```

Good: 10 powerups among 80 bricks, 5 each.

Hmm but top rows are tough — powerups there drop after 2 hits. Fine.

Now update() full:

```js
function update(dt){
  time += dt;
  // stars
  for (const s of stars){ s.y += s.sp*dt; if (s.y > H){ s.y = -2; s.x = Math.random()*W; } }
  // input
  if (state==='demo') aiControl(dt);
  else {
    let dir=0;
    if (keys.has('ArrowLeft')||keys.has('a')||keys.has('A')) dir--;
    if (keys.has('ArrowRight')||keys.has('d')||keys.has('D')) dir++;
    if (dir!==0) mouseX += dir*760*dt;
  }
  // expand timer
  if (paddle.expandTimer>0){ paddle.expandTimer=Math.max(0,paddle.expandTimer-dt); }
  const targetW = paddle.expandTimer>0?172:112;
  paddle.w += (targetW-paddle.w)*Math.min(1,dt*12);
  const minX=FIELD_L+paddle.w/2+1, maxX=FIELD_R-paddle.w/2-1;
  mouseX=clamp(mouseX,minX,maxX);
  paddle.x += (mouseX-paddle.x)*Math.min(1,dt*40);
  paddle.x=clamp(paddle.x,minX,maxX);
  // powerups
  for(let i=powerups.length-1;i>=0;i--){
    const p=powerups[i];
    p.y+=p.vy*dt; p.bob+=dt*5;
    if (p.y+p.h/2>=paddle.y && p.y-p.h/2<=paddle.y+paddle.h && Math.abs(p.x-paddle.x)<=paddle.w/2+p.w/2){
      applyPower(p); powerups.splice(i,1); continue;
    }
    if (p.y-p.h>H) powerups.splice(i,1);
  }
  // balls
  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    if (b.stuck){
      b.x=paddle.x; b.y=paddle.y-b.r-1; b.trail.length=0;
      b.timer-=dt;
      if (b.timer<=0){ b.stuck=false; const a=(Math.random()-0.5)*0.5; b.vx=b.speed*Math.sin(a); b.vy=-b.speed*Math.cos(a); }
    } else {
      const dist=b.speed*dt;
      const steps=Math.max(1, Math.ceil(dist/(b.r*0.8)));
      const sdt=dt/steps;
      for(let s=0;s<steps;s++){
        const px=b.x, py=b.y;
        b.x+=b.vx*sdt; b.y+=b.vy*sdt;
        if (b.x<FIELD_L+b.r){ b.x=FIELD_L+b.r; b.vx=Math.abs(b.vx); miniBurst(b.x,b.y,'#8fc7ff',2); }
        else if (b.x>FIELD_R-b.r){ b.x=FIELD_R-b.r; b.vx=-Math.abs(b.vx); miniBurst(...); }
        if (b.y<FIELD_TOP+b.r){ b.y=FIELD_TOP+b.r; b.vy=Math.abs(b.vy); miniBurst(...); }
        collideBricks(b,px,py);
        collidePaddle(b,px,py);
      }
      b.trail.push({x:b.x,y:b.y}); if (b.trail.length>10) b.trail.shift();
    }
    if (b.y-b.r>H){ balls.splice(i,1); }
  }
  if (balls.length===0){
    if (state==='demo'){ respawnBall(); }
    else {
      lives--;
      if (lives<=0){ state='over'; showOverlay('over'); beepLose(); }
      else { respawnBall(); beep(160,0.2,'sawtooth',0.06); }
    }
  }
  // bricks cleared
  if (bricks.length && bricks.every(b=>!b.alive)){
    if (state==='demo'){ buildLevel(); }
    else if (state==='playing'){ state='win'; showOverlay('win'); beepWin(); }
  }
  // particles
  for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.life-=dt; if(p.life<=0){particles.splice(i,1);continue;} p.vy+=700*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; }
  // floaters
  for(let i=floaters.length-1;i>=0;i--){ const f=floaters[i]; f.life-=dt; f.y+=f.vy*dt; if(f.life<=0) floaters.splice(i,1); }
  shake=Math.max(0,shake-dt*16);
}
```

Note: collideBricks may destroy brick and drop powerup; also could set state? no.

Also when state==='over' or 'win', keep updating background/particles but freeze gameplay? Simpler: keep everything updating but balls empty (no respawn since state!=='demo' and lives<=0 → would respawn... careful: in 'over', balls.length===0 triggers lives-- again → lives negative, showOverlay again. Guard: only handle life loss when state==='playing'. Let me guard: `if (balls.length===0 && state!=='over' && state!=='win'){...}`. And in over/win, keep simulating existing balls? balls empty anyway. Also in over/win, skip paddle input? Keep demo-like AI? Let's keep game world frozen except particles/floaters/stars: if state is over/win, skip ball updates? Balls are empty in over. In win, balls still bouncing — fun to watch behind overlay. Keep updating balls regardless; just guard life-loss to state==='playing'. And win check only in playing (already). Demo continues.

Also in 'over'/'win', overlay shown; pointer/key restarts.

miniBurst for wall hits: maybe skip to reduce noise; add tiny 2-particle sparks — okay keep small.

beep functions:
```js
let AC=null;
function ensureAudio(){ if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ AC=null; } } if(AC&&AC.state==='suspended'){ AC.resume().catch(()=>{}); } }
function beep(freq,dur,type='square',vol=0.05,when=0){
  if(!AC) return;
  try{
    const t=AC.currentTime+when;
    const o=AC.createOscillator(), g=AC.createGain();
    o.type=type; o.frequency.setValueAtTime(freq,t);
    g.gain.setValueAtTime(vol,t);
    g.gain.exponentialRampToValueAtTime(0.0001,t+dur);
    o.connect(g); g.connect(AC.destination);
    o.start(t); o.stop(t+dur+0.02);
  }catch(e){}
}
```
Call ensureAudio() in pointerdown/keydown handlers.

beepLose: beep(300,0.15,'sawtooth',0.06); beep(200,0.2,'sawtooth',0.06,0.12); beep(120,0.35,'sawtooth',0.06,0.26);
beepWin: ascending: 523,659,784,1047 with when 0,0.1,0.2,0.3.

Now HTML/CSS details.

```css
*{box-sizing:border-box}
html,body{margin:0;height:100%;}
body{
  background: radial-gradient(1200px 800px at 50% 20%, #101a3a 0%, #070b18 55%, #04060d 100%);
  display:flex; align-items:center; justify-content:center;
  font-family:"Segoe UI",system-ui,-apple-system,Arial,sans-serif;
  overflow:hidden; color:#dfe9ff;
  -webkit-user-select:none; user-select:none;
}
#wrap{position:relative; width:min(96vw, calc((100vh - 28px)*1.3333), calc((100dvh - 28px)*1.3333), 1060px);}
```
CSS min() with multiple args ok. calc inside min ok. But mixing vh and dvh both — just use dvh with vh fallback line before:
```css
#wrap{width:min(96vw, calc((100vh - 28px)*1.3333), 1060px);}
@supports (height:100dvh){ #wrap{ width:min(96vw, calc((100dvh - 28px)*1.3333), 1060px);} }
```
Fine.

canvas CSS: width:100%; aspect-ratio:800/600; height:auto; display:block; border-radius:12px; touch-action:none; box-shadow etc; background:#070b18.

Overlay CSS:
```css
#overlay{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; z-index:10; pointer-events:none; border-radius:12px; overflow:hidden;}
#overlay.hidden{display:none;}
#overlay.demo{align-items:flex-start; padding-top:14px; background:linear-gradient(to bottom, rgba(4,8,18,0.72), rgba(4,8,18,0) 34%);}
.pill{pointer-events:none; display:flex; align-items:center; gap:10px; background:rgba(8,14,30,0.85); border:1px solid rgba(96,205,255,0.45); color:#cfeaff; padding:10px 18px; border-radius:999px; font-size:14px; letter-spacing:0.4px; box-shadow:0 0 24px rgba(60,170,255,0.25), inset 0 0 12px rgba(80,180,255,0.08); backdrop-filter:blur(4px); white-space:nowrap;}
```
white-space nowrap might overflow at 375px: pill text long. Make font smaller on mobile via media query, and allow wrap: remove nowrap, add max-width:92%; text-align center; flex-wrap. At 375: canvas 360 wide; pill text "DEMO — AI playing · Click or press any key to start" wraps to 2 lines — okay. Use shorter text: "▶ DEMO — click or press any key to start". Add media query font-size 12px.

.dot blink animation.

.panel:
```css
.panel{pointer-events:auto; text-align:center; background:rgba(8,13,28,0.82); border:1px solid rgba(110,200,255,0.35); border-radius:16px; padding:28px 44px; box-shadow:0 0 50px rgba(50,150,255,0.25), inset 0 0 30px rgba(60,150,255,0.06); backdrop-filter:blur(6px); animation:pop .35s ease;}
.panel h1{margin:0 0 10px; font-size:42px; letter-spacing:4px; font-weight:800;}
h1.red{background:linear-gradient(180deg,#ff8a7a,#ff2d55); -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 30px rgba(255,60,90,0.35);}
```
text-shadow with transparent color shows shadow of glyphs — with background-clip text, text-shadow draws behind — can look odd (shadow visible through transparent text). Use filter: drop-shadow instead. `filter:drop-shadow(0 0 18px rgba(255,45,85,0.45));` OK.

h1.gold similar gold.

.score{font-size:20px; color:#ffe08a; margin:6px 0 18px; letter-spacing:1px; font-weight:700;}
button:
```css
.panel button{pointer-events:auto; cursor:pointer; font:inherit; font-weight:700; font-size:16px; letter-spacing:1.5px; color:#06121f; background:linear-gradient(180deg,#7fe3ff,#38b6ff); border:none; border-radius:10px; padding:12px 26px; box-shadow:0 0 22px rgba(80,190,255,0.55), 0 4px 0 #1a6ea8; transition:transform .08s, box-shadow .2s;}
.panel button:hover{box-shadow:0 0 32px rgba(120,220,255,0.8), 0 4px 0 #1a6ea8; transform:translateY(-1px);}
.panel button:active{transform:translateY(2px); box-shadow:0 0 12px rgba(80,190,255,0.5), 0 1px 0 #1a6ea8;}
.hint{margin:12px 0 0; font-size:12px; color:rgba(190,220,255,0.55); letter-spacing:1px;}
```

@keyframes pop {from{transform:scale(.85); opacity:0}} and blink.

Media query ≤480px: .panel{padding:20px 22px} h1{font-size:30px} .pill{font-size:12px; padding:8px 12px}.

Now overlay content builder with score.

Also small "controls" hint during play? Could draw tiny hint bottom-left in canvas: "← → / mouse to move" alpha 0.35 at bottom? Bottom area has paddle; draw at y=H-8 centered small: "mouse or ← → to move". Nice. Draw alpha 0.3 font 11px. OK.

Let me also consider the "3 seconds screenshot" — demo mode shows bricks, ball moving, paddle AI, overlay pill. 

Now, one concern: pointermove on window sets mouseX even during demo — then startGame keeps mouseX — fine. But in demo, aiControl overrides mouseX each frame. On start, mouseX = wherever mouse is; paddle lerps there — fine.

Another: after startGame from demo, the demo's bricks state resets — good.

Keyboard during demo: keydown → startGame. Good.

Also handle keydown repeat — fine.

Edge: `keys` is a Set of e.key. On keydown add; keyup delete. Use e.key values 'ArrowLeft' etc.

Now write drawIcon carefully:

```js
function drawIcon(type, cx, cy, s, color){
  ctx.save();
  ctx.strokeStyle=color; ctx.fillStyle=color; ctx.lineWidth=Math.max(1.5,s*0.28); ctx.lineCap='round'; ctx.lineJoin='round';
  if (type==='expand'){
    const y=cy, x0=cx-s, x1=cx+s;
    ctx.beginPath();
    ctx.moveTo(x0,y); ctx.lineTo(x1,y);
    ctx.moveTo(x0,y); ctx.lineTo(x0+s*0.55, y-s*0.55);
    ctx.moveTo(x0,y); ctx.lineTo(x0+s*0.55, y+s*0.55);
    ctx.moveTo(x1,y); ctx.lineTo(x1-s*0.55, y-s*0.55);
    ctx.moveTo(x1,y); ctx.lineTo(x1-s*0.55, y+s*0.55);
    ctx.stroke();
  } else {
    const r=s*0.34;
    ctx.beginPath();
    ctx.arc(cx, cy-s*0.5, r, 0, Math.PI*2);
    ctx.arc(cx-s*0.62, cy+s*0.42, r, 0, Math.PI*2);
    ctx.arc(cx+s*0.62, cy+s*0.42, r, 0, Math.PI*2);
    ctx.fill();
  }
  ctx.restore();
}
```

drawPower capsule:
```js
function drawPower(p){
  const w=p.w,h=p.h,x=p.x-w/2,y=p.y-h/2;
  const col = p.type==='expand' ? '#35f0a0' : '#f45df0';
  ctx.save();
  ctx.shadowColor=col; ctx.shadowBlur=14;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  g.addColorStop(0,'#ffffff'); g.addColorStop(0.25,col); g.addColorStop(1, shade2(col,-0.35));
```
shade works on hex; col is hex — ok: shade(col,-0.35).
```js
  rr(x,y,w,h,h/2); ctx.fillStyle=g; ctx.fill();
  ctx.shadowBlur=0;
  ctx.strokeStyle='rgba(255,255,255,0.85)'; ctx.lineWidth=1.2; rr(x+0.5,y+0.5,w-1,h-1,h/2-0.5); ctx.stroke();
  drawIcon(p.type, p.x, p.y+Math.sin(p.bob)*1.2, 6.5, '#0a1020');
  ctx.restore();
}
```
Icon dark on bright capsule — good contrast. Or white with dark stroke; dark on bright is cleaner.

drawBall, drawPaddle as planned.

drawParticles:
```js
for (const p of particles){
  const a=Math.max(p.life/p.max,0);
  ctx.globalAlpha=a;
  ctx.fillStyle=p.color;
  ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha=1;
```

drawFloaters:
```js
ctx.textAlign='center'; ctx.font='700 13px ...';
for f: alpha=life/max; fillStyle=f.color; fillText(f.text, f.x, f.y);
```

burst:
```js
function burst(x,y,color,n,pow){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2, sp=(90+Math.random()*240)*pow;
    particles.push({x,y,vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-120, size:1.5+Math.random()*2.6, life:0, max:0.45+Math.random()*0.5, color: Math.random()<0.25?'#ffffff':color});
    particles[particles.length-1].life = particles[particles.length-1].max;
  }
}
```
Set life=max at creation.

miniBurst: burst(x,y,color,3,0.4).

addFloater(x,y,text,color): floaters.push({x,y,text,color,vy:-46,life:0.9,max:0.9});

Now constants: 
```js
const W=800,H=600,HUD_H=46,WALL=10;
const FIELD_L=WALL, FIELD_R=W-WALL, FIELD_TOP=HUD_H+WALL; // 56
const ROWS=8, COLS=10, BRICK_H=22, GAP=6, BRICK_MARGIN=14;
const brickW=(FIELD_R-FIELD_L - BRICK_MARGIN*2 - GAP*(COLS-1))/COLS; // (780-28-54)/10=69.8
const BRICK_TOP=FIELD_TOP+16; // 72
```

Paddle: y=H-38=562, h=14, baseW=112, expandW=172.

Ball r=7, baseSpeed=400, maxSpeed=660.

Check brick rows bottom: BRICK_TOP + 8*28 - 6 = 72+224-6=290. Good.

HUD hearts: 3.

Also draw subtle "walls" glow. Fine.

Let me now also double check collidePaddle top-hit condition uses py (position before this substep). If ball moving down fast, per substep movement ≤ 5.6px, paddle h 14 — fine.

One more subtlety: after top bounce we set b.y = paddle.y - b.r; and vy negative. Good.

Ball vs paddle when ball exactly at sides with rel=±1 → angle 65°, vx=±sin65*speed≈0.906*speed, vy=-0.42*speed. Sharp angle. Center → vx=0 straight up. Matches spec.

fixAngle min vertical 0.22*speed ≈ 22% — after wall bounces ball could get shallow angles; fixAngle called after brick collisions only. Also call after paddle side bounce? Add fixAngle in collideBricks and after wall bounce? Wall bounce only flips vx; vy unchanged. If vy tiny (ball nearly horizontal), it could loop horizontally between walls through brick gaps... bricks occupy upper area; horizontal ball at brick rows would hit bricks and get vy flipped maybe. To be safe, call fixAngle(b) after wall collisions too? That changes vy sign? fixAngle preserves sign of vy, adjusts magnitude — safe. Call it after any reflection: walls, bricks. Paddle bounce already fine. I'll call fixAngle after wall x-bounces and brick hits.

Hmm but fixAngle when vy is exactly 0 (perfectly horizontal): dir = vy<0?-1:1 → vy=+minVy (downward) — could push ball downward when it was horizontal moving... if vy==0 moving right, we'd set vy positive (down) — ball then falls — acceptable (prevents infinite horizontal). But if ball moving horizontally upward-ish... vy==0 means horizontal; either sign fine. OK.

Actually careful: fixAngle uses b.speed — ensure b.speed set. Yes.

Now the demo AI maxSp: ball speed 400..660; AI 520. Paddle lerp dt*40 makes paddle track mouseX almost instantly, so effective AI speed = 520. At high ball speeds, angles sharp → horizontal distance large → misses happen. Good.

Also AI should lead the ball slightly: target = ball.x + ball.vx*0.08. Add.

Overlay show for demo at load; also handle resize? Canvas CSS handles.

Write showOverlay with kinds; also hideOverlay sets class 'hidden'.

Also initial: state='demo'; showOverlay('demo'); start loop.

Potential issue: `showOverlay('over')` uses score — fine.

Also add keyboard 'p' pause? Skip.

Let me also ensure no horizontal scrollbar: body overflow hidden. Good.

Now assemble the full file. Also add <title> and meta.

Let me write out everything carefully, checking syntax.

Also note: `ctx.roundRect` — I'm using custom rr, fine.

Text rendering: set textBaseline once per draw.

Let me write the final code.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Neon Breakout</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;height:100%}
  body{
    background:radial-gradient(1100px 700px at 50% 18%, #111b3d 0%, #080d1d 55%, #04060d 100%);
    display:flex;align-items:center;justify-content:center;
    font-family:"Segoe UI",system-ui,-apple-system,"Segoe UI",Arial,sans-serif;
    overflow:hidden;color:#dfe9ff;
    -webkit-user-select:none;user-select:none;
  }
  #wrap{position:relative;width:min(96vw,calc((100vh - 28px)*1.33333),1060px)}
  @supports (width:100dvh){#wrap{width:min(96vw,calc((100dvh - 28px)*1.33333),1060px)}}
  canvas{
    display:block;width:100%;height:auto;aspect-ratio:800/600;
    background:#070b18;border-radius:12px;touch-action:none;
    box-shadow:0 0 0 1px rgba(100,170,255,.16),0 22px 70px rgba(0,0,0,.65),0 0 90px rgba(40,120,255,.12);
  }
  #overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:10;pointer-events:none;border-radius:12px;overflow:hidden}
  #overlay.hidden{display:none}
  #overlay.demo{align-items:flex-start;padding-top:14px;background:linear-gradient(to bottom,rgba(4,8,18,.78),rgba(4,8,18,0) 38%)}
  .pill{display:flex;align-items:center;gap:10px;max-width:92%;flex-wrap:wrap;justify-content:center;
    background:rgba(8,14,30,.88);border:1px solid rgba(96,205,255,.45);color:#cfeaff;
    padding:10px 18px;border-radius:999px;font-size:14px;letter-spacing:.4px;text-align:center;
    box-shadow:0 0 26px rgba(60,170,255,.28),inset 0 0 14px rgba(80,180,255,.08)}
  .pill b{color:#7fe3ff}
  .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#ff4d6d;box-shadow:0 0 10px #ff4d6d;animation:blink 1s infinite}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
  .panel{pointer-events:auto;text-align:center;background:rgba(8,13,28,.85);border:1px solid rgba(110,200,255,.35);
    border-radius:16px;padding:30px 48px;box-shadow:0 0 55px rgba(50,150,255,.28),inset 0 0 30px rgba(60,150,255,.06);
    animation:pop .3s ease}
  @keyframes pop{from{transform:scale(.85);opacity:0}}
  .panel h1{margin:0 0 8px;font-size:44px;letter-spacing:5px;font-weight:800}
  .panel h1.red{background:linear-gradient(180deg,#ffb199,#ff2d55);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 16px rgba(255,45,85,.45))}
  .panel h1.gold{background:linear-gradient(180deg,#fff3b0,#ffb700);-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 16px rgba(255,190,40,.45))}
  .panel .score{font-size:20px;font-weight:700;letter-spacing:2px;color:#ffe08a;margin:4px 0 20px}
  .panel button{cursor:pointer;font:inherit;font-weight:700;font-size:16px;letter-spacing:2px;color:#06121f;
    background:linear-gradient(180deg,#8fe9ff,#38b6ff);border:none;border-radius:10px;padding:13px 28px;
    box-shadow:0 0 24px rgba(80,190,255,.55),0 4px 0 #1a6ea8;transition:transform .08s,box-shadow .2s}
  .panel button:hover{box-shadow:0 0 36px rgba(130,225,255,.85),0 4px 0 #1a6ea8;transform:translateY(-1px)}
  .panel button:active{transform:translateY(2px);box-shadow:0 0 12px rgba(80,190,255,.5),0 1px 0 #1a6ea8}
  .panel .hint{margin:14px 0 0;font-size:12px;letter-spacing:1.5px;color:rgba(190,220,255,.55)}
  @media (max-width:520px){
    .panel{padding:20px 24px}
    .panel h1{font-size:30px;letter-spacing:3px}
    .pill{font-size:12px;padding:8px 14px}
  }
</style>
</head>
<body>
<div id="wrap">
  <canvas id="game" width="800" height="600"></canvas>
  <div id="overlay" class="hidden"><div id="ovBox"></div></div>
</div>
<script>
...
</script>
</body>
</html>
```

Wait: overlay demo class positions pill at top via align-items flex-start — #overlay is flex; .pill is child of #ovBox... I put content into #ovBox. #ovBox should be the flex item; make #ovBox{display:contents}? Then pill aligns per overlay's align-items. Simpler: set overlay content directly: overlay.innerHTML = pill/panel; no ovBox. But re-binding button not needed (delegation on overlay). Let me drop #ovBox and set overlay.innerHTML. Overlay has classes 'demo'/'over'/'win'/'hidden'. For over/win, overlay is flex center with default background? Add slight dim: `#overlay.over,#overlay.win{background:rgba(4,8,18,.45)}`. And pointer-events none except button (panel has pointer-events:auto). But we also start on any window pointerdown, so pointer-events none is fine.

CSS: `#overlay.over,#overlay.win{background:rgba(4,8,18,.5)}`.

JS showOverlay:
```js
const overlay=document.getElementById('overlay');
function showOverlay(kind){
  if(kind==='demo'){
    overlay.className='demo';
    overlay.innerHTML='<div class="pill"><span class="dot"></span><span><b>DEMO</b> — AI is playing &nbsp;·&nbsp; <b>Click or press any key to start</b></span></div>';
  } else if(kind==='over'){
    overlay.className='over';
    overlay.innerHTML='<div class="panel"><h1 class="red">GAME OVER</h1><div class="score">SCORE&nbsp; '+score+'</div><button type="button">&#8635;&nbsp; PLAY AGAIN</button><div class="hint">or press any key</div></div>';
  } else if(kind==='win'){
    overlay.className='win';
    overlay.innerHTML='<div class="panel"><h1 class="gold">&#9733; YOU WIN! &#9733;</h1><div class="score">SCORE&nbsp; '+score+'</div><button type="button">&#8635;&nbsp; PLAY AGAIN</button><div class="hint">or press any key</div></div>';
  } else overlay.className='hidden';
}
```

Now full script. Let me write it linearly and check.

```js
'use strict';
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const W=800,H=600,HUD_H=46,WALL=10;
const FIELD_L=WALL,FIELD_R=W-WALL,FIELD_TOP=HUD_H+WALL;
const DPR=Math.min(window.devicePixelRatio||1,2);
canvas.width=W*DPR;canvas.height=H*DPR;
ctx.setTransform(DPR,0,0,DPR,0,0);

const ROWS=8,COLS=10,BGAP=6,BH=22,BM=14;
const brickW=(FIELD_R-FIELD_L-BM*2-BGAP*(COLS-1))/COLS;
const BTOP=FIELD_TOP+16;

const clamp=(v,a,b)=>v<a?a:v>b?b:v;
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function shade(hex,amt){const n=parseInt(hex.slice(1),16);let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
  if(amt>=0){r+=(255-r)*amt;g+=(255-g)*amt;b+=(255-b)*amt;}else{r*=1+amt;g*=1+amt;b*=1+amt;}
  return 'rgb('+(r|0)+','+(g|0)+','+(b|0)+')';}
```

State vars:
```js
let state='demo';
let score=0,lives=3,ballSpeed=400;
const BASE_SPEED=400,MAX_SPEED=660;
let bricks=[],balls=[],powerups=[],particles=[],floaters=[];
let paddle={x:W/2,y:H-38,w:112,h:14,expandTimer:0};
let mouseX=W/2;
let time=0,shake=0;
const keys=new Set();
```

Stars:
```js
const stars=[];
for(let i=0;i<70;i++)stars.push({x:Math.random()*W,y:Math.random()*H,s:0.6+Math.random()*1.4,sp:6+Math.random()*18,ph:Math.random()*6.28});
```

Row colors:
```js
const rowColors=['#ff4757','#ff7f50','#ffa502','#eccc68','#2ed573','#1e90ff','#3742fa','#a55eea'];
```

buildLevel:
```js
function buildLevel(){
  bricks=[];
  const rng=mulberry32((Math.random()*1e9)>>>0);
  for(let r=0;r<ROWS;r++){
    for(let c=0;c<COLS;c++){
      const x=FIELD_L+BM+c*(brickW+BGAP);
      const y=BTOP+r*(BH+BGAP);
      let type='normal';
      if(r===0||r===1)type='tough';
      else if(r===3&&c%4===1)type='tough';
      else if(r===5&&c%3===0)type='tough';
      const g=ctx.createLinearGradient(0,y,0,y+BH);
      if(type==='tough'){g.addColorStop(0,'#d6dee9');g.addColorStop(0.5,'#8b98ab');g.addColorStop(1,'#57637a');}
      else{const col=rowColors[r];g.addColorStop(0,shade(col,0.38));g.addColorStop(0.55,col);g.addColorStop(1,shade(col,-0.4));}
      bricks.push({x,y,w:brickW,h:BH,type,row:r,hp:type==='tough'?2:1,maxHp:type==='tough'?2:1,
        alive:true,cracked:false,power:null,grad:g,cracks:makeCracks(rng)});
    }
  }
  const order=bricks.map((_,i)=>i);
  for(let i=order.length-1;i>0;i--){const j=(rng()*(i+1))|0;const t=order[i];order[i]=order[j];order[j]=t;}
  for(let k=0;k<10;k++) bricks[order[k]].power=(k%2===0)?'expand':'multiball';
}
function makeCracks(rng){
  const w=brickW,h=BH,lines=[];
  for(let i=0;i<3;i++){
    let x=w*(0.2+0.6*rng()),y=h*(0.15+0.7*rng());
    const pts=[[x,y]];let ang=rng()*6.28;
    for(let s=0;s<4;s++){ang+=(rng()-0.5)*1.8;x=clamp(x+Math.cos(ang)*w*0.18,2,w-2);y=clamp(y+Math.sin(ang)*h*0.34,2,h-2);pts.push([x,y]);}
    lines.push(pts);
  }
  return lines;
}
```

respawnBall, startGame, showOverlay as planned.

Audio:
```js
let AC=null;
function ensureAudio(){if(!AC){try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){AC=null;}}if(AC&&AC.state==='suspended'){AC.resume().catch(()=>{});}}
function beep(freq,dur,type,vol,when){if(!AC)return;try{const t=AC.currentTime+(when||0);const o=AC.createOscillator(),g=AC.createGain();o.type=type||'square';o.frequency.setValueAtTime(freq,t);g.gain.setValueAtTime(vol||0.05,t);g.gain.exponentialRampToValueAtTime(0.0001,t+dur);o.connect(g);g.connect(AC.destination);o.start(t);o.stop(t+dur+0.03);}catch(e){}}
```

Input:
```js
window.addEventListener('pointermove',e=>{const r=canvas.getBoundingClientRect();mouseX=(e.clientX-r.left)*(W/r.width);});
window.addEventListener('pointerdown',e=>{
  ensureAudio();
  const r=canvas.getBoundingClientRect();mouseX=(e.clientX-r.left)*(W/r.width);
  if(state!=='playing'){startGame();}
  else{for(const b of balls)if(b.stuck)b.timer=0;}
});
overlay.addEventListener('pointerdown',e=>{ensureAudio();if(state!=='playing')startGame();});
```
Hmm window pointerdown already fires (bubbles from overlay). Just window listener enough. But overlay has pointer-events:none so events target canvas/wrap — still bubble to window. Keep single window listener.

keydown:
```js
window.addEventListener('keydown',e=>{
  if(e.key.indexOf('Arrow')===0||e.key===' '){e.preventDefault();}
  ensureAudio();
  keys.add(e.key);
  if(state!=='playing')startGame();
});
window.addEventListener('keyup',e=>keys.delete(e.key));
window.addEventListener('blur',()=>keys.clear());
```

startGame:
```js
function startGame(){
  if(state==='playing')return;
  state='playing';
  score=0;lives=3;ballSpeed=BASE_SPEED;
  paddle.x=W/2;paddle.w=112;paddle.expandTimer=0;mouseX=W/2;
  powerups.length=0;particles.length=0;floaters.length=0;
  buildLevel();
  respawnBall();
  showOverlay('hidden');
}
function respawnBall(){
  balls=[{x:paddle.x,y:paddle.y-9,r:7,vx:0,vy:0,speed:ballSpeed,stuck:true,timer:state==='demo'?0.7:1.0,trail:[]}];
  paddle.expandTimer=0;
}
```

aiControl:
```js
function aiControl(dt){
  let target=W/2;
  let lowest=null;
  for(const b of balls){if(!b.stuck&&(!lowest||b.y>lowest.y))lowest=b;}
  if(lowest)target=lowest.x+lowest.vx*0.08+Math.sin(time*2.3)*6;
  else if(powerups.length)target=powerups[0].x;
  if(powerups.length&&powerups[0].y>240)target=powerups[0].x;
  const maxSp=520,d=target-mouseX;
  mouseX+=clamp(d,-maxSp*dt,maxSp*dt);
}
```

Update as planned. collideBricks, collidePaddle, hitBrick, applyPower, dropPower, burst, addFloater, fixAngle, setSpeed.

hitBrick:
```js
function hitBrick(br){
  br.hp--;
  if(br.hp<=0){
    br.alive=false;
    const pts=(br.type==='tough'?150:100)+(ROWS-1-br.row)*10;
    score+=pts;
    addFloater(br.x+br.w/2,br.y+br.h/2,'+'+pts,br.type==='tough'?'#ffd32a':'#eaf6ff');
    burst(br.x+br.w/2,br.y+br.h/2,br.type==='tough'?'#9fb2c8':rowColors[br.row],18,1);
    shake=Math.min(shake+3,5);
    if(br.power)dropPower(br);
    ballSpeed=Math.min(MAX_SPEED,ballSpeed+5);
    for(const b of balls)if(!b.stuck)setSpeed(b,ballSpeed);
    beep(500+Math.random()*180,0.08,'square',0.045);
  }else{
    br.cracked=true;
    score+=25;
    addFloater(br.x+br.w/2,br.y+br.h/2,'+25','#9aa7bd');
    burst(br.x+br.w/2,br.y+br.h/2,'#8b98ab',7,0.6);
    beep(280,0.07,'sawtooth',0.05);
  }
}
```

setSpeed:
```js
function setSpeed(b,s){const m=Math.hypot(b.vx,b.vy)||1;b.vx=b.vx/m*s;b.vy=b.vy/m*s;b.speed=s;}
function fixAngle(b){const minVy=b.speed*0.22;if(Math.abs(b.vy)<minVy){const dir=b.vy<0?-1:1;const vx=Math.sqrt(Math.max(b.speed*b.speed-minVy*minVy,0));b.vy=dir*minVy;b.vx=(b.vx<0?-1:1)*vx;}}
```

collideBricks / collidePaddle as planned.

applyPower:
```js
function applyPower(p){
  score+=25;
  if(p.type==='expand'){
    paddle.expandTimer=10;
    addFloater(paddle.x,paddle.y-26,'WIDE PADDLE 10s','#3dff9c');
    beep(620,0.09,'sine',0.06);beep(930,0.12,'sine',0.06,0.08);
  }else{
    if(balls.length<3){
      const q=[-0.55,0.55,0];
      while(balls.length<3){
        const a=q.shift();
        const b={x:paddle.x,y:paddle.y-10,r:7,speed:ballSpeed,stuck:false,timer:0,trail:[],
          vx:ballSpeed*Math.sin(a),vy:-ballSpeed*Math.cos(a)};
        balls.push(b);
      }
      addFloater(paddle.x,paddle.y-26,'MULTI-BALL x3','#f45df0');
      beep(520,0.08,'triangle',0.06);beep(780,0.08,'triangle',0.06,0.07);beep(1040,0.12,'triangle',0.06,0.14);
    }else{
      score+=150;
      addFloater(paddle.x,paddle.y-26,'+150  MAX BALLS','#ffd32a');
      beep(880,0.1,'triangle',0.06);
    }
  }
}
```

dropPower:
```js
function dropPower(br){powerups.push({x:br.x+br.w/2,y:br.y+br.h/2,vy:150,w:40,h:18,type:br.power,bob:Math.random()*6.28});}
```

Render functions... then loop.

drawHUD:
```js
function drawHUD(){
  const g=ctx.createLinearGradient(0,0,0,HUD_H);
  g.addColorStop(0,'#0d1530');g.addColorStop(1,'#0a0f22');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,HUD_H);
  ctx.fillStyle='rgba(96,200,255,0.28)';ctx.fillRect(0,HUD_H-1,W,1);
  ctx.textBaseline='middle';
  ctx.textAlign='left';
  ctx.font='700 15px "Segoe UI",system-ui,Arial,sans-serif';
  ctx.fillStyle='#9fdcff';
  ctx.fillText('SCORE '+String(score).padStart(6,'0'),16,HUD_H/2+1);
  if(paddle.expandTimer>0){
    ctx.fillStyle='#3dff9c';
    ctx.fillText('WIDE '+paddle.expandTimer.toFixed(1)+'s',170,HUD_H/2+1);
  }
  ctx.textAlign='center';
  ctx.font='600 10px "Segoe UI",system-ui,Arial,sans-serif';
  ctx.fillStyle='rgba(150,200,255,0.45)';
  ctx.fillText('N E O N   B R E A K O U T',W/2,HUD_H/2+1);
  // hearts
  for(let i=0;i<3;i++){
    const x=W-22-i*26,y=HUD_H/2+1;
    drawHeart(x,y,11,i<lives);
  }
}
function drawHeart(cx,cy,s,filled){
  ctx.beginPath();
  ctx.moveTo(cx,cy+s*0.9);
  ctx.bezierCurveTo(cx-s*1.1,cy+s*0.1,cx-s*0.55,cy-s*0.85,cx,cy-s*0.25);
  ctx.bezierCurveTo(cx+s*0.55,cy-s*0.85,cx+s*1.1,cy+s*0.1,cx,cy+s*0.9);
  ctx.closePath();
  if(filled){ctx.save();ctx.shadowColor='#ff4d6d';ctx.shadowBlur=8;ctx.fillStyle='#ff4d6d';ctx.fill();ctx.restore();}
  else{ctx.strokeStyle='rgba(255,77,109,0.3)';ctx.lineWidth=1.4;ctx.stroke();}
}
```
Heart path check: start bottom (cy+0.9s), curve to left... produces heart-ish. Let me verify shape roughly: moveTo bottom point; bezier with control points left up, to center dip (cx, cy-0.25s); then mirror to bottom. The lobes: control (cx-1.1s, cy+0.1s) and (cx-0.55s, cy-0.85s) ending (cx, cy-0.25s). This draws left half: from bottom going up-left then to dip. Should look heart-like. Good enough.

Also bottom hint text:
```js
ctx.textAlign='center';ctx.font='11px ...';ctx.fillStyle='rgba(150,190,255,0.35)';
ctx.fillText('mouse or  ← →  to move', W/2, H-10);
```
Arrows glyphs ← → should render in most fonts. Fine.

drawWalls:
```js
const wallGrad=ctx.createLinearGradient(0,HUD_H,0,H);
wallGrad.addColorStop(0,'#1b2b4d');wallGrad.addColorStop(1,'#101a33');
function drawWalls(){
  ctx.fillStyle=wallGrad;
  ctx.fillRect(0,HUD_H,WALL,H-HUD_H);
  ctx.fillRect(W-WALL,HUD_H,WALL,H-HUD_H);
  ctx.fillRect(0,HUD_H,W,WALL);
  ctx.save();
  ctx.shadowColor='rgba(90,200,255,0.9)';ctx.shadowBlur=8;
  ctx.fillStyle='#7fe3ff';
  ctx.fillRect(WALL-2,HUD_H,2,H-HUD_H);
  ctx.fillRect(W-WALL,HUD_H,2,H-HUD_H);
  ctx.fillRect(0,HUD_H+WALL-2,W,2);
  ctx.restore();
}
```
Ball bounds use FIELD_L=WALL=10 → ball center min 10+7=17, wall inner edge at 10, glow line at 8..10 — ball doesn't overlap line. Good. Top: FIELD_TOP=56, wall strip 46..56, glow at 54..56. Ball min y = 56+7=63. Good.

Background grid:
```js
function drawGrid(){
  ctx.strokeStyle='rgba(90,140,255,0.05)';ctx.lineWidth=1;
  ctx.beginPath();
  for(let x=FIELD_L;x<=FIELD_R;x+=40){ctx.moveTo(x,HUD_H+WALL);ctx.lineTo(x,H);}
  for(let y=HUD_H+WALL;y<H;y+=40){ctx.moveTo(0,y);ctx.lineTo(W,y);}
  ctx.stroke();
}
```

Stars draw:
```js
function drawStars(){
  ctx.save();
  for(const s of stars){
    const a=0.12+0.18*(0.5+0.5*Math.sin(time*2+s.ph));
    ctx.globalAlpha=a;
    ctx.fillStyle='#8fc7ff';
    ctx.fillRect(s.x,s.y,s.s,s.s);
  }
  ctx.restore();
}
```
globalAlpha reset by save/restore. Good.

bgGrad cached:
```js
const bgGrad=ctx.createLinearGradient(0,0,0,H);
bgGrad.addColorStop(0,'#0a1128');bgGrad.addColorStop(0.55,'#070b18');bgGrad.addColorStop(1,'#04060d');
```

drawBrick as planned; note shadow per brick — 80 bricks × shadow might be okay. Alternatively skip shadow. Keep small shadowBlur 3.

Actually shadow on 80 filled paths per frame — Chrome handles fine.

drawPaddle:
```js
function drawPaddle(){
  const w=paddle.w,h=paddle.h,x=paddle.x-w/2,y=paddle.y;
  const ex=paddle.expandTimer>0;
  ctx.save();
  ctx.shadowColor=ex?'rgba(61,255,156,0.9)':'rgba(77,201,255,0.9)';
  ctx.shadowBlur=16;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  if(ex){g.addColorStop(0,'#7dffc0');g.addColorStop(1,'#17b862');}
  else{g.addColorStop(0,'#8fe9ff');g.addColorStop(1,'#1e90ff');}
  rr(x,y,w,h,h/2);ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  ctx.fillStyle='rgba(255,255,255,0.55)';
  rr(x+3,y+2,w-6,3.5,1.8);ctx.fill();
  ctx.restore();
}
```

drawBall with trail:
```js
function drawBall(b){
  for(let i=0;i<b.trail.length;i++){
    const t=b.trail[i],f=(i+1)/b.trail.length;
    ctx.globalAlpha=f*0.22;
    ctx.fillStyle='#8fd8ff';
    ctx.beginPath();ctx.arc(t.x,t.y,b.r*(0.35+0.65*f),0,6.29);ctx.fill();
  }
  ctx.globalAlpha=1;
  ctx.save();
  ctx.shadowColor='#bfeaff';ctx.shadowBlur=14;
  const g=ctx.createRadialGradient(b.x-2,b.y-2,1,b.x,b.y,b.r);
  g.addColorStop(0,'#ffffff');g.addColorStop(1,'#9fdcff');
  ctx.fillStyle=g;
  ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,6.29);ctx.fill();
  ctx.restore();
}
```

drawPower, drawParticles, drawFloaters as planned.

render:
```js
function render(){
  ctx.save();
  if(shake>0.15)ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
  ctx.fillStyle=bgGrad;ctx.fillRect(-8,-8,W+16,H+16);
  drawStars();drawGrid();drawWalls();
  for(const br of bricks)if(br.alive)drawBrick(br);
  for(const p of powerups)drawPower(p);
  drawParticles();
  drawPaddle();
  for(const b of balls)drawBall(b);
  drawFloaters();
  ctx.restore();
  drawHUD();
  // bottom hint
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='11px "Segoe UI",system-ui,Arial,sans-serif';
  ctx.fillStyle='rgba(150,190,255,0.32)';
  ctx.fillText('move: mouse or ← → arrow keys',W/2,H-9);
}
```

Loop:
```js
let last=performance.now();
function frame(now){
  let dt=(now-last)/1000;last=now;
  if(dt>0.05)dt=0.05;
  if(dt>0)update(dt);
  render();
  requestAnimationFrame(frame);
}
```

Init:
```js
buildLevel();
respawnBall();
showOverlay('demo');
requestAnimationFrame(frame);
```

respawnBall uses state — state='demo' initially set before. Order: declare state='demo' at top. Good.

Update function full — write carefully:

```js
function update(dt){
  time+=dt;
  for(const s of stars){s.y+=s.sp*dt;if(s.y>H){s.y=-2;s.x=Math.random()*W;}}
  if(state==='demo')aiControl(dt);
  else{
    let dir=0;
    if(keys.has('ArrowLeft')||keys.has('a')||keys.has('A'))dir--;
    if(keys.has('ArrowRight')||keys.has('d')||keys.has('D'))dir++;
    if(dir)mouseX+=dir*760*dt;
  }
  if(paddle.expandTimer>0)paddle.expandTimer=Math.max(0,paddle.expandTimer-dt);
  const targetW=paddle.expandTimer>0?172:112;
  paddle.w+=(targetW-paddle.w)*Math.min(1,dt*12);
  const minX=FIELD_L+paddle.w/2+1,maxX=FIELD_R-paddle.w/2-1;
  mouseX=clamp(mouseX,minX,maxX);
  paddle.x+=(mouseX-paddle.x)*Math.min(1,dt*40);
  paddle.x=clamp(paddle.x,minX,maxX);

  for(let i=powerups.length-1;i>=0;i--){
    const p=powerups[i];
    p.y+=p.vy*dt;p.bob+=dt*5;
    if(p.y+p.h/2>=paddle.y&&p.y-p.h/2<=paddle.y+paddle.h&&Math.abs(p.x-paddle.x)<=paddle.w/2+p.w/2){
      applyPower(p);powerups.splice(i,1);continue;
    }
    if(p.y-p.h>H)powerups.splice(i,1);
  }

  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    if(b.stuck){
      b.x=paddle.x;b.y=paddle.y-b.r-1;b.trail.length=0;
      b.timer-=dt;
      if(b.timer<=0){b.stuck=false;const a=(Math.random()-0.5)*0.5;b.vx=b.speed*Math.sin(a);b.vy=-b.speed*Math.cos(a);}
    }else{
      const dist=b.speed*dt;
      const steps=Math.max(1,Math.ceil(dist/(b.r*0.8)));
      const sdt=dt/steps;
      for(let s=0;s<steps;s++){
        const px=b.x,py=b.y;
        b.x+=b.vx*sdt;b.y+=b.vy*sdt;
        if(b.x<FIELD_L+b.r){b.x=FIELD_L+b.r;b.vx=Math.abs(b.vx);fixAngle(b);}
        else if(b.x>FIELD_R-b.r){b.x=FIELD_R-b.r;b.vx=-Math.abs(b.vx);fixAngle(b);}
        if(b.y<FIELD_TOP+b.r){b.y=FIELD_TOP+b.r;b.vy=Math.abs(b.vy);}
        collideBricks(b,px,py);
        collidePaddle(b,px,py);
      }
      b.trail.push({x:b.x,y:b.y});
      if(b.trail.length>10)b.trail.shift();
    }
    if(b.y-b.r>H)balls.splice(i,1);
  }

  if(balls.length===0&&(state==='playing'||state==='demo')){
    if(state==='demo')respawnBall();
    else{
      lives--;
      if(lives<=0){state='over';showOverlay('over');beep(320,0.15,'sawtooth',0.06);beep(210,0.2,'sawtooth',0.06,0.13);beep(120,0.4,'sawtooth',0.06,0.28);}
      else{respawnBall();beep(180,0.18,'sawtooth',0.06);}
    }
  }

  if(bricks.length>0&&bricks.every(b=>!b.alive)){
    if(state==='demo')buildLevel();
    else if(state==='playing'){
      state='win';showOverlay('win');
      beep(523,0.1,'triangle',0.06);beep(659,0.1,'triangle',0.06,0.1);beep(784,0.1,'triangle',0.06,0.2);beep(1047,0.25,'triangle',0.07,0.3);
    }
  }

  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];p.life-=dt;
    if(p.life<=0){particles.splice(i,1);continue;}
    p.vy+=700*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;
  }
  for(let i=floaters.length-1;i>=0;i--){
    const f=floaters[i];f.life-=dt;f.y+=f.vy*dt;
    if(f.life<=0)floaters.splice(i,1);
  }
  shake=Math.max(0,shake-dt*16);
}
```

Wait: fixAngle after wall x-bounce — if ball moving horizontally (vy≈0) bouncing off side wall, fixAngle sets vy to ±minVy — sign based on current vy sign (0 → +, downward). Could cause ball to start descending after side bounce — acceptable, prevents horizontal lock. But careful: fixAngle when vy=0 and ball at top region moving right: after right wall bounce, vy set +minVy (down) — fine.

But hmm: fixAngle called on every wall bounce even when vy large — no-op then. OK.

collideBricks:
```js
function collideBricks(b,px,py){
  for(let i=0;i<bricks.length;i++){
    const br=bricks[i];
    if(!br.alive)continue;
    if(b.x+b.r<br.x||b.x-b.r>br.x+br.w||b.y+b.r<br.y||b.y-b.r>br.y+br.h)continue;
    const cx=clamp(b.x,br.x,br.x+br.w),cy=clamp(b.y,br.y,br.y+br.h);
    const dx=b.x-cx,dy=b.y-cy;
    if(dx*dx+dy*dy>b.r*b.r)continue;
    let nx=0,ny=0;
    if(py+b.r<=br.y+0.01)ny=-1;
    else if(py-b.r>=br.y+br.h-0.01)ny=1;
    else if(px+b.r<=br.x+0.01)nx=-1;
    else if(px-b.r>=br.x+br.w-0.01)nx=1;
    else{
      const ol=(b.x+b.r)-br.x,orr=(br.x+br.w)-(b.x-b.r);
      const ot=(b.y+b.r)-br.y,ob=(br.y+br.h)-(b.y-b.r);
      if(Math.min(ol,orr)<Math.min(ot,ob))nx=ol<orr?-1:1;else ny=ot<ob?-1:1;
    }
    if(nx!==0){b.vx=(nx<0?-1:1)*Math.abs(b.vx);b.x=nx<0?br.x-b.r:br.x+br.w+b.r;}
    else{b.vy=(ny<0?-1:1)*Math.abs(b.vy);b.y=ny<0?br.y-b.r:br.y+br.h+b.r;}
    fixAngle(b);
    hitBrick(br);
    return;
  }
}
```

collidePaddle:
```js
function collidePaddle(b,px,py){
  const half=paddle.w/2;
  if(b.y+b.r<paddle.y||b.y-b.r>paddle.y+paddle.h)return;
  if(Math.abs(b.x-paddle.x)>half+b.r)return;
  if(b.vy>0&&py+b.r<=paddle.y+0.01){
    const rel=clamp((b.x-paddle.x)/half,-1,1);
    const ang=rel*Math.PI*0.36;
    b.vx=b.speed*Math.sin(ang);
    b.vy=-b.speed*Math.cos(ang);
    b.y=paddle.y-b.r;
    burst(b.x,paddle.y,'#8fd8ff',4,0.35);
    beep(200+Math.abs(rel)*120,0.05,'square',0.05);
  }else if(b.vy>0){
    const side=px<paddle.x?-1:1;
    b.vx=side*Math.abs(b.vx);
    b.x=paddle.x+side*(half+b.r);
  }else{
    b.y=paddle.y-b.r;
    b.vy=-Math.abs(b.vy);
  }
}
```

Note: burst on every paddle hit creates particles each substep potentially — only when collision detected once per substep; but ball resting on paddle... after bounce vy<0 so no repeat. Fine. Maybe throttle: only burst if Math.random()<0.5. Fine as is (4 particles).

Edge: ball stuck inside paddle region when paddle expands under ball — expand width grows, ball above paddle — no overlap since ball y < paddle.y - r normally. OK.

One more: when multiball spawns balls at paddle.y-10 with vy negative — they're above paddle, fine.

Also when ball lost and respawn, trail cleared.

Now check "ball must never pass through paddle": substep 5.6px vs paddle height 14 — the overlap check `b.y+b.r>=paddle.y && b.y-b.r<=paddle.y+paddle.h` catches any overlap; since per-substep movement < paddle height, cannot jump over. Sides: horizontal movement per substep 5.6 < ... side detection uses px comparison; if ball moves fast horizontally into paddle side while overlapping vertically — handled by side branch. Could ball pass through paddle horizontally in one substep? Paddle half-width ~56 + r → overlap window ~126px wide; movement 5.6px per substep — cannot skip. Good.

Bricks: height 22, width ~69.8; movement per substep ≤ 0.8*r=5.6 — cannot tunnel. 

Now HUD score padStart — score could exceed 999999? unlikely; padStart fine.

Check drawIcon for brick power icons: s=7 within brick h=22 — ok.

drawPower capsule icon s=6.5 within h=18 — expand arrow width 13 < 40 ok.

Potential visual: powerup bricks — icon white on colored brick; tough bricks gray — icon visible. Good.

Now overlay demo pill at top may cover top bricks? It's at top over HUD area mostly; gradient background covers top ~38%. Bricks start y=72; pill height ~40 + padding 14 → covers to ~54+ some. Slight overlap with top bricks possibly; semi-transparent, fine — it's a demo banner. Maybe make demo overlay background lighter: rgba(4,8,18,.6) to 30%. OK.

Also during demo, HUD shows score increasing — nice.

Now double-check the win overlay appears only in playing. In demo, bricks rebuilt. Good.

Check startGame guard: called from keydown when state 'over' → resets. Good. But note: keydown handler calls startGame even when state==='playing'? Guard inside startGame returns. But we also add key to keys — fine.

One issue: after game over, balls array empty; update skips ball processing; render draws nothing for balls. Overlay shown. Particles continue. Good. In 'over' state, paddle input: state!=='demo' so keyboard moves mouseX — harmless.

Also in 'over'/'win', aiControl not running (state!=='demo') — paddle stays. Fine.

pointerdown during 'over' → startGame → new game. 

Now check: `overlay` variable used in listener — define after DOM (script at end of body). Good.

Also ensure no console errors: `ctx.ellipse`? not used. `padStart` fine. Optional chaining avoided for safety? Modern Chrome fine.

`aspect-ratio` CSS with width 100% and height auto: canvas has intrinsic ratio from width/height attrs (800*dpr x 600*dpr — ratio same 4/3). Actually canvas attribute width set to 800*DPR (e.g., 1600) height 1200 — intrinsic ratio 4/3, so height:auto works even without aspect-ratio. I set both. Good.

Font in canvas: '700 15px "Segoe UI",system-ui,Arial,sans-serif' — fine.

One more requirement check: "paddle controlled with mouse and left/right arrow keys" ✓. "bounce angle depends on hit position" ✓. "at least 6 rows" — 8 rows ✓ mixing normal & 2-hit tough that crack ✓. "some bricks visibly carry power-up icon; drop when broken; catch → wider paddle 10s or multiball 3 balls" ✓. "ball never passes through bricks/paddle" ✓ substeps. "particles when brick breaks" ✓. "3 lives, score displayed" ✓. "game over & victory screens with restart" ✓. Demo mode with message ✓. 1280×800 & 375px ✓. No external requests ✓. No localStorage etc ✓.

Screenshot at 3s: demo running — ball launched after 0.7s, bouncing, maybe some bricks broken already, particles. 

Let me also double check `respawnBall` referenced before definition in init — function declarations hoisted. `buildLevel` hoisted. `showOverlay` hoisted. State vars declared with let before use at runtime — init code at bottom after all declarations. Order in script: constants, helpers, state, build functions (function decls), input listeners, init calls, loop start. Make sure `state='demo'` set before respawnBall called at init (respawnBall reads state for timer). Declare `let state='demo';` early. Good.

Also `paddle` object defined before respawnBall uses paddle — yes.

Edge: In demo, if user never interacts, lives never decrease (respawn instead) — HUD hearts stay 3. Fine.

Also during demo, score accumulates; on start reset to 0. Good.

Now, the demo AI: mouseX controlled; but user pointermove also sets mouseX — during demo aiControl overwrites each frame after pointermove? Order: pointermove event sets mouseX; next frame update→aiControl overrides. Fine.

Potential problem: on touch devices, pointermove during drag sets mouseX; ok.

Write drawBrick fully:

```js
function drawBrick(br){
  ctx.save();
  ctx.shadowColor='rgba(0,0,0,0.45)';ctx.shadowBlur=3;ctx.shadowOffsetY=2;
  rr(br.x,br.y,br.w,br.h,5);
  ctx.fillStyle=br.grad;ctx.fill();
  ctx.shadowColor='rgba(0,0,0,0)';ctx.shadowBlur=0;ctx.shadowOffsetY=0;
  ctx.lineWidth=1;
  ctx.strokeStyle=br.type==='tough'?'rgba(28,36,50,0.9)':'rgba(0,0,0,0.3)';
  rr(br.x+0.5,br.y+0.5,br.w-1,br.h-1,5);ctx.stroke();
  if(br.type==='tough'){
    ctx.strokeStyle='rgba(255,255,255,0.3)';
    rr(br.x+2.5,br.y+2.5,br.w-5,br.h-5,3);ctx.stroke();
    ctx.fillStyle='rgba(22,28,40,0.85)';
    const o=5;
    ctx.beginPath();ctx.arc(br.x+o,br.y+o,1.7,0,6.29);ctx.arc(br.x+br.w-o,br.y+o,1.7,0,6.29);ctx.arc(br.x+o,br.y+br.h-o,1.7,0,6.29);ctx.arc(br.x+br.w-o,br.y+br.h-o,1.7,0,6.29);ctx.fill();
    if(br.cracked){
      ctx.fillStyle='rgba(8,12,20,0.3)';
      rr(br.x,br.y,br.w,br.h,5);ctx.fill();
      ctx.strokeStyle='rgba(12,16,24,0.9)';ctx.lineWidth=1.4;
      for(const line of br.cracks){
        ctx.beginPath();
        ctx.moveTo(br.x+line[0][0],br.y+line[0][1]);
        for(let i=1;i<line.length;i++)ctx.lineTo(br.x+line[i][0],br.y+line[i][1]);
        ctx.stroke();
      }
    }
  }else{
    ctx.fillStyle='rgba(255,255,255,0.3)';
    rr(br.x+2,br.y+2,br.w-4,4.5,2.2);ctx.fill();
  }
  if(br.power){
    ctx.save();
    ctx.shadowColor='rgba(255,255,255,0.95)';ctx.shadowBlur=7;
    drawIcon(br.power,br.x+br.w/2,br.y+br.h/2,6.5,'rgba(255,255,255,0.95)');
    ctx.restore();
  }
  ctx.restore();
}
```

rr must begin path — yes.

drawPower:
```js
function drawPower(p){
  const w=p.w,h=p.h,x=p.x-w/2,y=p.y-h/2;
  const col=p.type==='expand'?'#35f0a0':'#f45df0';
  ctx.save();
  ctx.shadowColor=col;ctx.shadowBlur=13;
  const g=ctx.createLinearGradient(0,y,0,y+h);
  g.addColorStop(0,'#ffffff');g.addColorStop(0.35,col);g.addColorStop(1,shade(col,-0.45));
  rr(x,y,w,h,h/2);ctx.fillStyle=g;ctx.fill();
  ctx.shadowBlur=0;
  ctx.strokeStyle='rgba(255,255,255,0.8)';ctx.lineWidth=1.2;
  rr(x+0.6,y+0.6,w-1.2,h-1.2,h/2-0.6);ctx.stroke();
  drawIcon(p.type,p.x,p.y+Math.sin(p.bob)*1.2,6.2,'#0a1020');
  ctx.restore();
}
```

drawParticles/drawFloaters:
```js
function drawParticles(){
  for(const p of particles){
    ctx.globalAlpha=Math.max(p.life/p.max,0);
    ctx.fillStyle=p.color;
    ctx.beginPath();ctx.arc(p.x,p.y,p.size,0,6.29);ctx.fill();
  }
  ctx.globalAlpha=1;
}
function drawFloaters(){
  ctx.textAlign='center';ctx.textBaseline='middle';
  ctx.font='700 13px "Segoe UI",system-ui,Arial,sans-serif';
  for(const f of floaters){
    ctx.globalAlpha=Math.max(f.life/f.max,0);
    ctx.fillStyle=f.color;
    ctx.fillText(f.text,f.x,f.y);
  }
  ctx.globalAlpha=1;
}
```

burst:
```js
function burst(x,y,color,n,pow){
  for(let i=0;i<n;i++){
    const a=Math.random()*6.283,sp=(90+Math.random()*240)*pow;
    const max=0.45+Math.random()*0.5;
    particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-120,size:1.4+Math.random()*2.6,life:max,max,color:Math.random()<0.25?'#ffffff':color});
  }
}
function addFloater(x,y,text,color){floaters.push({x,y,text,color,vy:-46,life:0.9,max:0.9});}
```

Everything seems consistent. Now check update's brick-clear check: `bricks.every(b=>!b.alive)` — when state 'over' and bricks cleared? In over, balls empty, no hits, bricks remain. Fine. Also guard `state==='playing'` for win — done via else-if.

But note: if state==='over' and somehow all bricks dead (impossible without balls) — skip.

Also in demo when all bricks cleared → buildLevel() — new bricks with new powerups; score keeps. Good.

Check hitBrick ballSpeed increase applies to stuck balls too? `if(!b.stuck)` — stuck ball speed set at respawn from ballSpeed anyway. Fine.

Now the initial overlay: showOverlay('demo') sets class 'demo' — CSS `#overlay.demo` styles. But base `#overlay` has display flex; hidden class display none. className='demo' replaces 'hidden'. Good.

One more: pointer-events none on overlay means button clicks pass through to window pointerdown? pointer-events:none elements don't receive events; events go to element below (canvas/wrap) and bubble to window — window listener fires. So button click works via window handler even without pointer-events:auto. But keep pointer-events:auto on .panel anyway (harmless; then button receives pointerdown which bubbles to window too). Either way works.

Also add cursor pointer on canvas? not needed.

Prevent context menu on long press? add `canvas.addEventListener('contextmenu',e=>e.preventDefault());` optional — skip.

iOS Safari `webkitAudioContext` fallback included.

Now double-check CSS min() with calc: `width:min(96vw, calc((100vh - 28px)*1.33333), 1060px);` valid.

At 1280×800: (800-28)*1.33333=1029.3; min(1228.8, 1029.3, 1060)=1029.3 → height 772 ≤ 800 ✓ (with 28px total margin, centered). Good.

At 375×667: min(360, 845, 1060)=360 → height 270. Centered vertically. Good, no scrollbars (body overflow hidden anyway).

Now, "screenshot taken about 3 seconds after load" — demo visible. Ball launched at 0.7s. Good.

Let me also consider performance: shadowBlur on many elements each frame — bricks 80 × shadowBlur 3 — okay; ball glow; paddle glow; fine on Chrome.

Potential subtle bug: In update, when state==='over' or 'win', we still process balls (none) and powerups falling — fine. aiControl not called; keyboard moves mouseX (dir) — paddle moves behind overlay — harmless.

Another subtle: keydown ' ' (space) preventDefault — page doesn't scroll anyway.

Also when state==='demo' and user presses arrow key — startGame immediately; also keys.add — paddle moves. Good.

Now let me also add a small "level cleared" behavior in demo: buildLevel resets bricks but powerups array may still have falling ones — fine.

Check hitBrick when ball hits two bricks in same substep — we return after first. Second brick next substep. Fine.

Check that ball doesn't get stuck inside brick after position set: we set b.x/b.y to boundary outside brick. Good.

fixAngle after brick hit: if ball was moving mostly horizontally and hit brick side (nx), vy unchanged (maybe small) — fixAngle adjusts vy to min — good, prevents horizontal lock.

But fixAngle could make ball move downward when it should bounce up? It preserves sign of vy. If vy>0 small (moving down) and ball hits brick top... wait if moving down (vy>0) and hits brick top, we set ny=-1 → vy=-|vy| (up). fixAngle: |vy| now large. OK.

If ball hits brick side while moving down slowly: nx set, vy stays positive small → fixAngle keeps positive, magnitude raised to minVy — still moving down. Fine.

Now, one more requirement nuance: "tougher bricks that need 2 hits and visibly crack after the first one" — cracked rendering ✓ (cracks drawn). Also maybe change color slightly when cracked — we add dark overlay ✓.

"Some bricks visibly carry a power-up icon" — icon drawn on brick ✓.

I think we're good. Let me also handle the case where user clicks during demo — pointerdown sets mouseX from click position and starts game; paddle jumps to click x — fine.

Write final HTML now, carefully, complete.

Also add `<meta name="viewport">` done.

Let me write the complete script in order:

1. canvas/ctx/DPR
2. constants
3. helpers (clamp, mulberry32, shade, rr)
4. state vars, stars, rowColors, gradients (bgGrad, wallGrad)
5. buildLevel, makeCracks
6. respawnBall, startGame, showOverlay
7. audio (AC, ensureAudio, beep)
8. input listeners
9. aiControl
10. update, collideBricks, collidePaddle, hitBrick, applyPower, dropPower, burst, addFloater, setSpeed, fixAngle
11. draw functions
12. render
13. frame loop + init

Careful: functions used in listeners defined before listeners attach (hoisting covers function declarations).

Also `overlay` const defined before showOverlay usage at init.

Let me now also make sure `showOverlay('hidden')` → className='hidden' → CSS `#overlay.hidden{display:none}` ✓.

Double-check pill markup nesting for flex-wrap: `<div class="pill"><span class="dot"></span><span>...</span></div>` ✓.

Potential issue: `overlay.className='demo'` — but base styles for #overlay apply; `.demo` variant overrides align-items & adds background. ✓

Also `#overlay.over,#overlay.win{background:rgba(4,8,18,.5)}` add to CSS.

Edge: When window resized, nothing needed.

Final check of collidePaddle top-hit condition: `b.vy>0 && py+b.r<=paddle.y+0.01`. When ball approaches from above, py+r <= paddle.y until overlap begins; in the substep where overlap first occurs, px,py is pre-move position where py+r might be <= paddle.y (since movement ≤5.6 and paddle top contact...). Actually overlap occurs when b.y+b.r >= paddle.y; pre-move py+b.r could be paddle.y - something ≤ paddle.y ✓. So condition holds. Good.

But what if ball approaches from above but paddle moved up? Paddle y fixed. Fine.

Also what if ball is exactly overlapping from previous frame (e.g., after paddle expanded upward? paddle y fixed). OK.

One more: after top bounce, we don't check brick collision again this substep — order: walls, bricks, paddle. Paddle last — ball could pass through a brick that sits just above paddle? Bricks are high up; fine generally. Order bricks before paddle means brick collision takes precedence — good.

Now write it all out. I'll be careful with template literals inside innerHTML — using string concat with score.

Also `String(score).padStart(6,'0')` in HUD.

Let me write the final answer now. Also include a tiny favicon? Not needed (would be network? data URI fine but skip).

One more thought on the demo overlay text requirement: "Click or press a key to start" — our pill says "Click or press any key to start" ✓ clear.

Also ensure the game is visible behind pill — pill at top with gradient — ✓.

Counting rows: ROWS=8 ≥ 6 ✓.

Write final code.

Actually one more check on `fixAngle` being called in wall branch: after side-wall bounce, if |vy| < minVy we set vy sign from current vy (could be 0 → +). If ball is near top moving horizontally and hits side wall, it gets downward vy — fine.

But there's a subtle issue: fixAngle uses b.speed; stuck balls have speed set; ok.

Also initial ball speed: respawnBall sets speed=ballSpeed (400). setSpeed used when speeding up. In collidePaddle we use b.speed — consistent.

In hitBrick, after ballSpeed increase, we setSpeed for non-stuck balls — but the ball that hit (b) is in balls list, gets updated. Good.

Let me also cap dt in frame: done.

Potential division by zero: paddle.w/2 fine.

`keys.has(' ')`? not used.

Also prevent default on keydown for ' ' to avoid button activation when overlay button focused? If button focused and user presses space/enter → click event → bubbles → window pointerdown? No — keyboard click doesn't fire pointerdown. But our keydown handler: state!=='playing' → startGame anyway. Fine. Also preventDefault on space prevents button activation via keyboard — then keydown handler still runs startGame. OK.

Enter key: not prevented; if button focused, Enter triggers click → bubbles as click event (not pointerdown) — we don't listen to click on window... we listen pointerdown. Keyboard-generated click won't trigger pointerdown. But keydown handler already starts game on any key. Fine.

Now finalize. I'll write the code block.

Also: `ctx.fillText('move: mouse or ← → arrow keys', ...)` — arrows chars fine.

Let me make sure `rr` handles w/h small — capsule h/2=9 radius with h=18 → r=9 ok.

drawIcon multiball circles at cy±: s=6.2 → r=2.1, positions within capsule h=18: cy-3.1, cy+2.6 ±2.1 → within ±5.2+... fits.

OK writing final.

One more: in `update`, when state==='over'/'win', we still run the "balls empty" branch guard `(state==='playing'||state==='demo')` ✓.

And win check `bricks.every` runs every frame over 80 bricks — trivial.

Also in demo, if bricks cleared → buildLevel() — but this happens possibly while a powerup falling — fine.

Also note: in demo the win branch never triggers since state==='demo' handled first. ✓

Let me write out. I'll aim for clean formatting.

Also, small detail: `canvas` CSS `height:auto` + aspect-ratio — with width 100% of wrap. Wrap width set. ✓

Also body flex centering with overflow hidden — at small heights canvas might exceed? width formula caps by height so fine.

Final code below.