Models 44

Qwen3.8 Max (0902)

by Qwenreasoning defaultClean
Open in new window

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

How it went

Total time
11 min 43 s
Cost
$0.252
Output tokens
41,832
Spent thinking
28,556 tokens
Speed
59.7 tok/s
Console errors
0
First token after
1.8 s
First visible text after
9 min 39 s
Prompt tokens
412
Code size
31.2 KB
Finish reason
stop
Served by
Alibaba
Model id
qwen/qwen3.8-max-0902
Reasoning sent
model default
Max output
131,072 tokens
Generated
Nov 12, 2026

What it was asked

The prompt
“i want a breakout game where the paddle is a hamburger and every brick you break explodes into tons of little hearts”
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 · 597 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>Burger Breakout ♥</title>
<style>
  :root{
    --pink:#ff2e63; --hot:#ff5da2; --blush:#ff8fb3; --gold:#ffc857;
    --mint:#7be8c3; --coral:#ff7a5c; --cream:#fff3e6; --ink:#160a1c;
  }
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{overflow-x:hidden}
  body{
    min-height:100vh;
    font-family:"Courier New",ui-monospace,Consolas,monospace;
    color:var(--cream);
    background:
      radial-gradient(1100px 650px at 18% -10%, #3b1344 0%, rgba(59,19,68,0) 60%),
      radial-gradient(950px 600px at 85% 110%, #43102c 0%, rgba(67,16,44,0) 62%),
      radial-gradient(circle, rgba(255,255,255,.03) 1px, transparent 1.6px) 0 0/28px 28px,
      linear-gradient(#1b0c22, #0d0512);
  }
  /* ambient floating hearts */
  #amb{position:fixed;inset:0;overflow:hidden;z-index:0;pointer-events:none}
  #amb i{position:absolute;bottom:-8vh;left:var(--x);font-size:var(--s);opacity:var(--o);
    font-style:normal;text-shadow:0 0 14px currentColor;
    animation:rise var(--d) linear infinite;animation-delay:var(--dl)}
  @keyframes rise{to{transform:translateY(-118vh) rotate(38deg)}}

  .wrap{max-width:1000px;margin:0 auto;padding:12px 14px 20px;position:relative;z-index:1}

  /* ── marquee header ─────────────────────────── */
  header .logo{display:flex;align-items:center;justify-content:center;gap:clamp(8px,2vw,18px);margin:4px 0 10px}
  .lg{font-size:clamp(26px,4.2vw,42px);filter:drop-shadow(0 4px 6px rgba(0,0,0,.5));animation:bob 2.6s ease-in-out infinite}
  .lg:last-child{animation-delay:-1.3s}
  @keyframes bob{50%{transform:translateY(-6px) rotate(-6deg)}}
  .ttl{
    font-family:Impact,"Arial Black","Franklin Gothic Bold",sans-serif;
    font-size:clamp(26px,4.8vw,46px);letter-spacing:3px;line-height:1;color:var(--cream);
    text-shadow:0 3px 0 var(--pink), 0 7px 22px rgba(255,46,99,.5), 0 0 2px #000;
    text-align:center;
  }
  .ttl em{font-style:normal;color:var(--gold);text-shadow:0 3px 0 #a3671b, 0 7px 22px rgba(255,200,87,.35)}
  .tagline{margin-top:6px;text-align:center;font-size:clamp(8px,1.3vw,11px);
    letter-spacing:3px;color:#c78ba6;font-weight:700}

  /* ── arcade cabinet ─────────────────────────── */
  .cab{position:relative;border-radius:18px;padding-bottom:4px;
    background:linear-gradient(#26102b,#170a1d);
    border:3px solid #4b2354;
    box-shadow:0 0 0 6px #120716, 0 24px 60px rgba(0,0,0,.65),
      0 0 46px rgba(255,46,99,.14), inset 0 1px 0 rgba(255,255,255,.07)}
  .bulbs{height:8px;margin:10px 16px 0;opacity:.95;
    background-image:radial-gradient(circle at 4px 4px, #ffd98a 2.6px, rgba(255,200,87,.22) 3.4px, transparent 4.6px);
    background-size:22px 8px;animation:chase 1s linear infinite}
  @keyframes chase{to{background-position-x:22px}}

  /* HUD */
  .hud{display:flex;gap:8px;margin:10px 16px 0;flex-wrap:wrap;align-items:stretch}
  .hbox{flex:1;min-width:92px;background:linear-gradient(#2b1131,#1a0a20);
    border:1px solid #4b2354;border-radius:10px;padding:6px 12px 7px;
    display:flex;flex-direction:column;gap:2px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 3px 8px rgba(0,0,0,.35)}
  .lbl{font-size:9px;font-weight:700;letter-spacing:2.5px;color:#c76b98}
  .val{font-weight:700;font-size:clamp(16px,2vw,21px);line-height:1;color:var(--gold);
    text-shadow:0 0 12px rgba(255,200,87,.35);transform-origin:left center;display:inline-block}
  .val.pink{color:var(--hot);text-shadow:0 0 12px rgba(255,93,162,.4)}
  .val.mint{color:var(--mint);text-shadow:0 0 12px rgba(123,232,195,.35)}
  .val.dim{color:#8f6b7f;text-shadow:none}
  #score.pop{animation:pop .25s ease}
  @keyframes pop{40%{transform:scale(1.28)}}
  .hearts b{color:var(--pink);font-size:clamp(14px,1.8vw,19px);margin-right:3px;display:inline-block;
    text-shadow:0 0 10px rgba(255,46,99,.65);transition:transform .25s,color .25s}
  .hearts b.lost{color:#3c2140;text-shadow:none;transform:scale(.72)}
  .hearts b.burst{animation:hburst .45s ease}
  @keyframes hburst{35%{transform:scale(1.7) rotate(-14deg);color:#fff}}
  #comboBox{transition:border-color .2s, box-shadow .2s}
  #comboBox.hot{border-color:var(--pink);box-shadow:0 0 16px rgba(255,46,99,.4), inset 0 1px 0 rgba(255,255,255,.06)}
  #sndBtn{min-width:44px;border:1px solid #4b2354;border-radius:10px;cursor:pointer;
    background:linear-gradient(#2b1131,#1a0a20);color:var(--cream);font-size:17px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
  #sndBtn:hover{border-color:var(--hot)}

  /* screen */
  .screen{position:relative;margin:10px 16px 12px;border-radius:12px;overflow:hidden;
    border:2px solid #46204e;box-shadow:inset 0 0 46px rgba(0,0,0,.55)}
  canvas{display:block;width:100%;height:auto;background:#1c0b22;touch-action:none}
  .screen::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:2;opacity:.5;
    background:repeating-linear-gradient(0deg, rgba(255,255,255,.028) 0 1px, transparent 1px 3px)}
  .mode{position:absolute;top:10px;left:12px;z-index:4;pointer-events:none;
    font-size:10px;font-weight:700;letter-spacing:2px;color:var(--mint);
    background:rgba(6,14,12,.6);border:1px solid rgba(123,232,195,.4);
    padding:5px 10px;border-radius:99px}

  /* overlay */
  .ov{position:absolute;inset:0;z-index:5;display:flex;align-items:center;justify-content:center;
    text-align:center;padding:14px;pointer-events:none;transition:opacity .45s ease;
    background:radial-gradient(ellipse at 50% 42%, rgba(34,8,38,.42), rgba(9,3,13,.82))}
  .ov.hidden{opacity:0}
  #ovInner{display:flex;flex-direction:column;align-items:center;gap:clamp(8px,1.8vh,18px);max-width:92%}
  .dtag{font-size:10px;font-weight:700;letter-spacing:3px;color:var(--mint);
    border:1px solid rgba(123,232,195,.45);background:rgba(9,20,17,.45);
    padding:6px 12px;border-radius:99px}
  .ov h2{font-family:Impact,"Arial Black",sans-serif;font-weight:400;
    font-size:clamp(30px,6.4vw,60px);letter-spacing:3px;line-height:.95;color:var(--cream);
    text-shadow:0 3px 0 var(--pink), 0 8px 26px rgba(255,46,99,.55), 0 0 2px #000}
  .ov h2 em{font-style:normal;color:var(--gold);text-shadow:0 3px 0 #a3671b, 0 8px 26px rgba(255,200,87,.4)}
  .sub{font-family:Verdana,Geneva,sans-serif;color:#ffd7e6;
    font-size:clamp(11px,1.55vw,14px);max-width:52ch;line-height:1.5}
  .start{font-weight:700;color:#ffe9c9;font-size:clamp(12px,1.9vw,18px);letter-spacing:2px;
    padding:10px 20px;border:2px dashed rgba(255,233,201,.55);border-radius:10px;
    background:rgba(30,10,32,.5);animation:blink 1.15s steps(1) infinite}
  @keyframes blink{0%,58%{opacity:1}59%,100%{opacity:.12}}
  .ctl{font-size:clamp(8px,1.2vw,10.5px);letter-spacing:1.6px;color:#b98ca8;font-weight:700}
  .final{font-weight:700;font-size:clamp(15px,2.6vw,24px);color:var(--gold);letter-spacing:2px}
  .nb{color:var(--mint);font-size:.6em;vertical-align:middle;margin-left:8px;animation:blink .7s steps(1) infinite}

  footer{margin-top:12px;text-align:center;font-size:clamp(8px,1.2vw,10.5px);
    font-weight:700;letter-spacing:1.6px;color:#8f6b7f;line-height:1.9}
  footer b{color:var(--hot)} footer .g{color:var(--gold)} footer .m{color:var(--mint)}
  @media (max-width:560px){ .hbox{min-width:74px;padding:5px 9px 6px} .lbl{letter-spacing:1.5px} }
</style>
</head>
<body>
<div id="amb" aria-hidden="true"></div>

<div class="wrap">
  <header>
    <div class="logo">
      <span class="lg">🍔</span>
      <div>
        <h1 class="ttl">BURGER <em>BREAKOUT</em></h1>
        <p class="tagline">SMASH BRICKS ♥ BURST HEARTS ♥ KEEP THE BURGER ALIVE</p>
      </div>
      <span class="lg">💖</span>
    </div>
  </header>

  <div class="cab">
    <div class="bulbs"></div>

    <div class="hud">
      <div class="hbox"><span class="lbl">SCORE</span><span class="val" id="score">0</span></div>
      <div class="hbox"><span class="lbl">HI-SCORE</span><span class="val dim" id="hi">0</span></div>
      <div class="hbox" id="comboBox"><span class="lbl">COMBO</span><span class="val pink" id="combo">×1</span></div>
      <div class="hbox"><span class="lbl">LEVEL</span><span class="val mint" id="level">1</span></div>
      <div class="hbox"><span class="lbl">LIVES</span><span class="hearts" id="hearts"><b>♥</b><b>♥</b><b>♥</b></span></div>
      <button id="sndBtn" title="Toggle sound (M)" aria-label="toggle sound">🔊</button>
    </div>

    <div class="screen">
      <canvas id="cv" width="960" height="600"></canvas>
      <div class="mode" id="modeTag">● AUTO-DEMO</div>
      <div class="ov" id="ov"><div id="ovInner">
        <div class="dtag">● AUTO-DEMO — THE BURGER PLAYS ITSELF</div>
        <h2>BURGER <em>BREAKOUT</em></h2>
        <p class="sub">The paddle is a juicy double burger — and every brick you smash explodes into a shower of tiny hearts.</p>
        <div class="start">▶ CLICK OR PRESS ANY KEY TO START ◀</div>
        <div class="ctl">MOUSE / ← → MOVE · SPACE OR CLICK LAUNCH · CATCH FALLING ♥ FOR POWER-UPS · M SOUND</div>
      </div></div>
    </div>

    <div class="bulbs"></div>
  </div>

  <footer>
    CATCH FALLING HEARTS → <b>MULTIBALL</b> &amp; <span class="g">WIDE BURGER</span> ·
    COMBO BREAKS FOR BIG POINTS · <span class="m">HEART-SHAPED LEVEL 1</span><br>
    MADE WITH ♥ AND EXTRA PICKLES
  </footer>
</div>

<script>
(function(){
"use strict";
/* ═══ setup ═══ */
const W=960,H=600;
const cv=document.getElementById('cv'),ctx=cv.getContext('2d');
const $=id=>document.getElementById(id);
let DPR=1;
function fit(){DPR=Math.min(2,window.devicePixelRatio||1);cv.width=W*DPR;cv.height=H*DPR;ctx.setTransform(DPR,0,0,DPR,0,0);}
fit();window.addEventListener('resize',fit);

const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const rand=(a,b)=>a+Math.random()*(b-a);
const pick=a=>a[(Math.random()*a.length)|0];
function rr(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
function heart(c,x,y,s,rot,sx){c.save();c.translate(x,y);c.rotate(rot||0);c.scale(sx===undefined?1:sx,1);
  c.beginPath();c.moveTo(0,-s*0.35);
  c.bezierCurveTo(s*0.55,-s*1.05,s*1.35,-s*0.05,0,s*0.85);
  c.bezierCurveTo(-s*1.35,-s*0.05,-s*0.55,-s*1.05,0,-s*0.35);
  c.closePath();c.fill();c.restore();}

/* ═══ background (pre-rendered) ═══ */
const bg=document.createElement('canvas');bg.width=W;bg.height=H;
(function(g){
  const lg=g.createLinearGradient(0,0,0,H);
  lg.addColorStop(0,'#2c1034');lg.addColorStop(.55,'#1d0a24');lg.addColorStop(1,'#130616');
  g.fillStyle=lg;g.fillRect(0,0,W,H);
  const rg=g.createRadialGradient(W/2,-80,40,W/2,-80,540);
  rg.addColorStop(0,'rgba(255,93,162,.17)');rg.addColorStop(1,'rgba(255,93,162,0)');
  g.fillStyle=rg;g.fillRect(0,0,W,H);
  g.fillStyle='rgba(255,255,255,.03)';
  for(let x=16;x<W;x+=32)for(let y=16;y<H;y+=32)g.fillRect(x,y,2,2);
  const vg=g.createRadialGradient(W/2,H/2,H*0.42,W/2,H/2,W*0.72);
  vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,.34)');
  g.fillStyle=vg;g.fillRect(0,0,W,H);
  g.fillStyle='rgba(255,46,99,.28)';g.fillRect(0,0,4,H);g.fillRect(W-4,0,4,H);g.fillRect(0,0,W,4);
})(bg.getContext('2d'));
const drift=[{x:150,y:420,s:64,sp:.22,ph:0},{x:720,y:180,s:96,sp:.15,ph:2},{x:430,y:540,s:46,sp:.3,ph:4}];

/* ambient page hearts */
(function(){
  const amb=$('amb'),cols=['#ff2e63','#ff5da2','#ffc857','#ff8fb3'];
  for(let i=0;i<16;i++){const s=document.createElement('i');s.textContent='♥';
    s.style.setProperty('--x',(Math.random()*100)+'vw');
    s.style.setProperty('--s',(10+Math.random()*22)+'px');
    s.style.setProperty('--o',(0.08+Math.random()*0.15).toFixed(2));
    s.style.setProperty('--d',(14+Math.random()*18)+'s');
    s.style.setProperty('--dl',(-Math.random()*30)+'s');
    s.style.color=cols[i%4];amb.appendChild(s);}
})();

/* ═══ audio ═══ */
let AC=null,muted=false;
function ac(){try{if(!AC)AC=new (window.AudioContext||window.webkitAudioContext)();
  if(AC&&AC.state==='suspended')AC.resume();}catch(e){}}
function tone(f0,f1,dur,type,vol,delay){
  if(muted||!AC)return;try{
  const t0=AC.currentTime+(delay||0),o=AC.createOscillator(),g=AC.createGain();
  o.type=type;o.frequency.setValueAtTime(f0,t0);
  if(f1&&f1!==f0)o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t0+dur);
  g.gain.setValueAtTime(vol,t0);g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);
  o.connect(g);g.connect(AC.destination);o.start(t0);o.stop(t0+dur+0.03);}catch(e){}}
const sTick =()=>tone(240,190,.05,'square',.05);
const sBounce=r=>tone(180+Math.abs(r)*90,330,.09,'triangle',.17);
const sThud =()=>tone(150,110,.07,'square',.12);
const sPop  =c=>{const f=300+Math.min(8,c)*45;tone(f,f*1.9,.12,'square',.15);tone(f*1.5,f*2.6,.14,'sine',.09,.02);};
const sLaunch=()=>tone(330,660,.14,'triangle',.14);
const sPower=()=>[523,659,784,1047].forEach((f,i)=>tone(f,f,.1,'triangle',.13,i*.07));
const sLose =()=>tone(300,88,.5,'sawtooth',.15);
const sClear=()=>[392,523,659,784,1047].forEach((f,i)=>tone(f,f,.12,'square',.11,i*.08));

/* ═══ state ═══ */
let state='demo';            // demo | play | over
let score=0,hi=0,prevBest=0,lives=3,level=1,combo=0,comboT=0;
let ballSpeed=5,frame=0,launchT=0,emptyT=0;
let shakeT=0,shakeM=0,flashT=0,flashC='255,46,99',banner=null;
const bricks=[],balls=[],parts=[],popups=[],rings=[],powerups=[];
let aliveCount=0;
const paddle={x:W/2,y:H-64,w:112,baseW:112,h:48,vx:0,targetX:W/2,squash:0,wide:0,tilt:0};
const keys=new Set();

/* ═══ levels ═══ */
const HEART=[".XX...XX.","XXXX.XXXX","XXXXXXXXX","XXXXXXXXX","XXXXXXXXX",".XXXXXXX.","..XXXXX..","...XXX...","....X...."];
const DIA  =[".....X.....","....XXX....","...XXXXX...","..XXXXXXX..",".XXXXXXXXX.","..XXXXXXX..","...XXXXX...","....XXX....",".....X....."];
const HP=[['#ff2e63','#c9184a'],['#ff4d7e','#d92f63'],['#ff6f9c','#e24a78'],['#ff8fb3','#e56a90'],['#ffb3cf','#e98aad']];
function mkBrick(x,y,w,h,c1,c2,hp){
  const g=ctx.createLinearGradient(0,y,0,y+h);g.addColorStop(0,c1);g.addColorStop(1,c2);
  return {x,y,w,h,c1,c2,g,hp:hp||1,max:hp||1,alive:true,flash:0};}
function buildMask(rows,colorFor,bh){
  const cols=rows[0].length,gap=6,MX=26,bw=(W-MX*2-(cols-1)*gap)/cols;
  rows.forEach((row,r)=>{[...row].forEach((ch,c)=>{
    if(ch==='X'){const p=colorFor(r,c);bricks.push(mkBrick(MX+c*(bw+gap),60+r*(bh+gap),bw,bh,p[0],p[1]));}});});}
function buildStripes(){
  const cols=11,rows=5,gap=6,MX=26,bh=26,bw=(W-MX*2-(cols-1)*gap)/cols;
  const pal=[['#ff2e63','#c9184a'],['#ff7a5c','#e0563a'],['#ffc857','#dfa33a'],['#7be8c3','#4cc49c'],['#8ab6ff','#5a86e0']];
  for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
    let p=pal[r],hp=1;
    if(level>1&&(r===1||r===3)&&c%3===1){p=['#8a4b2d','#5f3018'];hp=2;}
    bricks.push(mkBrick(MX+c*(bw+gap),60+r*(bh+gap),bw,bh,p[0],p[1],hp));}}
function buildLevel(n){
  bricks.length=0;
  const f=(n-1)%3;
  if(f===0)buildMask(HEART,r=>HP[Math.min(r,8-r)],26);
  else if(f===1)buildStripes();
  else buildMask(DIA,(r,c)=>(r+c)%2?['#ffc857','#dfa33a']:['#ff5da2','#d63d7e'],24);
  aliveCount=bricks.length;}
function newBall(){return{x:paddle.x,y:paddle.y-14,vx:0,vy:0,r:8,stuck:true,trail:[],off:rand(-24,24),cd:0};}
function launch(b){b.stuck=false;const a=rand(-.35,.35);b.vx=Math.sin(a)*ballSpeed;b.vy=-Math.cos(a)*ballSpeed;sLaunch();}
function launchStuck(){const b=balls.find(b=>b.stuck);if(b){launch(b);launchT=0;}}

/* ═══ effects ═══ */
function shake(m,t){shakeM=Math.max(shakeM,m);shakeT=Math.max(shakeT,t);}
function burst(b){
  const n=26+((Math.random()*10)|0),
  cols=[b.c1,'#ff2e63','#ff5d8f','#ff8fb3','#ffc2d4','#fff0f5'];
  for(let i=0;i<n;i++){
    const a=-Math.PI/2+rand(-1.2,1.2),sp=rand(1.2,5.6);
    parts.push({x:b.x+Math.random()*b.w,y:b.y+Math.random()*b.h,
      vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-0.8,s:rand(2.6,8),rot:rand(0,6.3),vr:rand(-.14,.14),
      c:pick(cols),life:rand(55,100),g:rand(.1,.17),wob:rand(0,6),ws:rand(.14,.34),fl:rand(0,6),trail:false});}
  if(parts.length>1100)parts.splice(0,parts.length-1100);
  rings.push({x:b.x+b.w/2,y:b.y+b.h/2,r:6,life:16,c:b.c1});}
function miniBurst(x,y,n,c){
  for(let i=0;i<n;i++){const a=rand(0,6.3),sp=rand(.8,3);
    parts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-1,s:rand(2,4.5),rot:rand(0,6.3),vr:rand(-.15,.15),
      c:c||pick(['#ffc857','#ff8fb3','#fff3e6']),life:rand(28,50),g:.12,wob:rand(0,6),ws:.2,fl:rand(0,6),trail:false});}}

/* ═══ HUD ═══ */
const scoreEl=$('score'),hiEl=$('hi'),comboEl=$('combo'),comboBox=$('comboBox'),levelEl=$('level'),
      heartsEl=$('hearts'),ov=$('ov'),ovInner=$('ovInner'),modeTag=$('modeTag');
function setScore(popIt){scoreEl.textContent=score;hiEl.textContent=Math.max(hi,score);
  if(popIt){scoreEl.classList.remove('pop');void scoreEl.offsetWidth;scoreEl.classList.add('pop');}}
function comboUI(){comboEl.textContent='×'+Math.max(1,combo);comboBox.classList.toggle('hot',combo>=2);}
function setLevel(){levelEl.textContent=level;}
function updHearts(){[...heartsEl.children].forEach((b,i)=>b.classList.toggle('lost',i>=lives));}
function breakHeartUI(){const el=heartsEl.children[lives];if(el){el.classList.add('burst');setTimeout(()=>el.classList.remove('burst'),500);}}

/* ═══ core actions ═══ */
function destroyBrick(k){
  k.alive=false;aliveCount--;
  combo=comboT>0?combo+1:1;comboT=130;comboUI();
  const pts=50*combo;score+=pts;if(score>hi)hi=score;setScore(true);
  burst(k);
  popups.push({x:k.x+k.w/2,y:k.y+6,txt:(combo>1?'×'+combo+' ':'')+'+'+pts,
    c:'#ffd7e6',life:55,vy:-1,size:combo>2?21:15});
  shake(3+Math.min(6,combo),8);sPop(combo);
  ballSpeed=Math.min(8.4,ballSpeed+0.008);
  if(Math.random()<0.16&&powerups.length<2)
    powerups.push({x:k.x+k.w/2,y:k.y+k.h/2,type:Math.random()<.5?'multi':'wide',t:rand(0,6)});
  if(aliveCount<=0&&!banner){banner={t:130};balls.length=0;powerups.length=0;
    score+=500;setScore(true);sClear();shake(6,16);flashT=14;flashC='255,190,225';}}
function hitBrick(k){
  k.hp--;k.flash=6;
  if(k.hp<=0)destroyBrick(k);
  else{miniBurst(k.x+k.w/2,k.y+k.h/2,6,'#ffc9a3');score+=10;setScore(true);sThud();shake(2,4);}}
function bouncePaddle(b){
  const rel=clamp((b.x-paddle.x)/(paddle.w/2),-1,1),th=rel*1.08;
  const sp=Math.max(ballSpeed,Math.hypot(b.vx,b.vy));
  b.vx=Math.sin(th)*sp+paddle.vx*0.3;b.vy=-Math.cos(th)*sp;
  if(b.vy>-2.4)b.vy=-2.4;
  b.y=paddle.y+4-b.r-1;b.cd=4;
  paddle.squash=1;miniBurst(b.x,paddle.y+8,3,'#ffe9c9');sBounce(rel);}
function fixFlat(b){
  const sp=Math.hypot(b.vx,b.vy);
  if(sp>0&&Math.abs(b.vy)/sp<0.22){const s=Math.sign(b.vy)||-1;
    b.vy=s*sp*0.26;b.vx=(Math.sign(b.vx)||1)*Math.sqrt(Math.max(.5,sp*sp-b.vy*b.vy));}}
function applyPower(pu){
  if(pu.type==='multi'){
    for(const da of[-0.55,0.55]){if(balls.length>=6)break;
      balls.push({x:paddle.x,y:paddle.y-22,vx:Math.sin(da)*ballSpeed,vy:-Math.cos(da)*ballSpeed,
        r:8,stuck:false,trail:[],off:0,cd:0});}
    popups.push({x:paddle.x,y:paddle.y-42,txt:'MULTIBALL!',c:'#ffd7e6',life:62,vy:-1,size:19});
  }else{
    paddle.wide=720;
    popups.push({x:paddle.x,y:paddle.y-42,txt:'WIDE BURGER!',c:'#ffc857',life:62,vy:-1,size:19});}
  miniBurst(paddle.x,paddle.y,14,pu.type==='multi'?'#ff5da2':'#ffc857');sPower();}
function loseLife(){
  lives--;breakHeartUI();updHearts();flashT=18;flashC='255,46,99';shake(9,15);sLose();
  combo=0;comboT=0;comboUI();
  if(lives<=0)gameOver();else balls.push(newBall());}
function demoReset(){
  score=0;setScore();lives=3;updHearts();level=1;setLevel();combo=0;comboUI();
  ballSpeed=5;banner=null;launchT=0;buildLevel(1);balls.length=0;balls.push(newBall());powerups.length=0;}
function gameOver(){
  state='over';modeTag.textContent='GAME OVER';if(score>hi)hi=score;setScore();
  ovInner.innerHTML='<div class="dtag">THE BURGER HAS FALLEN</div>'+
    '<h2>GAME <em>OVER</em></h2>'+
    '<div class="final">SCORE '+score+(score>prevBest&&score>0?'<span class="nb">★ NEW BEST ★</span>':'')+'</div>'+
    '<div class="start">▶ CLICK OR PRESS ANY KEY TO PLAY AGAIN ◀</div>'+
    '<div class="ctl">THE BURGER BELIEVES IN YOU 🍔</div>';
  ov.classList.remove('hidden');}
function startGame(){
  state='play';modeTag.textContent='♥ PLAY';ov.classList.add('hidden');
  score=0;lives=3;level=1;combo=0;comboT=0;ballSpeed=5;banner=null;launchT=0;emptyT=0;
  prevBest=hi;
  buildLevel(1);balls.length=0;balls.push(newBall());powerups.length=0;popups.length=0;
  setScore();setLevel();updHearts();comboUI();flashT=8;flashC='255,190,225';}

/* ═══ input ═══ */
function pressStart(){ac();
  if(state==='demo'||state==='over')startGame();
  else if(state==='play')launchStuck();}
window.addEventListener('keydown',e=>{
  if(['Space','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].indexOf(e.code)>=0)e.preventDefault();
  if(e.code==='KeyM'){toggleMute();return;}
  if(e.repeat)return;keys.add(e.code);pressStart();});
window.addEventListener('keyup',e=>keys.delete(e.code));
window.addEventListener('pointerdown',e=>{if(e.target.closest('#sndBtn'))return;pressStart();});
cv.addEventListener('pointermove',e=>{
  if(state!=='play')return;
  const r=cv.getBoundingClientRect();
  paddle.targetX=(e.clientX-r.left)/r.width*W;});
function toggleMute(){muted=!muted;$('sndBtn').textContent=muted?'🔇':'🔊';}
$('sndBtn').addEventListener('click',toggleMute);
document.addEventListener('visibilitychange',()=>{last=performance.now();});

/* ═══ paddle control ═══ */
function aiPaddle(dt){
  let target=W/2,best=null,bd=1e9;
  for(const b of balls){if(b.stuck)continue;
    if(b.vy>0){const t=(paddle.y-b.y)/b.vy;if(t<bd){bd=t;best=b;}}}
  if(best)target=best.x+best.vx*Math.min(bd,40)*0.55+Math.sin(frame*0.05)*6;
  else if(powerups.length)target=powerups[0].x;
  paddle.targetX=clamp(target,paddle.w/2+10,W-paddle.w/2-10);}
function movePaddle(dt){
  const max=16*dt,d=paddle.targetX-paddle.x,ox=paddle.x;
  paddle.x=clamp(paddle.x+clamp(d,-max,max),paddle.w/2+8,W-paddle.w/2-8);
  paddle.vx=paddle.x-ox;
  paddle.tilt+=(clamp(paddle.vx*0.05,-.14,.14)-paddle.tilt)*0.2*dt;}

/* ═══ update ═══ */
function update(dt){
  frame++;
  const tw=paddle.wide>0?paddle.baseW*1.5:paddle.baseW;
  paddle.w+=(tw-paddle.w)*Math.min(1,.12*dt);
  if(paddle.wide>0)paddle.wide-=dt;
  if(state==='play'){
    if(keys.has('ArrowLeft')||keys.has('KeyA'))paddle.targetX=paddle.x-70;
    if(keys.has('ArrowRight')||keys.has('KeyD'))paddle.targetX=paddle.x+70;
  }else aiPaddle(dt);
  movePaddle(dt);
  paddle.squash*=Math.pow(.86,dt);

  /* balls */
  for(let i=balls.length-1;i>=0;i--){const b=balls[i];
    if(b.cd>0)b.cd-=dt;
    if(b.stuck){b.x=paddle.x+b.off*0.4;b.y=paddle.y-14;b.trail.length=0;
      if(state!=='play'){launchT++;if(launchT>55){launch(b);launchT=0;}}continue;}
    b.trail.push({x:b.x,y:b.y});if(b.trail.length>7)b.trail.shift();
    if(frame%6===0&&parts.length<800)
      parts.push({x:b.x,y:b.y,vx:0,vy:.2,s:2.2,rot:rand(0,6),vr:.05,c:'#ff8fb3',life:18,g:0,wob:rand(0,6),ws:.2,fl:0,trail:true});
    const sp=Math.hypot(b.vx,b.vy),steps=Math.max(1,Math.ceil(sp/5));
    for(let s=0;s<steps;s++){
      b.x+=b.vx*dt/steps;b.y+=b.vy*dt/steps;
      if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx);sTick();}
      else if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);sTick();}
      if(b.y<b.r+2){b.y=b.r+2;b.vy=Math.abs(b.vy);sTick();}
      if(b.vy>0&&b.y+b.r>=paddle.y+4&&b.y<paddle.y+36&&Math.abs(b.x-paddle.x)<=paddle.w/2+b.r*0.7)
        bouncePaddle(b);
      if(b.cd<=0)for(const k of bricks){if(!k.alive)continue;
        const cx=clamp(b.x,k.x,k.x+k.w),cy=clamp(b.y,k.y,k.y+k.h);
        const dx=b.x-cx,dy=b.y-cy;
        if(dx*dx+dy*dy<=b.r*b.r){
          if(Math.abs(dx)>Math.abs(dy))b.vx=(dx>0?1:-1)*Math.abs(b.vx);
          else b.vy=(dy>0?1:-1)*Math.abs(b.vy);
          b.cd=5;hitBrick(k);break;}}
      fixFlat(b);}
    if(b.y>H+40)balls.splice(i,1);}

  if(balls.length===0&&!banner){emptyT+=dt;
    if(emptyT>35){emptyT=0;
      if(state==='play')loseLife();else if(state==='demo')demoReset();}}
  else emptyT=0;

  /* powerups */
  for(let i=powerups.length-1;i>=0;i--){const p=powerups[i];
    p.y+=2.1*dt;p.x+=Math.sin(frame*0.06+p.t)*0.9*dt;
    if(p.y>paddle.y&&p.y<paddle.y+paddle.h+12&&Math.abs(p.x-paddle.x)<paddle.w/2+14){
      applyPower(p);powerups.splice(i,1);}
    else if(p.y>H+30)powerups.splice(i,1);}

  /* particles */
  for(let i=parts.length-1;i>=0;i--){const p=parts[i];
    p.vy+=p.g*dt;p.vx*=.995;p.wob+=p.ws*dt;p.fl+=.3*dt;
    p.x+=p.vx*dt+Math.sin(p.wob)*0.45*dt;p.y+=p.vy*dt;p.rot+=p.vr*dt;p.life-=dt;
    if(p.life<=0||p.y>H+30)parts.splice(i,1);}
  for(let i=rings.length-1;i>=0;i--){const r=rings[i];r.r+=4.2*dt;r.life-=dt;if(r.life<=0)rings.splice(i,1);}
  for(let i=popups.length-1;i>=0;i--){const p=popups[i];p.y+=p.vy*dt;p.life-=dt;if(p.life<=0)popups.splice(i,1);}

  if(comboT>0){comboT-=dt;if(comboT<=0){combo=0;comboUI();}}
  for(const k of bricks)if(k.flash>0)k.flash-=dt;
  if(banner){banner.t-=dt;
    if(banner.t<=0){banner=null;level++;setLevel();buildLevel(level);
      balls.length=0;balls.push(newBall());
      ballSpeed=Math.min(8.4,5+0.32*(level-1));flashT=10;flashC='255,190,225';}}
  for(const d of drift){d.y-=d.sp*dt;if(d.y<-110){d.y=H+90;d.x=rand(60,W-60);}}
}

/* ═══ drawing ═══ */
function eyeAim(){
  let best=null,bd=1e9;
  for(const b of balls){const d=(b.x-paddle.x)**2+(b.y-paddle.y)**2;if(d<bd){bd=d;best=b;}}
  if(!best)return{x:0,y:-1};
  const dx=best.x-paddle.x,dy=best.y-(paddle.y+14),m=Math.hypot(dx,dy)||1;
  return{x:dx/m*1.9,y:dy/m*1.9};}
function drawBurger(){
  const p=paddle,w=p.w,x=p.x,B=H-16,T=B-48,L=x-w/2;
  ctx.save();
  ctx.fillStyle='rgba(0,0,0,.35)';ctx.beginPath();ctx.ellipse(x,B+4,w*0.46,6,0,0,7);ctx.fill();
  ctx.translate(x,B);ctx.rotate(p.tilt);
  ctx.scale(1+p.squash*.16,1-p.squash*.2);ctx.translate(-x,-B);
  if(p.wide>0){ctx.strokeStyle='rgba(255,200,87,'+(.25+.15*Math.sin(frame*.2))+')';
    ctx.lineWidth=3;ctx.beginPath();ctx.ellipse(x,B-24,w*0.62,34,0,0,7);ctx.stroke();}
  ctx.fillStyle='#e59a41';rr(L+3,B-11,w-6,11,5);ctx.fill();
  ctx.fillStyle='#c97e2e';rr(L+3,B-5,w-6,5,4);ctx.fill();
  ctx.fillStyle='#6f3a1f';rr(L-4,B-21,w+8,12,6);ctx.fill();
  ctx.fillStyle='#54290f';rr(L-4,B-14,w+8,5,4);ctx.fill();
  ctx.fillStyle='#ffc23d';rr(L+2,B-25,w-4,6,3);ctx.fill();
  ctx.beginPath();
  for(const fx of[.18,.5,.82]){ctx.moveTo(L+w*fx,B-19);ctx.lineTo(L+w*fx+6,B-12);ctx.lineTo(L+w*fx+12,B-19);}
  ctx.fill();
  ctx.fillStyle='#ff5a4e';rr(L+5,B-29,w-10,5,3);ctx.fill();
  ctx.fillStyle='#7ed35f';rr(L-2,B-33,w+4,6,3);ctx.fill();
  ctx.beginPath();for(let sx=L+5;sx<L+w-4;sx+=11)ctx.arc(sx,B-27,5,0,Math.PI);ctx.fill();
  const grd=ctx.createLinearGradient(0,T,0,B-30);
  grd.addColorStop(0,'#f7bd63');grd.addColorStop(1,'#dd8f34');
  ctx.fillStyle=grd;ctx.beginPath();ctx.moveTo(L,B-30);
  ctx.bezierCurveTo(L,T-6,L+w,T-6,L+w,B-30);ctx.closePath();ctx.fill();
  ctx.strokeStyle='rgba(255,255,255,.4)';ctx.lineWidth=3;ctx.lineCap='round';
  ctx.beginPath();ctx.moveTo(L+w*0.2,T+17);ctx.quadraticCurveTo(L+w*0.38,T+7,L+w*0.56,T+13);ctx.stroke();
  ctx.fillStyle='#ffe9c9';
  [[.2,.6],[.36,.34],[.54,.28],[.72,.5],[.44,.66],[.8,.7]].forEach(s=>{
    ctx.beginPath();ctx.ellipse(L+w*s[0],T+6+s[1]*16,3.4,2,s[0]*2-1,0,7);ctx.fill();});
  const aim=eyeAim(),eyeY=T+13;
  for(const s of[-1,1]){
    ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(x+s*w*0.115,eyeY,4.2,0,7);ctx.fill();
    ctx.fillStyle='#3a2418';ctx.beginPath();ctx.arc(x+s*w*0.115+aim.x,eyeY+aim.y,2.1,0,7);ctx.fill();}
  ctx.fillStyle='rgba(255,110,140,.4)';
  ctx.beginPath();ctx.arc(x-w*0.21,eyeY+5,3,0,7);ctx.fill();
  ctx.beginPath();ctx.arc(x+w*0.21,eyeY+5,3,0,7);ctx.fill();
  ctx.strokeStyle='#5b3a1a';ctx.lineWidth=2;
  ctx.beginPath();ctx.arc(x,eyeY+4,4.5,0.15*Math.PI,0.85*Math.PI);ctx.stroke();
  ctx.restore();}
function drawBall(b){
  for(let j=0;j<b.trail.length;j++){const t=b.trail[j],f=(j+1)/b.trail.length;
    ctx.globalAlpha=f*0.22;ctx.fillStyle='#ff8fb3';
    ctx.beginPath();ctx.arc(t.x,t.y,b.r*(0.4+0.6*f),0,7);ctx.fill();}
  ctx.globalAlpha=1;
  const g=ctx.createRadialGradient(b.x-3,b.y-3,1,b.x,b.y,b.r);
  g.addColorStop(0,'#ff8a72');g.addColorStop(.6,'#f4443c');g.addColorStop(1,'#c92a30');
  ctx.fillStyle=g;ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fill();
  ctx.fillStyle='#58c95f';
  ctx.beginPath();ctx.ellipse(b.x-2.5,b.y-b.r+1,3.4,1.8,-.6,0,7);ctx.fill();
  ctx.beginPath();ctx.ellipse(b.x+2.5,b.y-b.r+1,3.4,1.8,.6,0,7);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.55)';ctx.beginPath();ctx.arc(b.x-3,b.y-3.5,2.2,0,7);ctx.fill();}
function drawBrick(k){
  ctx.fillStyle=k.g;rr(k.x,k.y,k.w,k.h,5);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.16)';rr(k.x+2,k.y+2,k.w-4,k.h*0.4,4);ctx.fill();
  ctx.fillStyle='rgba(255,255,255,.28)';heart(ctx,k.x+k.w/2,k.y+k.h/2+1,k.h*0.22,0,1);
  if(k.hp<k.max){ctx.strokeStyle='rgba(0,0,0,.4)';ctx.lineWidth=1.5;
    ctx.beginPath();ctx.moveTo(k.x+k.w*0.3,k.y+2);ctx.lineTo(k.x+k.w*0.42,k.y+k.h*0.5);
    ctx.lineTo(k.x+k.w*0.34,k.y+k.h-2);ctx.moveTo(k.x+k.w*0.7,k.y+2);
    ctx.lineTo(k.x+k.w*0.6,k.y+k.h*0.6);ctx.stroke();}
  if(k.flash>0){ctx.globalAlpha=k.flash/6*0.7;ctx.fillStyle='#fff';rr(k.x,k.y,k.w,k.h,5);ctx.fill();ctx.globalAlpha=1;}}

function render(){
  ctx.drawImage(bg,0,0,W,H);
  ctx.save();
  if(shakeT>0){const m=shakeM*Math.min(1,shakeT/10);
    ctx.translate((Math.random()-.5)*m,(Math.random()-.5)*m);}
  for(const d of drift){ctx.fillStyle='rgba(255,93,162,.05)';
    heart(ctx,d.x,d.y,d.s,Math.sin(frame*0.005+d.ph)*0.2,1);}
  for(const k of bricks)if(k.alive)drawBrick(k);
  for(const p of powerups){
    const pu=1+0.12*Math.sin(frame*0.18+p.t),c=p.type==='multi'?'#ff2e63':'#ffc857';
    ctx.fillStyle=p.type==='multi'?'rgba(255,46,99,.22)':'rgba(255,200,87,.22)';
    ctx.beginPath();ctx.arc(p.x,p.y,17*pu,0,7);ctx.fill();
    ctx.fillStyle=c;heart(ctx,p.x,p.y,11*pu,Math.sin(frame*.1+p.t)*.15,1);
    ctx.fillStyle='#fff';ctx.font='bold 8px "Courier New",monospace';ctx.textAlign='center';
    ctx.fillText(p.type==='multi'?'MULTI':'WIDE',p.x,p.y+24);}
  for(const r of rings){ctx.globalAlpha=Math.max(0,r.life/16);
    ctx.strokeStyle=r.c;ctx.lineWidth=3;ctx.beginPath();ctx.arc(r.x,r.y,r.r,0,7);ctx.stroke();}
  ctx.globalAlpha=1;
  for(const p of parts){ctx.globalAlpha=clamp(p.life/(p.trail?10:22),0,1);
    ctx.fillStyle=p.c;
    heart(ctx,p.x,p.y,p.s,p.rot,0.8+0.25*Math.sin(p.fl));}
  ctx.globalAlpha=1;
  for(const b of balls)drawBall(b);
  drawBurger();
  for(const p of popups){ctx.globalAlpha=clamp(p.life/28,0,1);
    ctx.font='bold '+p.size+'px "Courier New",monospace';ctx.textAlign='center';
    ctx.fillStyle=p.c;ctx.fillText(p.txt,clamp(p.x,70,W-70),p.y);}
  ctx.globalAlpha=1;
  if(state==='play'&&balls.some(b=>b.stuck)){
    ctx.globalAlpha=0.55+0.45*Math.sin(frame*0.12);
    ctx.font='bold 12px "Courier New",monospace';ctx.textAlign='center';ctx.fillStyle='#ffe9c9';
    ctx.fillText('CLICK OR PRESS SPACE TO LAUNCH ♥',clamp(paddle.x,140,W-140),paddle.y-32);
    ctx.globalAlpha=1;}
  if(banner){
    const first=banner.t>65,pr=first?(130-banner.t)/65:(65-banner.t)/65;
    ctx.save();ctx.globalAlpha=clamp(Math.min(pr*3,(1-pr)*3+.15),0,1);ctx.textAlign='center';
    ctx.font='900 46px Impact,"Arial Black",sans-serif';
    const txt=first?'LEVEL CLEAR!':'LEVEL '+(level+1);
    ctx.lineWidth=8;ctx.strokeStyle='rgba(255,46,99,.9)';ctx.strokeText(txt,W/2,H*0.42);
    ctx.fillStyle='#fff3e6';ctx.fillText(txt,W/2,H*0.42);
    ctx.font='bold 15px "Courier New",monospace';ctx.fillStyle='#ffc857';
    ctx.fillText(first?'♥ SO MANY HEARTS ♥':'GET READY',W/2,H*0.42+30);ctx.restore();}
  if(flashT>0){ctx.fillStyle='rgba('+flashC+','+((flashT/20)*0.28)+')';ctx.fillRect(0,0,W,H);}
  ctx.restore();
  if(flashT>0)flashT-=1;
  if(shakeT>0){shakeT-=1;if(shakeT<=0)shakeM=0;}
}

/* ═══ loop ═══ */
let last=performance.now();
function loop(t){requestAnimationFrame(loop);
  let dt=(t-last)/16.667;last=t;dt=clamp(dt||1,0.1,2.5);
  update(dt);render();}
buildLevel(1);balls.push(newBall());updHearts();comboUI();setLevel();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (28,556 tokens)
Let's build a Breakout game. Requirements:
- Paddle is a hamburger 🍔
- Blocks explode into tons of tiny hearts when destroyed
- Self-playing demo mode behind a "Click or press any key to start" overlay
- Everything inline, single HTML file
- Looks good at 1280×800, adapts down to 375px
- No external resources
- Must display something meaningful at first paint
- Zero console errors

Design direction: playful arcade vibes. Candy/Valentine arcade aesthetic? Blocks exploding into hearts suggests a love/Valentine theme. Let's make it juicy: dark warm background, warm palette. Hearts = pink/red. Use a rich dark plum/burgundy arcade cabinet vibe with multiple candy colors (pink, coral, gold, mint) — multi-accent, vibrant, arcade-like. Layered background: drifting hearts, subtle patterns, vignette.

Typography: since external fonts aren't allowed, need to be clever with system fonts. Stack like "Impact, 'Arial Black', sans-serif" for a chunky arcade display, monospace for score: 'Courier New' / ui-monospace. That gives a distinctive display face paired with readable body.

Game plan:
- Full canvas game.
- Blocks: grid, colored rows (candy colors). Heart particles explode when block breaks — tons of them.
- Paddle: hamburger emoji 🍔 drawn on canvas, or draw custom hamburger with canvas shapes. Custom drawing gives a crafted feel. Actually drawing a nice hamburger on canvas: bun top with sesame seeds, lettuce, cheese, patty, bottom bun. This is more crafted than an emoji and renders consistently. Let's go with canvas-drawn burger paddle. Ball could be a meatball or tomato — keep ball as a round tomato-ish red/orange ball with a highlight. Cute.

Features:
- Score, lives (draw as hearts ❤), combo multiplier.
- Screen shake on block destroy, squash/stretch on paddle on hit.
- Particle hearts: physics with gravity, rotation, fluttering (heart flapping scale), fade out. "Tons" — about 24-40 hearts per block. Need performance cap: cap max particles around 900, reuse.
- Also floating "+points" text.
- Sound: WebAudio bleeps — little synth blips for paddle hit, block destroy (pop), explosion. Must be initialized on user gesture; demo mode is silent until interaction. Autoplay policy: AudioContext requires gesture; wrap sounds so they only play after user starts. Demo mode silent — that's fine.
- Self-playing AI: paddle follows predicted ball x with slight error/imperfection, occasionally misses? In demo, better not miss so the game keeps going and shows action. Simple AI: track ball's landing x via simple reflection prediction, or simply lerp paddle toward ball.x with clamped speed. To make it look alive, add slight noise.
- Overlay: "Click or press any key to start" — but the game runs behind (demo mode). When user interacts, switch to player control (mouse/touch/arrow keys) and start the real game (reset? Or keep state? Classic approach: demo plays behind the title; on click the game starts fresh with ball stuck to paddle, click/space to launch). On start, show a brief "Move: mouse / ← →".

Lives: 3 hearts. On ball loss: lose one heart, screen flash, ball reset. On 0 lose: game over screen with score and "click to restart". On win (all blocks destroyed): "You're adorable! YOU WIN" screen, confetti hearts, click to restart. After a win in demo, auto-restart the demo.

Layout: this is a game, so the game screen itself is the focus. Wrap it in an arcade-styled frame: title at top, HUD. Put a marquee-like header: "BURGER BREAKOUT" and heart accent. The page opens with the game front and center. Side info minimal — HUD inside canvas or in DOM strip above canvas: SCORE, COMBO, LIVES hearts, LEVEL. Let's do DOM HUD above canvas styled arcade-y.

Page structure:
- Background: layered — deep plum radial gradient, floating heart particles (CSS animated or behind a second canvas), subtle diagonal candy stripes overlay at low opacity.
- Cabinet frame: game area with chunky border, glow, "screws" in corners, etc.
- Header: game logo text with burger + heart emoji accents, tagline.
- HUD bar: score (large monospace), combo meter, lives as hearts, level.
- Footer strip: control hints, high score. Keep small.

Colors: palette:
- bg deep berry: #1a0b1e / #240f2a with radial glows of #3d1030, #14213c. Multiple warm accents: hot pink #ff4d8d, coral #ff7a5c, gold #ffc857, mint #7be8c3, cream #fff3e0. Arcade multi-color.

Canvas visuals:
- Blocks: rounded rectangles with candy colors per row, glossy top highlight, tiny heart emblem in center of each block — reinforces the theme. When hit (if multi-hit?), keep blocks at 1-hit, except maybe gold "power" blocks that drop a power-up. Keep scope sane: multi-ball power-up? Multi-ball is fun and shows heart chaos. Power-ups: falling hearts that when caught grant multi-ball (x3) or wide burger. Implement: heart power-up drops from some blocks (25% chance), falling heart sprite; catch with burger → 2 extra balls + sound. Fun and fits theme.

- Ball: draw as small round "tomato" red with highlight, slight motion trail (trail = fading circles) — trail can be tiny hearts too? Occasionally spawn a small trail heart particle for charm (cheap: every few frames, spawn a small fading heart at ball position).

- Particles: hearts with size 3-9px, random velocities upward + spread, gravity, rotation, horizontal sway (vx flutter via sin), fade out after ~1.2s. Use pool array; cap 1000; draw hearts as path function. Heart path: standard bezier heart. Pre-compute? Path drawing per frame with 1000 hearts is fine if path is simple. Use ctx transform per particle — 1000 transforms per frame OK in Chrome. Also additive glow: shadowBlur is expensive; avoid per-particle shadow. Fill with rgba color.

- Screen shake: offset canvas transform with damped random.
- Block destroy: also spawn ring flash + score popup.

Block layout: 9 cols x 5 rows with heart-shaped blocks in center? Cute: rows of candy-colored blocks, and some arranged as a heart? A heart-shaped block formation is distinctive. 11x8 heart-shape grid using mask. Heart-shape block layout is very on-theme and distinctive. Two designs could be possible: level 1 = heart-shaped formation. Let's define heart mask on a grid (columns 11, rows 9-ish):

Classic heart pixel map (11 wide x 9 tall): let me construct:
Rows (x from 0..10):
Row0:  . X X . . . . X X . .  Hmm, standard 11x9 heart:
Use this pattern string:
```
".XX...XX."
"XXXX.XXXX"
XXXXXXXXX
XXXXXXXXX
XXXXXXXXX
.XXXXXXX.
..XXXXX..
...XXX...
....X....
```
That's width 9. Good: 9 columns, 9 rows. Block size computed to fit canvas width. 9 cols fits nicely. Level 1 heart shape, all 1-hit, rows colored with pink/red gradient.

Level 2: regular rows (candy stripes) with more blocks and higher speed, maybe some 2-hit "chocolate" blocks. Level 3+: repeat rows with increasing speed. Keep level counter; on win → next level with new formation, ball speed slightly up, retain score. In demo mode it just keeps going.

Actually make it simpler: cycle formations: [heart, stripes, heart-inverted?]. Let's do 3 formations: heart, full rows with gaps (classic), diamond. Rotate per level.

Paddle burger drawing (canvas): width ~110, height ~46. Draw:
- Bottom bun: rounded rectangle with ellipse for bread color (#e8a34f / #f0b45c), darker on bottom.
- Patty: dark brown rounded rectangle (#6b3a21) slightly wider, charred specks.
- Cheese: yellow (#ffc857) drip with triangles hanging.
- Lettuce: frilled green strip (wavy) — draw arcs of mint green (#8fd461).
- Tomato: red thin strip (#ff5d5d)? maybe skip to reduce clutter; add thin tomato.
- Top bun: bigger dome (arc/ellipse), golden, sesame seeds as small rotated cream ellipses.
- Also on hit: squash animation (scaleY down, scaleX up) then spring back. Eyes on the burger? Cute little face on the top bun? Kawaii face would be adorable — two eyes + a smile on the bun. Yes, adds character. Burger looks toward ball direction (eyes offset toward ball). A lively detail.

HUD combo: combo increases if block broken within 2 seconds of last break; multiplier text "x3!" popup. Score: base 50 * row value * combo.

Ball physics: standard reflection, angle from paddle hit position. Speed increases slightly per hit, up to max. Ball lost when y > bottom. Multi-ball: array of balls; only lose life when all balls gone.

Power-ups: types: 'multi' (heart, spawn 2 extra balls), 'wide' (gold star? make it a big heart = wide burger 12s). Falling speed ~2.2px/frame with sway. Catch radius = paddle rect. Floating label text "+MULTIBALL".

Sound (WebAudio, created on first gesture):
- Paddle: soft "boop" square/triangle, pitch varies with hit position.
- Wall: short tick.
- Block destroy: pop + rising sparkle based on combo (two oscillators).
- Power-up catch: happy arpeggio.
- Life lost: descending tone.
- Win: little fanfare.
Keep as small helper function with oscillator + gain envelope. Master gain ~0.25. Mute toggle button in HUD (M key). Mute on by default before start — no sound anyway. Add sound toggle button in HUD.

Controls: mouse move on canvas moves paddle (pointermove). Touch: touchmove. Keyboard: ArrowLeft/Right or A/D move. Space/click to launch ball when stuck. P pause? Keep minimal: click to start, maybe Esc to pause — skip pause to reduce scope, but the click-to-start overlay is required.

Demo AI: while state === 'demo': paddle.targetX = predicted x. Simple: if ball moving down, move toward ball.x with max speed and small sine wobble; if moving up, drift to center. Launch stuck ball automatically after 0.6s. On power-ups the AI catches them (since it follows ball, close enough). Ensure demo doesn't lose: give the AI generous speed and no error; but occasional losing a life is fine for showing that effect too. Keep perfect tracking with max speed slightly above ball horizontal speed.

Overlay design: large title on top of the game canvas — overlay of DOM inside the canvas. "BURGER BREAKOUT" with the burger drawn on canvas behind. Overlay content: title, tagline "Every brick explodes into hearts ♥", flashing "Click or press any key to start" text, control hints. Behind: demo AI keeps playing. DOM overlay with pointer-events none except that a click anywhere starts it. Blinking "insert coin" style text with CSS animation — arcade-y.

Game over/win: show overlay again with score, "Click to play again". Demo doesn't end game over: in demo, if lives hit 0, silently reset demo.

Responsive: canvas logical size fixed say 920x640 (or 900x620), scale via CSS to fit container width with aspect. Use devicePixelRatio scaling for crispness. Container max-width ~960px, centered. HUD DOM above. Below 375px everything shrinks; ensure HUD wraps.

Viewport 1280x800: center column with title, HUD, canvas — should fit within 800 height: title ~70px, HUD ~54px, canvas height — if width 920 with canvas 900x600 aspect 1.5 → height 600 + frame ~ total 750. Tight but ok; set canvas aspect to 900x580? Let's use logical 960x600. Container max-width 1024 with padding. On 800px height, page can scroll slightly — acceptable but aim to fit: header compact (~64), HUD ~48, canvas 600*(w/960). With max-width 980 → canvas display ~ 940 wide x 587 high; total ≈ 64+48+590+footer ≈ 730 < 800 ✓.

Background floating hearts: absolutely-positioned DOM hearts (span with ♥ text) animated with CSS keyframes to rise, low opacity, varied sizes/delays, plus two radial gradients + subtle grid/noise via repeating-linear-gradient. Generate ~18 spans in JS. Alive background.

HUD details: left: SCORE with big monospace number; center: HI-SCORE (session only). Lives as hearts: 3 heart icons that pop-animate on loss; COMBO indicator appears. Right: LEVEL badge, sound toggle button. Style HUD as an arcade scoreboard strip: dark panel with inset border, monospace gold numbers. Score numbers animate on score (pop scale via CSS class).

Title header: "BURGER" + "BREAKOUT" chunky Impact style, with burger emoji and heart. Layered text-shadow for depth (pink/gold offset shadows — arcade marquee). Also marquee bulbs? Add small blinking bulb dots around the canvas frame via CSS repeating gradient border? Can make frame with dotted bulbs: use box-shadow dots, too heavy; simpler: top strip of bulbs using repeating-linear-gradient with hue animation. Keep classy: chunky frame with gradient border and inner glow, plus corner "rivets".

Fonts: display: Impact/'Arial Black' fallback; body/HUD: ui-monospace/'Courier New' for arcade numbers; hints text use system-ui. Good.

Code structure:
HTML skeleton:
- <header> logo.
- <main> .cabinet > .hud + .screen (canvas + .overlay)
- <footer> hints.
- <style> all.
- <script> game.

JS outline:
- const W=960,H=600; canvas setup with DPR resize function (fit to container CSS width, canvas.width = cssW*dpr, ctx.setTransform(scale...)). Actually simpler: canvas.width=W*dpr etc, ctx.scale(dpr,dpr), CSS width 100% via aspect-ratio: 960/600. So draw in logical coords. Resize handled automatically by CSS; pointer coords map via rect scaling.

- State machine: 'demo' | 'title'? Demo IS the behind-title state with AI playing. On input → 'playing' with fresh setup. On death with lives 0 → 'gameover' overlay (only if from playing; if demo, auto-reset). On blocks cleared → next level with a banner "LEVEL 2 ♥" popup.

Entities:
- paddle {x,y,w,h,baseW, squash, targetX, wideTimer}
- balls [] {x,y,vx,vy,r,stuck, stuckOffset}
- bricks [] {x,y,w,h,color,hp,alive,row, wob}
- particles [] hearts {x,y,vx,vy,rot,vr,size,life,maxLife,color, sway, swaySpeed}
- popups [] {x,y,text,life,color,size}
- powerups [] {x,y,type,t}
- shake {t, mag}
- combo {count, timer}
- score, hiscore(session), lives, level, muted.

Heart draw function: path scaled by s: standard:
ctx.moveTo(0, s*0.35); bezier left lobe, right lobe, to bottom point. Classic parametric:
moveTo(0, -0.25s)? Use a known one:
```
function heartPath(ctx, s){
  ctx.beginPath();
  ctx.moveTo(0, s*0.3);
  ctx.bezierCurveTo(s*0.5, -s*0.3, s*1.1, s*0.25, 0, s);
  Hmm.
```
Standard heart bezier (origin top center-ish):
```
ctx.moveTo(0, 0.35*s);
ctx.bezierCurveTo(0, 0.32*s...
```
Use this widely-used one (centered):
```
ctx.moveTo(0, s*0.25);
ctx.bezierCurveTo(-s*0.5, -s*0.35, -s, s*0.15, 0, s*0.9);
ctx.bezierCurveTo(s, s*0.15, s*0.5, -s*0.35, 0, s*0.25);
```
Roughly a heart. Let's define: heart at (0,0) with size s meaning half-width ≈ s:
```
p.moveTo(0, -s*0.4);
p.bezierCurveTo(s*0.5, -s*1.0, s*1.3, -s*0.1, 0, s*0.8);
p.bezierCurveTo(-s*1.3, -s*0.1, -s*0.5, -s*1.0, 0, -s*0.4);
```
That gives a heart with top notch at -0.4s and bottom tip at 0.8s. Should look fine at small sizes. Test mentally: start at top notch (0,-0.4s). First curve control (0.5s,-1.0s) upper right, control2 (1.3s,-0.1s), endpoint bottom tip (0,0.8s): forms right lobe. Then mirror back for left lobe. Yeah, that's a heart.

Brick collision: circle-rect AABB; reflect based on penetration side. For simplicity and arcade feel: on hit, determine overlap and flip vx or vy accordingly. Destroy block → spawnHearts(brick) big burst: count ~ 26 + size factor; velocity: random angle mostly upward, speed 1-6; colors from brick's palette + pink tones. Also spawn 2-3 larger hearts. Score popup at brick center. Shake mag += min( ...). Sound pitch by combo.

Ball-paddle: if ball.y+r >= paddle.top && ball within paddle x range && vy>0: reflect with angle = (ball.x - paddle.cx)/(paddle.w/2) → angle max 60°, speed maintained; set vy negative. Squash paddle. Sound. If ball is stuck, on click/space launch.

Power-up spawn: on brick destroy, rand<0.18 → spawn a falling heart type multi (pink heart) or wide (gold heart) 50/50. Fall at vy 2.4, sway in x by sin. Catch → apply: multi: 2 extra balls spawned from caught position at angles; wide: paddle.w = baseW*1.5 for 12s (timer). Popup text.

Level formations:
- makeHeart(): 9 cols; pattern strings above; brick w computed: playfield margin 24 each side; bw = (W-2*margin - (cols-1)*gap)/cols; h=22. top offset 70 (below HUD top area inside canvas; since HUD is DOM, top of canvas can start at 16 + maybe level banner). Use top 48.
Row colors: gradient from #ff4d8d → #ff7ab8? Use array: ['#ff5d8f','#ff7aa8','#ff9e5e','#ffc857','#7be8c3']? For heart formation use red/pink: rows all pink/red shades.
- makeStripes(): cols 11, rows 5, skip some pattern (checker offset), colors by row (candy). Some blocks hp2 (dark chocolate) on level ≥2.
- makeDiamond(): diamond-shaped mask grid 11x9.

Level cycle formations array [heart, stripes, diamond]; level n uses formations[(n-1)%3], speed base + (n-1)*0.35.

Ball speed: base 5.2 + level*0.3, max 8.5; on paddle hit slight increase 0.02 up to cap? Keep constant per level to avoid runaway; simpler: speed set on launch, +0.15 per brick destroyed up to max. Hmm — go with gradual speed: on each brick destroyed speedFactor += 0.004.

Frame loop with fixed-ish dt: use requestAnimationFrame with dt clamp, step physics by dt/16.67 multiplier for consistency.

Paddle AI (demo): target = predicted intercept. Simple version: if any ball going down, pick nearest descending ball, target = ball.x + small sin wobble; else center. Move paddle.x toward target with max speed 7*dtFactor. Also auto-launch stuck ball after 45 frames.

Player input: pointermove on canvas → paddle.targetX (with lerp for smoothness). Keyboard: while key held set velocity ±9 (track in set). Use keyboard in 'playing'.

Overlay (DOM):
#overlay with .panel:
- .ov-title "BURGER BREAKOUT"
- .ov-sub "the paddle is a burger · every brick bursts into hearts"
- .ov-start blinking "CLICK OR PRESS ANY KEY TO START"
- .ov-controls "MOUSE / ← → to move · SPACE to launch · catch falling hearts"
Game over variant changes text: JS sets innerHTML.

Overlay pointer-events none; listen for input on window: click/keydown anywhere → if demo/gameover → startGame(). During 'playing', keydown space launches stuck ball. Prevent scroll on space: preventDefault on Space/Arrows.

Start transition: quick white/pink flash, sound ctx resume, overlay hides (class .hide → opacity 0, pointer-events none).

In-play level complete: banner popup big text "LEVEL CLEAR ♥" then next formation with a small delay. Implementation: state 'playing'; when bricksGone, bannerTimer set, spawn next level after 90 frames, reset ball to stuck.

Lives: draw DOM hearts; update function toggles classes (pop + grey out). Actually keep DOM hearts in HUD: three ♥ spans; on loss, add .lost (grey, shrink).

Session high score: keep in memory only; show "HI 12345".

Extra juice:
- Ball trail: save last ~6 positions per ball and draw fading circles with pink tint.
- Burger eyes look at nearest ball.
- On paddle hit, spawn 3 small crumb particles? spawn 2 sesame particles — cute. Small white ellipses falling.
- Background inside canvas: subtle vertical gradient + faint dot grid + slow drifting faint big hearts (2-3, alpha 0.04) for depth.
- Vignette via radial gradient overlay draw (cheap: precompute? just fillRect with gradient every frame, fine).

Performance: particle cap: if particles.length > 900, splice oldest. Hearts drawn with save/translate/rotate per particle — 900 fine.

Edges: iframe sandbox — fine. No storage APIs.

Accessibility: aria-label on buttons; but demo iframe — fine.

Now overlay should be present at first paint with demo playing behind. Overlay semi-transparent dark so demo visible: background rgba(16,6,20,0.55). Title huge.

Title styling: layered shadows: color cream, text-shadow: 0 2px 0 #ff4d8d? Better: -webkit-text-stroke maybe. Impact with letter-spacing, pink/gold offset shadow:
text-shadow: 3px 3px 0 #ff3d7f, 6px 6px 0 rgba(0,0,0,.35). And slight rotate? Keep straight. Add a small "♥" blinking.

Header layout: logo left-center with mini badges: "🍔 PADDLE" "💥 HEARTS" — hmm, keep header compact: logo center with burger and heart flanking. Since this is a game page, center the arcade marquee for the cabinet is appropriate for the genre. Fine.

Footer: small line: "MOUSE or ← → move · SPACE launch · M mute" plus "catch the falling hearts ♥".

Sound toggle button in HUD: 🔊/🔇 text button.

Now writing the code carefully. Aim for clean, well-organized code. Let me write.

Key constants:
W=960,H=600. Wall margin: play area full canvas; wall = canvas edges; draw thin side wall stripes? Draw subtle walls.

Brick geometry: marginX=26; topY=64; gap=6; heart pattern cols=9 → bw=(960-52-8*6)/9=(960-52-48)/9=860/9≈95.5 h=24. Rows 9 → height 9*24+8*6=216+48=264, top 64 → bottom 328. Paddle y=H-46. OK.

Stripes: cols=11, rows=5: bw=(960-52-10*6)/11=(960-52-60)/11=848/11≈77. h=26, gap6. Pattern: skip some pattern like (r+c)%3==0? Make it look designed: rows offset; just full rows with alternating colors, plus hp2 middle row.
Diamond: pattern 11x9:
```
".....X.....",
"....XXX....",
"...XXXXX...",
"..XXXXXXX..",
".XXXXXXXXX.",
"..XXXXXXX..",
"...XXXXX...",
"....XXX....",
".....X.....",
```
Nice.

Brick colors per row arrays:
heart: ['#ff2e63','#ff5d8f','#ff7aa8','#ff9ac0','#ffb3cf','#ff9ac0','#ff7aa8','#ff5d8f','#ff2e63'] rows, top→bottom red→pink→red.
stripes: ['#ff2e63','#ff7a5c','#ffc857','#7be8c3','#7ab8ff']? Candy rainbow: pink, coral, gold, mint, sky. Multiple accents.
diamond: gradient golds/pink alternating.

Brick draw: rounded rect r=6, vertical gradient (lighter top), inner highlight line, small heart icon in center (darker shade). hp2 bricks: draw with darker chocolate + heart in gold, cracks appear on hp 1 after first hit (draw crack line). Simplify: hp2 color '#8a4b2d' chocolate with cream heart; after hit, draw crack line. 

Block break burst color: from palette of brick color + ['#ff2e63','#ff5d8f','#ff8fb3','#ffc2d4'] mix.

Popup text: '+150' small monospace, and combo 'x3' shown separately near paddle? Combo HUD centered bottom? Place combo popup at brick: "x3 +150".

Combo logic: comboTimer=120 frames; on destroy: if timer>0 combo++ else combo=1; timer=120; points=brickPoints*combo; brickPoints=50. Score popup shows total.

Now write paddle draw function drawBurger(x,y,w,h,squash):
- apply squash: scale(wScale, hScale) around center bottom.
Layer heights (ratios of h=46):
 topBun 0.42h, lettuce 0.12, cheese 0.12, patty 0.2, bottomBun 0.18. Slight overlap.
Drawing order bottom to top:
 bottomBun: rounded rect w, h*0.18 at bottom, color #e69a3c, bottom darker.
 patty: rounded rect w*1.04 wider, h*0.22, color #7a4020, darker bottom; specks.
 cheese: w*1.0, thin h*0.1 #ffc23d + drips: 3 triangles below edges.
 lettuce: wavy: series of half-circles hanging, color #86d96c, w*1.06.
 tomato: thin red strip maybe skip to reduce clutter; skip, lettuce+cheese enough. Actually a thin tomato adds color: h*0.07 #ff5a4e w*0.98. ok.
 topBun: dome: ellipse w/2 wide, h*0.5 tall? Draw as path: rounded top: use ctx.ellipse for a half-ellipse: ctx.ellipse(cx, bunBaseY, w/2, hTop, 0, PI, 0) then close along bottom → dome. Color gradient #f2b04e→#d98c2f; highlight arc; sesame: 6 small cream ellipses rotated randomly (precomputed positions relative).
 Face: two black eyes on top of bottomBun? Usually kawaii face is on the top bun. Eyes at dome center: white circles + black pupils offset toward ball direction; smile arc. Blush circles pink alpha. Cute!

Burger tilt: slight rotation by paddle velocity — nice: rotate by clamp(vel*0.02).

Eye pupil target: direction toward nearest ball; compute dx,dy normalize * 2px.

Now DOM/CSS writing.

CSS palette variables:
--bg1:#160a1c; --bg2:#2a0f33; --pink:#ff2e88; --hotpink:#ff5da2; --gold:#ffc857; --coral:#ff7a5c; --mint:#7be8c3; --cream:#fff3e6; --panel:#221028;

Body: background: radial-gradient(1200px 700px at 20% -10%, #3b1344, transparent 60%), radial-gradient(1000px 600px at 85% 110%, #40102a, transparent 60%), linear-gradient(#160a1c,#0e0612). Plus subtle dots: background-image radial-gradient(circle, rgba(255,255,255,.03) 1px, transparent 1px) size 26px.

Floating heart spans: .amb container fixed inset overflow hidden z 0; hearts animation floatUp linear infinite; random left, size, delay, duration, opacity 0.12-0.3, color pink/gold; use ♥ character with text-shadow glow.

Cabinet: .cab { position relative; border: 3px solid #4b2354; border-radius: 18px; background: linear-gradient(#241026,#180a1d); box-shadow: 0 20px 60px rgba(0,0,0,.6), 0 0 0 6px #120716, 0 0 40px rgba(255,46,136,.15), inset 0 1px 0 rgba(255,255,255,.06); } Inner .screen {position:relative;} canvas {display:block;width:100%;height:auto;aspect-ratio:960/600? width 100% and height auto will work with canvas attribute size (with ratio of width/height attributes).} Also set canvas style aspect-ratio for safety.

Marquee bulbs: strip along cabinet top with .bulbs: height 10px background: repeating-linear-gradient(90deg, gold dots)? Use repeating radial-gradient: background-image: radial-gradient(circle 3px, var(--gold) 3px, rgba(255,200,87,.15) 4px, transparent 5px); background-size: 26px 10px; animation: blink alternate opacity/color shift. Use filter for 2 color layers alternating? Simplify: animate background-position-x to chase: bg-pos 0→26px linear 1s infinite. Plus color alternation via hue? Keep gold chase lights.

HUD: display flex; gap; background #1b0c20 inset border; numbers font-family:'Courier New' big bold gold with glow. Hearts: span.hearts with red color, .lost → color:#4a3050 transform scale(.8).

Overlay: absolute inset 0, display flex column center, gap, background: radial-gradient(ellipse at center, rgba(30,8,32,.55), rgba(12,4,16,.82)); backdrop-filter maybe blur(2px) — keep minimal, maybe not; add slight blur(2px). Transition opacity .4s. .hidden{opacity:0; pointer-events:none}.

Title in overlay: font Impact 64px cream with pink shadow; sub line; blinking start text (animation: blink steps). Controls hint smaller monospace, dim.

Also show "DEMO" tag in overlay corner? Show "AUTO-DEMO PLAYING" small. Nice touch: .demo-tag top-left "● DEMO" blinking.

Game over panel contents dynamically replaced: container #ovInner.

Now JS details. Let me write completely and carefully check correctness.

```js
const W=960,H=600;
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
let DPR=1;
function fit(){
  DPR=Math.min(2,window.devicePixelRatio||1);
  cv.width=W*DPR; cv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
}
fit();
```
CSS scales display; drawing coords are logical. Resize listener refits (though width attr fixed; DPR only changes on zoom — call once plus on resize for safety).

Pointer mapping: rect=cv.getBoundingClientRect(); px=(e.clientX-rect.left)/rect.width*W.

State:
```js
let state='demo'; // demo | play | over
let score=0,hi=0,lives=3,level=1,combo=0,comboT=0;
let shakeT=0,shakeM=0,flashT=0,levelBanner=0;
let frame=0,muted=false;
let launchT=0; // auto-launch timer in demo
```
Paddle:
```js
const paddle={x:W/2,y:H-46,w:112,baseW:112,h:46,vx:0,targetX:W/2,squash:0,wide:0,tilt:0};
```
Balls array; createBall(stuck=true).

Bricks: array; count alive.

Particles etc arrays.

Formations:
```js
const HEART=[
"XX...XX",?
```
Let me construct 9-wide heart:
Row0: ".XX...XX." → wait 9 chars: . X X . . . X X . = ".XX...XX." ✓ (9)
Row1: "XXXX.XXXX"? Standard heart second row more full: "XXXXXXXXX"? Classic pixel heart:
```
.XX...XX.
XXXX.XXXX
XXXXXXXXX
XXXXXXXXX
XXXXXXXXX
.XXXXXXX.
..XXXXX..
...XXX...
....X....
```
Row1 has a notch: "XXXX.XXXX". Good, 9 rows x 9 cols.

DIAMOND rows (width 11) as above.

buildLevel(n):
```js
bricks.length=0;
const f=(n-1)%3;
if(f===0) buildMask(HEART, heartColors, 24);
else if(f===1) buildStripes();
else buildMask(DIAMOND, diamondColors, ...);
```
buildMask(pattern, colorsFor(row,col), gap): cols=pattern[0].length; bw=(W-2*MX-(cols-1)*gap)/cols; rows... For heart with 9 cols and bw ~95, h=26 looks good.

Brick object: {x,y,w,h,c1,c2,hp,max:hp,alive:true, wob:Math.random()*6}
c1 top color, c2 darker: compute via helper shade(hex, amt)? Simpler: give palette rows {main, dark} pairs. Define color pairs per row:
heartPalette=[['#ff2e63','#c9184a'],['#ff4d7e','#d62d5c'],['#ff6f9c','#e04a75'],['#ff8fb3','#e56a8d'],['#ffb3cf','#e98aa8'], ... mirrored]
Rows 9: mirrored at index 4.
stripes: rows of candy pairs:
[['#ff2e63','#c9184a'],['#ff7a5c','#e05a3a'],['#ffc857','#e0a63a'],['#7be8c3','#4fc79e'],['#7ab8ff','#4f8fe0']]
diamond: gold/pink alternate: rows alternate between gold pair and pink pair.

buildStripes: cols=11 rows=5 gap6 h=26; skip pattern: alternate (c + r%2)%2 skip? That gives checker — could look nice too. Instead do full rows but middle row hp2 for "choco" block. All bricks hp1, except rows 1 and 3 have every 3rd brick hp2 (chocolate). hp2 color chocolate ['#8a4b2d','#5f3018'] with cream heart.

Speed: ballSpeed=5+0.35*(n-1) capped 8.2.

Ball:
```js
function newBall(stuck){return {x:paddle.x,y:paddle.y-12,vx:0,vy:0,r:8,stuck:!!stuck,trail:[]};}
```
On launch: angle=-PI/2 + rand(-.3,.3); speed=ballSpeed.

Physics update per frame (dt as frame ratio):
- Update paddle:
  - If demo: AI.
  - Else: if keys held: paddle.targetX += dir*9*dt? Use velocity: paddle.x += dir*9*dt; else lerp to targetX (mouse). Simplify: always paddle.x += clamp(target - x, -14*dt,14*dt) with target set by mouse/AI; keyboard adjusts targetX continuously. Then compute vx = newX-oldX for tilt.
  - Clamp x to [w/2+10, W-w/2-10].
  - Wide timer decrement → lerp width to target (base or 1.5x): paddle.w += (targetW - paddle.w)*0.15.
  - Squash decay: squash*=0.85.
- Update balls:
  - If stuck: x=paddle.x+off, y=paddle.y-12; demo auto-launch after launchT; play: wait for space/click or auto after 2s? Let player click; small "CLICK TO LAUNCH" hint text above paddle if stuck & play.
  - Else: move with substep: steps=Math.ceil(speed/6); loop step: x+=vx*dt/steps etc, check wall collisions:
    - x-r<8 wall (draw wall at 8? wall thickness 0, use 0): x<r → x=r, vx=abs(vx), sound tick, small spark.
    - x>W-r → mirror.
    - y<r+? top: y<r → y=r, vy=abs(vy).
    - Paddle collision when vy>0 and y+r>=paddle.y-? use top of patty as collision line: py=paddle.y+8? Define paddleTop=paddle.y+6 (top of bun). If ball.y+r>=paddleTop && ball.y-r<=paddleTop+20 && |ball.x-paddle.x|<=paddle.w/2+r*0.6 → bounce. angle=(ball.x-paddle.x)/(paddle.w/2) clamped [-1,1]; ang = angle*1.05 (max ~60deg): vx=sin*speed, vy=-cos*speed with speed=current magnitude; clamp min vy to -speed*0.45? cos small when angle 1 → nearly horizontal; clamp angle to ±0.95 rad? Max angle 65°: a=angle*(65/90)*? compute: vx=sin(theta)*sp, vy=-cos(theta)*sp, theta=angle*1.13 (max 65°). Also add paddle influence: vx+=paddle.vx*0.2.
    - Brick collision: iterate alive bricks, AABB circle test; on hit: resolve reflection: compare ball center and brick center, determine side via overlap:
      ```
      const cx=clamp(ball.x,b.x,b.x+b.w), cy=clamp(ball.y,b.y,b.y+b.h);
      const dx=ball.x-cx, dy=ball.y-cy;
      if(dx*dx+dy*dy<=r*r){ hit }
      ```
      Then decide flip: if |dx|>|dy| vx=sign(dx)*|vx| else vy=sign(dy)*|vy|; edge case where ball center inside rect (cx==ball.x etc) → flip vy. Only handle one brick per substep (break) for stability.
      damage: hp--; if 0 → destroyBrick else crack effect + small particles + sound thud. Push ball slightly out.
  - Lost: ball.y-r>H+20 → remove; if balls empty → loseLife().
- Update powerups: fall, sway, catch check (if y within paddle.y..+h and |x-paddle.x|<w/2+10) → apply; remove if y>H+30.
- Update particles, popups, combo timer, shake decay, banner timer.
- Check level clear: alive==0 → levelClear(): banner 100 frames; then level++, buildLevel, reset ball to single stuck, (retain score/lives). If demo: keep going. In play, show "LEVEL n" banner text via popup.
- Check win? Infinite levels; keep cycling. "win" state doesn't need to exist.

loseLife(): lives--; update DOM hearts; flashT=20; shake; sound sad; if lives<=0: if demo → silently reset demo (score=0,lives=3,level=1,buildLevel, ball stuck, keep state demo); else state='over', show over overlay with score, save hi.
Else respawn single ball stuck.

destroyBrick(b, ball): b.alive=false; aliveCount--; combo logic; score+= pts; update HUD; spawn hearts: burst(b); popup; maybe spawn powerup; shake(4+combo, 8); sound pop(combo).

burst(b): count=26; for each: particles.push(heart particle at random pos within brick, velocity: ang=rand(-PI,0) weighted upward: ang=-PI/2+ (rand-0.5)*2.2; sp=1.5+rand*4.5; vx=cos*sp, vy=sin*sp - 1; size 3+rand*6; color pick from [b.c1,'#ff2e63','#ff5d8f','#ff8fb3','#ffc2d4','#ffd7e6']; rot random; vr ±0.15; life 60+rand*50; sway rand.
Also 6 larger "chunk" hearts size 8-11 with slower fade? Same generator covers.
Also spawn a ring: rings.push({x,y,r:4,vr:5,life:14,c:b.c1}).
Cap: if particles.length>1100 → particles.splice(0, particles.length-1100).

Particle update: vy+=0.12 gravity; vx*=0.99; x+=vx+Math.sin((life)*swayFreq)*swayAmp? use p.wob += p.ws; x+=vx+Math.sin(p.wob)*0.4. life--; alpha=min(1,life/25). Remove at life<=0 or y>H+30.

Particle draw: ctx.save translate rotate scale(1, 0.85+0.3*sin(wob)) for flap. Fill heart path with size. Colors solid; no shadow.

Score popup: {x,y,txt,c,life:50,vy:-0.8,size} draw with monospace bold; alpha by life.

Sound:
```js
let AC=null;
function ac(){ if(!AC){AC=new (window.AudioContext||window.webkitAudioContext)();} if(AC.state==='suspended')AC.resume(); return AC;}
function beep({f=440,f2,t=0.1,type='square',v=0.2,delay=0}){ if(muted||!AC)return; ... create osc+gain, envelope exp ramp }
```
Only init AC on first user gesture (inside start handler). Demo silent.
Sounds: sTick (wall): f 220 square short v.06; sPaddle: triangle 180→320 based on hit; sPop(combo): square f=300+combo*40 + a second osc sparkle with fifth; sPower: arpeggio [523,659,784,1046] with 60ms intervals; sLose: saw 300→120 0.4s; sLaunch: 400→700 quick.

HUD DOM ids: #score,#hi,#level,#hearts (three spans), #sound button, #combo? Show combo as floating text near score: add #combo chip that appears (class show) when combo≥2, text "COMBO x3".

Input:
```js
function pressStart(){ // any key/click
 if(state==='demo'||state==='over'){ startGame(); }
 else if(state==='play'){ launchStuck(); }
}
window.addEventListener('keydown',e=>{ if(e.repeat)return; const k=e.code; if(['Space','ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.code))e.preventDefault(); if(k==='KeyM'){toggleMute();return;} keys.add(k); pressStart(); });
window.addEventListener('pointerdown',e=>{ if(e.target.closest('#sndBtn'))return; pressStart(); });
cv.addEventListener('pointermove',e=>{ if(state!=='play')return; const r=cv.getBoundingClientRect(); paddle.targetX=(e.clientX-r.left)/r.width*W; });
```
Keyboard movement in update: if keys has left/right: paddle.targetX=paddle.x±10*dt... Actually targetX approach conflicts with mouse; keyboard: paddle.x directly ±8.5*dt and set targetX=x. ok.

startGame(): state='play'; score=0;lives=3;level=1;combo=0; buildLevel(1); balls=[newBall(true)]; powerups=[]; hide overlay; updateHUD; hearts reset; ac(); launch sound? On click ensure AC resume.
Also add hint "CLICK / SPACE TO LAUNCH" text near stuck ball during play.

Game over show: overlay innerHTML with "GAME OVER", score, hiscore, "CLICK OR PRESS ANY KEY TO PLAY AGAIN". state='over'. Behind, game freezes? Keep particles animating as backdrop: keep rAF updating particles and shake but pause balls/bricks? Simplest: keep full simulation running during 'over' but without balls (balls empty) — paddle idles with AI? Make it attract: switch paddle to AI drift. Fine: allow paddle AI when state!=='play'.

Level banner: levelBannerT frames with text "LEVEL "+level+" ♥"; draw big center text with scale pop.

Demo AI launch: stuck balls get launchT++ and if >50 launch.

HUD update: cheap per-frame DOM text updates? Only on change: keep lastScore etc.

Now drawing order per frame:
1. Clear; draw bg: gradient fill; faint dot grid (pre-draw pattern? draw loop dots 40px alpha .03 — 24*15=360 dots/frame ok; or pre-render to offscreen once). Pre-render bg to offscreen canvas once: gradient + dots + vignette-ish. Do: bgCan = create offscreen; draw once; drawImage each frame. Big faint hearts drifting: 3 objects with positions updated slowly, draw heart large alpha .04 pinkish — draw over bg.
2. apply shake translate (save, translate rand*mag).
3. walls: side glow strips subtle.
4. bricks (skip dead): rounded rect gradient, gloss, small heart emblem, crack if hp<max. Slight idle wobble? Keep static but on hit flash (flashT property): brick.flash=6 set on damage → draw white overlay alpha flash/6.
5. powerups: draw as hearts with glow circle pulsating.
6. particles hearts.
7. rings (stroke circle expanding, alpha).
8. balls + trail.
9. paddle burger.
10. popups text; banner; stuck hint.
11. flash overlay (red-ish on life loss / pink on level clear) flashT.
12. restore.

HUD DOM updates: score count-up animation? Simple immediate with pop class retrigger:
```js
function setScore(v){scoreEl.textContent=v; scoreEl.classList.remove('pop'); void scoreEl.offsetWidth; scoreEl.classList.add('pop');}
```
Pop animation is cheap.

Combo chip: show/hide.

Hearts UI: build spans: for i<3 span ♥; function updHearts(){ spans.forEach((s,i)=> s.classList.toggle('lost', i>=lives)); } On loss, add 'break' animation to the one just lost.

Now CSS blink animation:
@keyframes blink{0%,55%{opacity:1}56%,100%{opacity:.15}} .ov-start{animation:blink 1.1s steps(1) infinite}.

Ambient hearts JS generate: 16 spans with random --x, size, duration, delay, hue class. CSS: .amb i{position:absolute;bottom:-40px;left:var(--x);font-size:var(--s);color:var(--c);opacity:var(--o);animation:rise var(--d) linear infinite;animation-delay:var(--dl); text-shadow:0 0 12px currentColor;} @keyframes rise{to{transform:translateY(-110vh) rotate(30deg)}}. Negative delays so they appear scattered on first paint — important: use negative delays so hearts are visible immediately.

Overlay contents initial (demo/title):
```
<div class="tag">AUTO-DEMO · WATCH THE BURGER PLAY</div>
<h1>BURGER<span>BREAKOUT</span></h1> — maybe 2 lines with burger emoji between? DOM emoji fine. Actually overlay could include a canvas-drawn burger? Use 🍔 text big? Rendering varies but ok. Draw "BURGER BREAKOUT" with 🍔 and 💖 flanking.
<p class="sub">the paddle is a juicy burger — every brick you smash bursts into a shower of hearts</p>
<div class="ov-start">▶ CLICK OR PRESS ANY KEY TO START ◀</div>
<div class="ctl">MOUSE or ← → move · SPACE / CLICK launch · catch falling ♥ for power-ups · M mute</div>
```
Keep compact so demo visible: overlay gradient more transparent at center? Title in top area, start in middle-lower, demo action visible in middle. Use justify-content:space-around with padding. Make title area opaque strip? Simplify: column centered with gap; keep bg fairly transparent (rgba(18,6,22,.45)).

Game over overlay: replace .ovInner innerHTML:
```
<h1>GAME OVER</h1>
<div class="final">SCORE <b>12 340</b></div> (+NEW BEST! if applicable)
<div class="ov-start">CLICK OR PRESS ANY KEY TO PLAY AGAIN</div>
```

Level clear popup: banner text drawing on canvas: "LEVEL CLEAR!" then "LEVEL 2". Implementation: on clear: banner={t:110,txt1:'LEVEL CLEAR ♥',txt2:'GET READY'}; during t>55 show txt1 else txt2; then build next. Pause balls during banner? Balls likely all dead? No — level clears when ball destroys last brick, ball(s) alive. Should next level only build after all balls lost? Classic Breakout just transitions: clear balls, banner, new level with stuck ball. Do: clear balls, banner, then new stuck ball.

Powerup types visual: multi = pink heart with a small x2 label? Draw heart + white '×2' text above? Keep: multi heart is hotpink pulsating; wide heart is gold bigger. On catch: popup 'MULTIBALL!' / 'WIDE BURGER!'.

Wide: targetW=baseW*1.45 for 12s; stack refresh.

Multi: spawn 2 balls from current ball position with velocities rotated ±25°: for each ball b: create clones with vx=cos(θ±),...; cap total balls at 6.

Sound toggle button: place absolute top-right of HUD: 🔊 / 🔇.

Now HUD numbers: font-family:'Courier New',monospace; font-weight:bold; color gold; text-shadow glow. Label small letter-spaced pink.

Title header:
```
<header>
 <div class="logo"><span class="lg-b">🍔</span><div class="tt"><h1>BURGER BREAKOUT</h1><p class="tagline">SMASH BRICKS · BURST HEARTS · KEEP THE BURGER ALIVE</p></div><span class="lg-h">💖</span></div>
</header>
```
h1 Impact 40px letter-spacing 2, color cream, shadow: 0 3px 0 #ff2e63, 0 6px 0 rgba(0,0,0,.4). Pulse scale on logo emoji via CSS.

Footer: small centered dim monospace: hints + "made with ♥ and extra pickles".

Ensure width 375px: HUD wraps: flex-wrap; canvas full-width; overlay text scales with clamp(). Header h1 clamp(22px,5vw,40px). Use clamp throughout.

Prevent horizontal scroll: body overflow-x hidden.

DPR sharpness: canvas width=W*DPR; CSS width 100%; no problem.

One more detail: "iframe only receives keyboard events after click" — start message covers.

Now write full file carefully. Ensure no console errors: careful with undefined variables.

Order inside script:
1. Helpers (clamp, rand, pick, rounded rect)
2. canvas setup + bg offscreen
3. ambient hearts DOM generation
4. sound
5. game state & entities
6. formations & buildLevel
7. particles/burst/popups/rings
8. burger draw
9. update logic
10. render
11. loop
12. input handlers
13. HUD functions
14. init: buildLevel(1), ball, start rAF, show overlay default.

BG offscreen: const bg=document.createElement('canvas'); bg.width=W;bg.height=H; draw gradient: linear top #2b1034 → bottom #150818; radial pink glow top-center alpha .12; dot grid: x step 32: circle r1 rgba(255,255,255,.028); vignette: radial transparent→rgba(0,0,0,.35) edges. Draw once.

Drifting ambient hearts inside canvas: array of 3 {x,y,s,sp,ph}: slowly rise, wrap. alpha .05 color '#ff5da2'. Draw before bricks.

Heart draw helper:
```js
function heart(c,x,y,s,rot){c.save();c.translate(x,y);if(rot)c.rotate(rot);c.beginPath();c.moveTo(0,-s*0.35);c.bezierCurveTo(s*0.55,-s*1.05,s*1.35,-s*0.05,0,s*0.85);c.bezierCurveTo(-s*1.35,-s*0.05,-s*0.55,-s*1.05,0,-s*0.35);c.fill();c.restore();}
```
Wait path: from top notch, right lobe to bottom tip, then back via left lobe. My bezier: start (0,-.35s); C(cp1 .55,-1.05)(cp2 1.35,-.05)(end 0,.85) right side; then mirror C(-1.35...)(-.55...)(0,-.35). Yeah, closes at start. Fill.

Brick emblem: draw heart with darker shade alpha .35 size h*0.22 centered.

Rounded rect helper rr(x,y,w,h,r).

Paddle draw details (concretely):
```js
function drawPaddle(){
 const p=paddle;
 const w=p.w,h=p.h,x=p.x,y=p.y;
 ctx.save();
 ctx.translate(x,y+h); // pivot bottom center
 ctx.rotate(p.tilt);
 const sq=p.squash; // 0..1
 ctx.scale(1+sq*0.18,1-sq*0.22);
 ctx.translate(-x,-(y+h));
 const L=x-w/2,T=y,B=y+h;
 // bottom bun
 ctx.fillStyle='#e0973f'; rr(L+2,B-h*0.20,w-4,h*0.20,7); fill; darker bottom: fill '#c97f2e' rect lower half? Simplify gradient: create linear per layer? Use 2 fills: main + a bottom shading arc. Simplify: solid + thin darker rect at bottom of that layer.
 ...
}
```
Define layers (h=46):
 bottomBun: y0=B-10, hh=10
 patty: y0=B-19, hh=11 (wider: L-4,w+8)
 cheese: y0=B-22, hh=5 plus drips
 tomato: y0=B-26,hh=4.5 (inset 4)
 lettuce: y0=B-29,hh=5 wavy bottom
 topBun dome: base y=B-28, rise 20 → apex y=y≈ B-48 ≈ T. Total ~48 ≈ h fine.

Colors:
 bunTop gradient #f6b95c→#dd8f34; sesame #ffe9c9 small ellipses; shine arc rgba(255,255,255,.35).
 lettuce #7ed35f with scallops: fill base rect then half-circle scallops r5 spaced 10 at bottom.
 tomato #ff5a4e.
 cheese #ffc23d drips: triangles at cheese bottom width 12 height 8, positions L+w*0.2,0.5,0.8.
 patty #6f3a1f with darker #572b14 bottom, specks darker dots.
 bottom bun #e59a41.
 Face: dome: eyes at x±w*0.13, y=B-38: white r4.5 circle, pupil r2.2 black offset toward aim direction; blink occasionally (every ~240 frames scaleY) — cute: pupil normal, eyelid: skip eyelids, add blink via drawing white rect? Keep simple: skip blink, add blush circles rgba(255,120,150,.35) r3 at x±w*0.22,y=B-33. Smile: arc stroke #5b3a1a width 2 at (x, B-33) r5 0..PI.

Pupil aim: nearest ball dx normalize*2.

Shadow under burger: ellipse dark alpha .3 at B+4 before burger.

Trail: for each ball keep array of {x,y}; push per frame, cap 7; draw with decreasing alpha radius pinkish (#ff9ac0) additive? Use globalAlpha.

Ball draw: gradient? Simpler: fill #ff5a4e? make ball a "meatball": #b0552e with highlight, or tomato red #ff4d4d with green stem? Small leaf on top for tomato is adorable: draw small green leaf triangle on top. Tomato ball fits burger theme (ingredient!). Ball = tomato: red circle, darker bottom shading, small highlight, small green star leaf on top. Rotate with movement? Keep static leaf. Trail hearts: every 5 frames spawn a small fading heart particle at ball position (alpha .5, size 2.5, life 20, no gravity? add p.grav=0 flag).

Ring effect: stroke circle expanding.

Now update loop timing: let last=performance.now(); function loop(t){ raf; dt=min(2.5,(t-last)/16.667); last=t; frame++; update(dt); render(); }.

Combo timer decrement by dt.

Write destroyBrick:
```js
function destroyBrick(b,bx){ // bx=ball for direction
 b.alive=false; aliveCount--;
 combo = comboT>0? combo+1 : 1; comboT=130;
 const pts=50*combo; score+=pts; if(score>hi)hi=score;
 setScoreHUD();
 burst(b);
 popups.push({x:b.x+b.w/2,y:b.y,txt:(combo>1?'x'+combo+' ' '')+'+'+pts,c:'#ffd7e6',life:55,vy:-1,size:combo>2?20:16});
 shake(3+Math.min(6,combo),7);
 sPop(combo);
 if(Math.random()<0.16) spawnPower(b);
}
```
shake(m,t): shakeM=Math.max(shakeM,m); shakeT=Math.max(shakeT,t).

burst(b):
```js
const n=26+((Math.random()*10)|0);
const cols=[b.c1,'#ff2e63','#ff5d8f','#ff8fb3','#ffc2d4','#fff0f5'];
for(i..n){
 const px=b.x+Math.random()*b.w, py=b.y+Math.random()*b.h;
 const a=-Math.PI/2+(Math.random()-0.5)*2.4, sp=1.2+Math.random()*4.6;
 parts.push({x:px,y:py,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-0.8,s:2.6+Math.random()*5.5,rot:Math.random()*6.3,vr:(Math.random()-.5)*.25,c:pick(cols),life:55+Math.random()*45,g:0.11+Math.random()*.05,wob:Math.random()*6,ws:.15+Math.random()*.2,fl:Math.random()*6});
}
if(parts.length>1100)parts.splice(0,parts.length-1100);
rings.push({x:b.x+b.w/2,y:b.y+b.h/2,r:6,life:16,c:b.c1});
```
Particle update:
```js
p.vy+=p.g*dt; p.vx*=0.995; p.x+=p.vx*dt+Math.sin(p.wob)*0.5*dt?
```
wob+=ws*dt; x+=vx*dt; y+=vy*dt; rot+=vr*dt; life-=dt; flap=0.8+0.25*Math.sin(p.fl+=0.3*dt).
Draw: alpha=Math.min(1,p.life/22); ctx.globalAlpha=alpha; heart(ctx,p.x,p.y,p.s,p.rot) with scaleX flap: incorporate in heart call? Add scale param: use ctx.save translate rotate scale(flap,1) then path. Implement heart with optional sx.

Popup draw: font 'bold '+size+'px "Courier New",monospace'; textAlign center; alpha=life/55; y+=vy*dt.

Brick draw with flash and crack:
```js
function drawBrick(b){
 const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
 g.addColorStop(0,lighten? use b.c1); g.addColorStop(1,b.c2);
 rr(...r5) fill g;
 gloss: fillRect top 40% rgba(255,255,255,.18) with r? Use small rr at top with radius alpha .18.
 emblem heart: ctx.fillStyle='rgba(0,0,0,.18)' heart center size b.h*0.26.
 Actually brighter emblem: white alpha .25.
 if(b.hp<b.max) draw crack: strokeStyle rgba(0,0,0,.35) zigzag line.
 if(b.flash>0){ctx.globalAlpha=b.flash/6*0.7; white fill; reset}
}
```
Precreate gradients per brick in build (store b.g? Gradients tied to coords; fine, create once at build).

Powerup draw: pulsating scale=1+0.15*sin(frame*0.15+p.t); glow: radial? Draw a soft circle rgba color .25 r 16 then heart size 11 in color; plus small sparkle rotation. On catch: burstSmall of 12 hearts at paddle.

Apply powerup:
```js
function applyPower(pu){
 if(pu.type==='multi'){ const src=balls[0]||{x:paddle.x,y:paddle.y-20,vx:0,vy:-5};
   const sp=ballSpeed; for of [-0.45,0.45]: if(balls.length<6){const a=-Math.PI/2+of; balls.push({x:paddle.x,y:paddle.y-16,vx:Math.sin(a)*sp? note vx=sp*Math.sin(a), vy=-sp*Math.cos(a)... use consistent formulas: vx=sp*sin(a), vy=-sp*cos(a) with a=0 upward.}
   popup 'MULTIBALL ♥' gold
 } else { paddle.wide=720; popup 'WIDE BURGER!' }
 sPower();
}
```
Angle convention: earlier paddle bounce: vx=sin(theta)*sp, vy=-cos(theta)*sp. Use same on launch: a=rand; vx=sp*sin(a); vy=-sp*cos(a).

Paddle bounce:
```js
const rel=clamp((ball.x-paddle.x)/(paddle.w/2+ball.r*0.5),-1,1);
const th=rel*1.1; // max ~63deg
const sp=Math.max(ballSpeed, Math.hypot(ball.vx,ball.vy));
ball.vx=sp*Math.sin(th)+paddle.vx*0.25;
ball.vy=-Math.abs(sp*Math.cos(th));
// enforce min vertical
if(ball.vy>-2.2)ball.vy=-2.2;
ball.y=paddleTop-ball.r-0.5;
paddle.squash=1; sBounce(rel);
spawn 2 crumbs.
```
paddleTop=paddle.y+14? Bun apex at y (B-48 relative)... paddle.y is top of burger (T). Collision top = paddle.y+2. Use paddle.y+4.

Paddle y: paddle.y=H-64 → burger occupies 48px, bottom at H-16, leaves margin. Set paddle.y=H-66, h=48. Ball lost when y>H+30.

Wall bounce sound subtle.

AI details:
```js
function aiPaddle(dt){
 let target=W/2;
 let best=null,bd=1e9;
 for(const b of balls){ if(b.stuck)continue; const t=(paddle.y-b.y)/Math.max(0.1,b.vy); if(b.vy>0&&t<bd){bd=t;best=b;} }
 if(best){ target=best.x+best.vx*Math.min(bd,40)*0.6; target+=Math.sin(frame*0.05)*6; }
 else if(powerups.length){target=powerups[0].x;}
 paddle.targetX=clamp(target,paddle.w/2+10,W-paddle.w/2-10);
 movePaddleToward(dt, 8.5);
}
```
movePaddleToward(dt,max): const d=paddle.targetX-paddle.x; const step=clamp(d,-max*dt,max*dt); paddle.vx=step/Math.max(dt,0.001)*?? For tilt use (newx-oldx)/dt: paddle.vx=(newX-oldX)/(dt||1); store as per-frame vel: vxFrame=newX-oldX; tilt=clamp(vxFrame*0.04,-0.12,0.12); tilt lerp.

Player control: pointermove sets targetX; keyboard:
```js
if(keys.has('ArrowLeft')||keys.has('KeyA'))paddle.targetX=paddle.x-11*dt*??
```
targetX is goal and movement clamped at max speed 14/frame — set targetX=paddle.x-(11) etc for keyboard; each frame relative from current x: paddle.targetX=paddle.x-200*dt? That compounds; simpler: set targetX=paddle.x-40 (goal ahead), lerp speed 14 → effectively move 14*dt. Same for right. Works.

Max lerp speed: mouse flicks also clamped 16*dt — fine for arcade feel and near ball.

Ball substepping: speed max ~9, brick h 26 → 1 step enough? At 45° 9px/frame might cut corner; use 2 substeps always when sp>6. steps=Math.ceil(sp/5).

Also prevent ball from getting stuck horizontal: if |vy|<1.5 normalize boost: vy sign*1.5 renormalize? Occasionally apply: if(Math.abs(ball.vy)<1.6){ball.vy=ball.vy<0?-1.6:1.6; const sp=...} simpler: after wall/bounce, enforce minimum |vy| 2 via rescale:
```js
const sp=Math.hypot(vx,vy); if(Math.abs(vy)/sp<0.25){ const sgn=vy>=0?1:-1; vy=sgn*sp*0.28; vx=Math.sign(vx||1)*Math.sqrt(sp*sp-vy*vy);}
```
After brick and paddle interactions apply.

HUD DOM structure:
```html
<div class="hud">
 <div class="hud-box"><span class="lbl">SCORE</span><span id="score" class="val">0</span></div>
 <div class="hud-box"><span class="lbl">HI-SCORE</span><span id="hi" class="val dim">0</span></div>
 <div class="hud-box combo" id="comboBox"><span class="lbl">COMBO</span><span id="combo" class="val pink">x1</span></div>
 <div class="hud-box"><span class="lbl">LEVEL</span><span id="level" class="val mint">1</span></div>
 <div class="hud-box"><span class="lbl">LIVES</span><span id="hearts" class="hearts"><b>♥</b><b>♥</b><b>♥</b></span></div>
 <button id="sndBtn" title="sound">🔊</button>
</div>
```
On small screens hide hi? flex wrap is fine.

Combo box pulse when >1: toggle class hot.

Now overlay initial HTML markup:

Also "DEMO" badge: place .modeTag top-left inside screen: shows "AUTO-DEMO" when demo, "1P" while playing? Cute: element updated by state.

Level clear transition: on aliveCount==0:
```js
function levelClear(){
 state? if during play: banner={t:120}; sWin(); clear powerups; balls.length=0;
 after timer in update: level++, buildLevel(level), balls=[newBall(true)], banner=null, ballSpeed=Math.min(8.4,5+0.35*(level-1));
}
```
Implement banner as object {t}. Update in update: if(banner){banner.t-=dt; if(banner.t<=0){...apply next...}}. Draw: if t>60 show "LEVEL CLEAR ♥" else "LEVEL "+(level+1)+" — GET READY". Scale pop: s=1+0.2*sin? Use ease.

Demo too uses same path (level increments, fine).

flashT color: loseFlash red: draw rgba(255,46,99,flash/20*0.25). On clear: pink white.

Initial spawn: buildLevel(1); balls=[newBall(true)]; updateHUD.

Sound button handler: toggle muted, update label; stopPropagation so click doesn't start game? But click should also start game maybe; the button is in HUD outside screen; pointerdown on window triggers pressStart regardless of target except when target is sndBtn. If user clicks mute during demo → probably shouldn't start game; exclude: if(e.target.closest('#sndBtn'))return.

Also keydown M toggles mute without starting? Our keydown handler calls toggleMute and returns before pressStart — good, and also not start. But M during play fine too.

Enter/Space during over → restart via pressStart.

Edge: pointerdown also fires on canvas touch → will also move; add touchmove handler on cv for touch drag: e.preventDefault, map touch. Use pointermove with pointerType any — pointermove fires on touch drag too after pointerdown. Enough; add CSS touch-action:none on canvas.

Prevent context menu? Skip.

Also pause on tab hidden? rAF auto-pauses; dt clamp handles resume jump. Good.

Now assemble final code. Watch out for syntax. Also include <meta viewport>.

Also check: no localStorage used. No alert. Good.

Ambient hearts generation JS:
```js
const amb=document.getElementById('amb');
const cols=['#ff2e63','#ff5da2','#ffc857','#ff8fb3'];
for(let i=0;i<16;i++){const s=document.createElement('i');s.textContent='♥';
 s.style.setProperty('--x',(Math.random()*100)+'vw');
 s.style.setProperty('--s',(10+Math.random()*22)+'px');
 s.style.setProperty('--o',0.08+Math.random()*0.16);
 s.style.setProperty('--d',(14+Math.random()*18)+'s');
 s.style.setProperty('--dl',(-Math.random()*30)+'s');
 s.style.color=cols[i%4];
 amb.appendChild(s);}
```
CSS:
#amb{position:fixed;inset:0;overflow:hidden;z-index:0;pointer-events:none}
#amb i{position:absolute;bottom:-8vh;left:var(--x);font-size:var(--s);opacity:var(--o);font-style:normal;animation:rise var(--d) linear infinite;animation-delay:var(--dl);text-shadow:0 0 14px currentColor}
@keyframes rise{to{transform:translateY(-115vh) rotate(40deg)}}

Content wrapper z-index 1 position relative.

Layout container:
.wrap{max-width:1000px;margin:0 auto;padding:14px 16px 22px;position:relative;z-index:1}

Header .logo{display:flex;align-items:center;justify-content:center;gap:14px;margin:6px 0 12px}
.lg{font-size:clamp(26px,4vw,40px);animation:bob 2.4s ease-in-out infinite}
h1.ttl{font-family:Impact,'Arial Black',...;font-size:clamp(26px,4.6vw,44px);letter-spacing:3px;color:#fff3e6;text-shadow:0 3px 0 #ff2e63,0 7px 18px rgba(255,46,99,.45);line-height:1}
.tagline{font:11px monospace;color:#c78ba6;letter-spacing:2.5px;margin-top:5px;text-align:center}

Cab bulbs: .cab::before? Use a div .bulbs inside .cab top: position absolute top:-2? Place strip inside at top with margin: .bulbs{height:8px;margin:10px 14px 0;background-image:radial-gradient(circle at 4px 4px,#ffd98a 2.6px,rgba(255,200,87,.25) 3.4px,transparent 4.4px);background-size:22px 8px;animation:chase 1s linear infinite;opacity:.9}
@keyframes chase{to{background-position-x:22px}}
Second row below screen too.

.screen{position:relative;margin:10px 14px 14px;border-radius:12px;overflow:hidden;border:2px solid #46204e;box-shadow:inset 0 0 40px rgba(0,0,0,.5)}
canvas{display:block;width:100%;height:auto;background:#1c0b22;touch-action:none}

Overlay: .ov{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(8px,1.6vh,16px);text-align:center;background:radial-gradient(ellipse at 50% 40%,rgba(32,8,36,.38),rgba(10,4,14,.78));transition:opacity .45s;padding:16px;z-index:5;pointer-events:none}
.ov.hidden{opacity:0}
.ov .tag{font:700 10px/1 monospace;letter-spacing:3px;color:#7be8c3;border:1px solid rgba(123,232,195,.4);padding:6px 10px;border-radius:99px;background:rgba(9,20,17,.4)}
.ov h2{font-family:Impact...;font-size:clamp(30px,6vw,58px);color:#fff3e6;letter-spacing:3px;text-shadow:0 3px 0 #ff2e63,0 8px 24px rgba(255,46,99,.5);margin:0;line-height:.95}
.ov h2 .br{display:block;font-size:.5em;letter-spacing:6px;color:#ffc857;text-shadow:0 2px 0 #a3671b}
Actually two lines: "BURGER" large and "BREAKOUT" maybe. Composition: <h2>BURGER BREAKOUT</h2> single-line clamp.
.sub{color:#ffd7e6;font-size:clamp(11px,1.6vw,14px);max-width:52ch; font-family:system-ui}
.start{font-family:monospace;font-weight:bold;color:#ffe9c9;font-size:clamp(13px,2vw,18px);letter-spacing:2px;padding:10px 18px;border:2px dashed rgba(255,233,201,.55);border-radius:10px;background:rgba(30,10,32,.45);animation:blink 1.15s steps(1) infinite}
.ctl{font:10.5px monospace;color:#b98ca8;letter-spacing:1.5px}
.final{font:700 clamp(16px,3vw,24px) monospace;color:#ffc857;letter-spacing:2px}
.nb{color:#7be8c3;font:bold 12px monospace;letter-spacing:2px;animation:blink .7s steps(1) infinite}

modeTag: .mode{position:absolute;top:10px;left:12px;z-index:4;font:bold 10px monospace;letter-spacing:2px;color:#7be8c3;background:rgba(6,14,12,.55);border:1px solid rgba(123,232,195,.35);padding:5px 9px;border-radius:99px;pointer-events:none}
Also small "SND M" hint bottom-right inside screen: .mini{...} maybe skip.

HUD CSS:
.hud{display:flex;align-items:stretch;gap:8px;margin:10px 14px 0;flex-wrap:wrap}
.hbox{flex:1;min-width:96px;background:linear-gradient(#2a1130,#1b0a20);border:1px solid #4b2354;border-radius:10px;padding:7px 12px;display:flex;flex-direction:column;gap:2px;box-shadow:inset 0 1px 0 rgba(255,255,255,.05)}
.lbl{font:700 9px monospace;letter-spacing:2.5px;color:#c76b98}
.val{font:bold 20px/1 'Courier New',monospace;color:#ffc857;text-shadow:0 0 12px rgba(255,200,87,.35)}
.val.pink{color:#ff5da2}.val.mint{color:#7be8c3}.val.dim{color:#8f6b7f;text-shadow:none}
.hearts b{color:#ff2e63;font-size:18px;margin-right:3px;text-shadow:0 0 10px rgba(255,46,99,.6);display:inline-block;transition:.25s}
.hearts b.lost{color:#3c2140;text-shadow:none;transform:scale(.75)}
.hearts b.pop{animation:heartpop .4s}
@keyframes heartpop{40%{transform:scale(1.6) rotate(-12deg)}}
#comboBox{transition:.2s} #comboBox.hot{border-color:#ff2e63;box-shadow:0 0 14px rgba(255,46,99,.35), inset...}
#score.pop{animation:pop .25s} @keyframes pop{40%{transform:scale(1.25)}}
.val transform-origin left.
#sndBtn{...}

Footer: footer{margin-top:12px;text-align:center;font:10.5px monospace;color:#8f6b7f;letter-spacing:1.6px}
footer b{color:#ff5da2}

Now full JS. Let me draft carefully with the tricky parts in my head.

Potential bug areas:
- heart() path with scale param.
- buildMask generic:
```js
function buildMask(rows,rowColor,bh){
 const cols=rows[0].length,gap=6,MX=26;
 const bw=(W-MX*2-(cols-1)*gap)/cols;
 rows.forEach((row,r)=>{ [...row].forEach((ch,c)=>{ if(ch==='X'){ const [c1,c2]=rowColor(r,c); bricks.push(mkBrick(MX+c*(bw+gap),64+r*(bh+gap),bw,bh,c1,c2)); } });});
}
```
Heart palette function:
```js
const HP=[['#ff2e63','#c9184a'],['#ff4d7e','#d92f63'],['#ff6f9c','#e24a78'],['#ff8fb3','#e56a90'],['#ffb3cf','#e98aad']];
rowColor:r=>HP[Math.min(r,8-r)]
```
Heart row 9: min(r,8-r) 0..4 ✓.

Diamond rows (11 wide):
```js
const DIA=['.....X.....','....XXX....','...XXXXX...','..XXXXXXX..','.XXXXXXXXX.','..XXXXXXX..','...XXXXX...','....XXX....','.....X.....'];
color:(r,c)=> (r+c)%2? gold pair : pink pair
```
Stripes:
```js
function buildStripes(){
 const cols=11,rows=5,gap=6,MX=26,bh=26;
 const bw=(W-MX*2-(cols-1)*gap)/cols;
 const pal=[['#ff2e63','#c9184a'],['#ff7a5c','#e0563a'],['#ffc857','#dfa33a'],['#7be8c3','#4cc49c'],['#8ab6ff','#5a86e0']];
 for(r..)for(c..){ let pr=pal[r];let hp=1;
   if(level>1&&(r===1||r===3)&&c%3===1){pr=['#8a4b2d','#5f3018'];hp=2;}
   push mkBrick(...,pr0,pr1,hp) }
}
```
mkBrick(x,y,w,h,c1,c2,hp=1): {x,y,w,h,c1,c2,hp,max:hp,alive:true,flash:0,grad:null} create gradient lazily in draw? Create at build: g=ctx.createLinearGradient(0,y,0,y+h) — but ctx transform DPR is setTransform logical coords, gradient tied to coords not transform state at creation; fine to create with logical coords.

Brick count for heart: row counts 4+8? Compute: row0 ".XX...XX."=4,row1 "XXXX.XXXX"=8,rows2-4=9*3=27,row5 7,row6 5,row7 3,row8 1 → 55 blocks. Good.

aliveCount=bricks.length.

Ball speed var ballSpeed=5.0.

newBall:
```js
function newBall(){return{x:paddle.x,y:paddle.y-14,vx:0,vy:0,r:8,stuck:true,trail:[],off:(Math.random()-.5)*30};}
```
stuck follows paddle.x+off.

launch(ball): ball.stuck=false; const a=(Math.random()-.5)*0.7; ball.vx=Math.sin(a)*ballSpeed; ball.vy=-Math.cos(a)*ballSpeed; sLaunch();
launchStuck(): balls.filter stuck → launch first.

update(dt):
```js
frame++;
// paddle width target
const tw=paddle.wide>0?paddle.baseW*1.5:paddle.baseW;
paddle.w+=(tw-paddle.w)*0.12*dt; clamp dt effect fine.
if(paddle.wide>0)paddle.wide-=dt;
// control
if(state==='play'){ keyboard adjust; }
else aiPaddle(dt);
// hmm during 'over' also AI. During play if no input? Fine.
movePaddle(dt);
paddle.squash*=Math.pow(0.86,dt);
// balls
for(i=balls.length-1;i>=0;i--){...}
if(balls.length===0&&!banner){ if(state==='play') loseLife(); else resetDemo(); }
```
Careful: after loseLife, might still be alive>0 → respawn ball; guard with flag: only trigger loseLife when balls empty AND !banner AND !respawnPending. Set pendingRespawn timer 40 frames then spawn. Simplify: when balls empty: ballsEmptyT+=dt; if(ballsEmptyT>35){ballsEmptyT=0; if(state==='play')loseLife(); else demoReset();} else skip. But loseLife also spawns stuck ball immediately → balls non-empty. During banner balls also empty — skip while banner. ok add condition !banner.

loseLife(){
 lives--; updHearts(true); flashT=18; flashC='255,46,99'; shake(8,14); sLose();
 if(lives<=0){ gameOver(); } else { balls.push(newBall()); }
}
gameOver(){ state='over'; if(score>hi)hi=score; setHi(); showOver(); }

demoReset(){ score=0;setScoreHUD(); lives=3;updHearts(); level=1; setLevel(); buildLevel(1); balls=[newBall()]; powerups.length=0; }

Ball loop:
```js
for(let i=balls.length-1;i>=0;i--){const b=balls[i];
 if(b.stuck){ b.x=paddle.x+b.off*0.4; b.y=paddle.y-14; if(state!=='play'){launchT++; if(launchT>55){launch(b);launchT=0;}} b.trail.length=0; continue;}
 b.trail.push({x:b.x,y:b.y}); if(b.trail.length>7)b.trail.shift();
 const sp=Math.hypot(b.vx,b.vy); const steps=Math.max(1,Math.ceil(sp/5));
 let dead=false;
 for(let s=0;s<steps&&!dead;s++){
  b.x+=b.vx*dt/steps; b.y+=b.vy*dt/steps;
  if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx);sTick();}
  else if(b.x>W-b.r){b.x=W-b.r;b.vx=-Math.abs(b.vx);sTick();}
  if(b.y<b.r){b.y=b.r;b.vy=Math.abs(b.vy);sTick();}
  // paddle
  if(b.vy>0&&b.y+b.r>=paddle.y+4&&b.y<paddle.y+34&&Math.abs(b.x-paddle.x)<=paddle.w/2+b.r*0.7){
    bouncePaddle(b);
  }
  // bricks
  for(const k of bricks){ if(!k.alive)continue;
    const cx=clamp(b.x,k.x,k.x+k.w),cy=clamp(b.y,k.y,k.y+k.h);
    const dx=b.x-cx,dy=b.y-cy;
    if(dx*dx+dy*dy<=b.r*b.r){
      // resolve
      if(Math.abs(dx)>Math.abs(dy)) b.vx=(dx>0?1:-1)*Math.abs(b.vx);
      else b.vy=(dy>0?1:-1)*Math.abs(b.vy);
      b.x+=b.vx*0.5;b.y+=b.vy*0.5? nudge slightly along dir: b.x+= (dx||0)*0.2... keep simple.
      hitBrick(k); dead=true? no—brick hit doesn't kill ball; dead refers to ball lost. brick hit: just break inner loop: use flag hitB=true; break;
    }
  }
  fixFlat(b);
 }
 if(b.y>H+40){balls.splice(i,1); continue;}
}
```
Use brickHit flag to break brick loop per substep.

fixFlat(b): const sp=Math.hypot(b.vx,b.vy); if(sp>0&&Math.abs(b.vy)/sp<0.22){const s=Math.sign(b.vy)||-1;b.vy=s*sp*0.26;b.vx=Math.sign(b.vx||1)*Math.sqrt(Math.max(0.5,sp*sp-b.vy*b.vy));}

bouncePaddle(b):
```js
const rel=clamp((b.x-paddle.x)/(paddle.w/2),-1,1);
const th=rel*1.08;
const sp=Math.max(ballSpeed,Math.hypot(b.vx,b.vy));
b.vx=Math.sin(th)*sp+paddle.vx*0.3;
b.vy=-Math.cos(th)*sp;
if(b.vy>-2.4)b.vy=-2.4;
b.y=paddle.y+4-b.r-0.5? set above paddle: b.y=paddle.y- b.r+2; hmm collision condition y+r>=paddle.y+4; place b.y=paddle.y+4-b.r-1.
paddle.squash=1; crumbs(b.x,paddle.y); sBounce(rel);
```

hitBrick(k): k.hp--; k.flash=6; if(k.hp<=0){destroyBrick(k);} else { small burst 6 hearts; sThud(); score+=10; setScoreHUD(); }

Update combo timer: if(comboT>0){comboT-=dt; if(comboT<=0){combo=0; comboUI();}}

Powerups update:
```js
for(i--){p.y+=2.1*dt; p.x+=Math.sin(frame*0.06+p.t)*0.8*dt;
 if(p.y>paddle.y&&p.y<paddle.y+paddle.h+10&&Math.abs(p.x-paddle.x)<paddle.w/2+14){applyPower(p);splice}
 else if(p.y>H+30)splice}
```

Particle updates as described. Rings: r+=4*dt; life-=dt.

Banner update:
```js
if(banner){banner.t-=dt; if(banner.t<=0){banner=null; level++; setLevel(); buildLevel(level); balls=[newBall()]; ballSpeed=Math.min(8.4,5+0.32*(level-1)); flashT=10; flashC='255,180,220';}}
```
Trigger levelClear(): banner={t:130}; balls.length=0; powerups.length=0; sClear(); popup? Also score bonus +500.

Popup updates standard.

render():
```js
ctx.clearRect? drawImage(bg,0,0,W,H);
// drifting hearts inside canvas
ctx.save();
if(shakeT>0){shakeT-=1; const m=shakeM*(shakeT/10); ctx.translate((Math.random()-.5)*m,(Math.random()-.5)*m); if(shakeT<=0)shakeM=0;}
```
Note dt: shakeT-=dt. Manage.

Draw bricks, particles behind? Order: bg, drifting hearts, rings, bricks, powerups, particles (hearts above bricks — yes, explosion above), balls, paddle, popups, banner, flash.

Stuck hint during play: if state==='play' && balls.some stuck: draw pulsating text above paddle: 'CLICK OR SPACE TO LAUNCH' small monospace alpha sin.

Banner draw:
```js
if(banner){const t=banner.t;const txt=t>65?'LEVEL CLEAR ♥':'LEVEL '+(level+1);const sub=t>65?'': 'GET READY';
 ctx textAlign center; big Impact 44 fill cream stroke pink shadow; scale pop: sc=1+0.15*Math.sin(clamp((130-t)/12,0,1)*Math.PI)? Simple: alpha fade in.
}
```

Flash: if(flashT>0){flashT-=dt; ctx.fillStyle=`rgba(${flashC},${(flashT/20)*0.28})`;fillRect}

Draw powerup:
```js
function drawPower(p){const pu=1+0.12*Math.sin(frame*0.18+p.t);
 ctx.save();ctx.translate(p.x,p.y);
 halo: ctx.fillStyle=rgba(color,.22); circle r18*pu fill.
 heart size 12*pu color p.c no rotation; inner white small heart alpha .5 size 5 offset -2?
 plus sparkle ✦: two small rotating lines? skip.
 ctx.restore();
}
```
Colors: multi '#ff2e63' label? Draw small '×3' below heart in white? Keep icon + distinguish: wide '#ffc857'. On catch popup shows effect. Also add tiny label under falling heart: small text 'MULTI'/'WIDE' in white 8px — helpful. ok draw label.

drawBall(b): trail first: for j of trail alpha=j/len*0.25 radius b.r*(j/len) color '#ff8fb3'. Tomato: main circle gradient radial #ff6b5e→#e03a3a; bottom shading arc; highlight small white alpha; leaf: at top small two green ellipses rotated ±0.5 color #58c95f; stem small rect. Cute.

drawBrick crack: if hp<max: draw 2 dark stroke lines in zigzag.

Now bg offscreen creation function makeBG():
```js
const bg=document.createElement('canvas');bg.width=W;bg.height=H;const g=bg.getContext('2d');
const lg=g.createLinearGradient(0,0,0,H);lg.addColorStop(0,'#2c1034');lg.addColorStop(.55,'#1d0a24');lg.addColorStop(1,'#140716');g.fillStyle=lg;g.fillRect(0,0,W,H);
const rg=g.createRadialGradient(W/2,-80,50,W/2,-80,520);rg.addColorStop(0,'rgba(255,93,162,.16)');rg.addColorStop(1,'rgba(255,93,162,0)');g.fillStyle=rg;g.fillRect(0,0,W,H);
dots: g.fillStyle='rgba(255,255,255,.03)';for(x=16;x<W;x+=32)for(y=16;y<H;y+=32){g.fillRect(x,y,2,2)? use arc r1.} fillRect cheaper.
vignette: radial center transparent → rgba(0,0,0,.32): create radial (W/2,H/2, H*0.45 → W*0.75).
side walls: subtle gradient strip width 6 pinkish? draw inside render instead? Bake walls: g.fillStyle='rgba(255,46,99,.25)'; fillRect(0,0,4,H) and (W-4,0,4,H); top bar too. ok bake.
```
Drifting hearts inside canvas: amb2=[{x:150,y:400,s:60,sp:.25,ph:0},{x:700,y:200,s:90,...},{x:420,y:520,s:45}]; update: y-=sp*dt; if(y<-100){y=H+80;x=rand}; draw with heart alpha .045 color '#ff5da2' slight rotate by sin(frame*.005+ph).

HUD functions:
```js
const $=id=>document.getElementById(id);
function setScore(pop){$('score').textContent=score; if(pop){const el=$('score');el.classList.remove('pop');void el.offsetWidth;el.classList.add('pop');} $('hi').textContent=Math.max(hi,score);}
```
hi updated live = max(hi,score) display.
comboUI(): box hot class when combo>=2; text 'x'+Math.max(1,combo).
setLevel(): $('level').textContent=level.
updHearts(brokeIdx): const bs=document.querySelectorAll('#hearts b'); bs.forEach((b,i)=>{const lost=i>=lives; if(lost&&!b.classList.contains('lost')&&brokeIdx) {b.classList.add('pop');} b.classList.toggle('lost',lost);});
Simpler: on loseLife, add pop to the lost one (index lives after decrement → element at index lives) then lost.

Overlay show/hide:
```js
function showOverlay(html){ov.classList.remove('hidden'); if(html)ovInner.innerHTML=html; }
function hideOverlay(){ov.classList.add('hidden');}
```
Mode tag text: 'AUTO-DEMO' / 'PLAY' / 'GAME OVER'.

Default overlay HTML is initial DOM markup (so first paint shows even before JS — good: keep static markup in HTML, JS only replaces on game over).

Initial: state='demo' with everything set up; rAF starts immediately.

Sound functions with AC guard:
```js
function tone(f0,f1,dur,type,vol,delay=0){ if(muted||!AC)return; const t0=AC.currentTime+delay; const o=AC.createOscillator(),g=AC.createGain(); o.type=type;o.frequency.setValueAtTime(f0,t0); if(f1)o.frequency.exponentialRampToValueAtTime(Math.max(30,f1),t0+dur); g.gain.setValueAtTime(vol,t0); g.gain.exponentialRampToValueAtTime(0.0001,t0+dur); o.connect(g).connect(AC.destination); o.start(t0);o.stop(t0+dur+.02);}
sTick(){tone(240,200,.05,'square',.05)}
sBounce(rel){tone(180+Math.abs(rel)*80,320,.09,'triangle',.18)}
sPop(c){const f=300+Math.min(8,c)*45;tone(f,f*1.9,.12,'square',.16);tone(f*1.5,f*2.6,.14,'sine',.1,.02);}
sThud(){tone(150,110,.07,'square',.12)}
sLaunch(){tone(330,660,.15,'triangle',.15)}
sPower(){[523,659,784,1047].forEach((f,i)=>tone(f,f,.1,'triangle',.14,i*.07));}
sLose(){tone(300,90,.5,'sawtooth',.16)}
sClear(){[392,523,659,784,1047].forEach((f,i)=>tone(f,f,.12,'square',.12,i*.08));}
```
AC connect destination fine. Create AC only on user gesture in pressStart/startGame: call ac() there.

pressStart():
```js
function pressStart(){
 ac();
 if(state==='demo'||state==='over'){ startGame(); }
 else launchStuck();
}
```
startGame(){ state='play'; hideOverlay(); score=0;lives=3;level=1;combo=0;comboT=0; ballSpeed=5; buildLevel(1); balls=[newBall()]; powerups.length=0; parts.length=0? keep particles from demo for continuity — ok keep but clear popups; setScore();setLevel();updHearts();comboUI(); modeTag 'PLAY'; flashT=8 pink;}
Also launchT=0.

Wait: on start the ball is stuck; player needs to click/space to launch; show hint. Click also came from pressStart... click that starts the game will also call launchStuck immediately? pressStart when state demo → startGame only; the same click won't launch (good, gives player agency). But pointerdown event → launch on next click. Fine. Keyboard too: first key starts; second key launches. ok.

Key handling: e.preventDefault on Space and arrows to avoid page scroll. Note: iframe gets keys after click — fine.

movePaddle(dt):
```js
const max=16*dt;
const d=paddle.targetX-paddle.x;
const mv=clamp(d,-max,max);
const ox=paddle.x;
paddle.x=clamp(paddle.x+mv,paddle.w/2+8,W-paddle.w/2-8);
paddle.vx=paddle.x-ox; // per frame
paddle.tilt+= (clamp(paddle.vx*0.05,-.14,.14)-paddle.tilt)*0.2*dt;
```
Keyboard in play branch before movePaddle:
```js
if(keys.has('ArrowLeft')||keys.has('KeyA'))paddle.targetX=paddle.x-60;
if(keys.has('ArrowRight')||keys.has('KeyD'))paddle.targetX=paddle.x+60;
```
Player max speed 16/frame ~960px/s ok.

AI max speed 8.5 — ball vx max ~8.5; good.

In demo, AI also catches powerups? Falls with x; AI target is ball; might miss — fine, or add: if any powerup y> H-220 → target = powerup.x (only if no immediate descending ball). Add priority: if powerup near bottom and no descending ball within 150px? Keep simple: only when no descending ball target powerup. Good enough; demos usually show powerups sometimes caught.

Popup color param; combo popup bigger.

Crumb particles: use parts with type crumb flag? Add small hearts instead — simpler: spawn 3 small gold hearts at paddle. Consistent theme.

Also ball trail occasional heart: every 6 frames per ball push a tiny particle {s:2.2,life:18,g:0,vy:0.2,vx:0,c:'#ff8fb3'}. Guard count: only when parts.length<800.

Level start popup: on buildLevel during play: popups.push center 'LEVEL '+level big? Banner covers. Skip.

Score popup on powerup: popups.push({x:paddle.x,y:paddle.y-30,txt:'MULTIBALL!',c:'#ffd7e6',life:60,vy:-1,size:18}).

Now burger draw function concretely (write once):

```js
function drawBurger(){
 const p=paddle,w=p.w,x=p.x, B=H-16; // bottom line
 const h=48,T=B-h;
 ctx.save();
 // shadow
 ctx.fillStyle='rgba(0,0,0,.35)';ctx.beginPath();ctx.ellipse(x,B+4,w*0.46,6,0,0,7);ctx.fill();
 ctx.translate(x,B);ctx.rotate(p.tilt);
 const sq=p.squash;ctx.scale(1+sq*.16,1-sq*.2);
 ctx.translate(-x,-B);
 const L=x-w/2;
 // bottom bun
 grad? solid:
 ctx.fillStyle='#e59a41';rr(L+3,B-11,w-6,11,5);ctx.fill();
 ctx.fillStyle='#c97e2e';rr(L+3,B-5,w-6,5,4);ctx.fill();
 // patty
 ctx.fillStyle='#6f3a1f';rr(L-4,B-21,w+8,12,6);ctx.fill();
 ctx.fillStyle='#54290f';rr(L-4,B-14,w+8,5,4);ctx.fill();
 // cheese
 ctx.fillStyle='#ffc23d';rr(L+2,B-25,w-4,6,3);ctx.fill();
 // drips
 ctx.beginPath();ctx.moveTo(L+w*0.18,B-20);ctx.lineTo(L+w*0.18+7,B-12);ctx.lineTo(L+w*0.18+14,B-20);... fill same color; three drips.
 Wait cheese is B-25..B-19; drip goes downward from B-19 to B-12 over patty. ok.
 // tomato
 ctx.fillStyle='#ff5a4e';rr(L+5,B-29,w-10,5,3);ctx.fill();
 // lettuce
 ctx.fillStyle='#7ed35f';rr(L-2,B-33,w+4,6,3);ctx.fill();
 scallops: ctx.beginPath(); for(i...) arc(cx,B-27,5,0,PI) fill;
 Actually scallop half-circles pointing down at y=B-27 radius 5 → bulge downward over tomato. Fill path arc(0..PI) draws lower half. Loop x from L+4 to L+w-4 step 11.
 // top bun dome
 const grd=ctx.createLinearGradient(0,T,0,B-30);grd.addColorStop(0,'#f7bd63');grd.addColorStop(1,'#dd8f34');
 ctx.fillStyle=grd;
 ctx.beginPath();ctx.moveTo(L,B-30);ctx.bezierCurveTo(L,T-6,L+w,T-6,L+w,B-30);ctx.closePath();ctx.fill();
 // dome height: T=B-48; curve apex ≈ T-6+... bezier max ≈ (B-30 + 3*(T-6))/4? apex ~ T+? fine visually approx.
 Actually apex of a cubic with both controls at T-6: y(.5)=(y0+3c1+3c2+y3)/8=(B-30+3(T-6)+3(T-6)+B-30)/8=(2B-60+6T-36)/8 = (2(B+3T)-96)/8 with T=B-48: (2B+6B-288-96)/8=(8B-384)/8=B-48 ✓ apex B-48=T.
 // shine
 ctx.strokeStyle='rgba(255,255,255,.4)';lw 3; arc: ctx.beginPath();ctx.arc(x+? draw arc along dome: ctx.ellipse(x-w*0.16,T+14,w*0.16,7,-0.5,?) simpler: small curved stroke: beginPath moveTo(L+w*0.2,T+16) quadraticCurveTo(L+w*0.38,T+6,L+w*0.58,T+12); stroke.
 // sesame
 ctx.fillStyle='#ffe9c9'; positions [[.22,.62],[.38,.38],[.55,.3],[.7,.52],[.45,.68],[.8,.72]] relative (fx, fy within dome): sx=L+w*fx, sy=T+6+fy*16; ellipse rotate fx*3: ctx.beginPath();ctx.ellipse(sx,sy,3.4,2,fx*2-1,0,7);fill();
 // face
 eyeY=T+26? dome spans T..B-30 (height 18)? B-30-T=18. Eyes at T+11 maybe. Set eyeY=T+12, ex=±w*0.11.
 white r4.2; pupil r2 offset toward aim direction (nearest ball): compute aim=(dx,dy) normalized*1.8; fill #3a2418.
 blush: rgba(255,110,140,.4) circle r3 at x±w*0.2, eyeY+5.
 smile: stroke #5b3a1a lw2 arc(x,eyeY+4,4.5,0.15PI,0.85PI).
 ctx.restore();
}
```
paddle.y used for collision = T (B-48). Define paddle.y=H-16-48=H-64. With H=600 paddle.y=536. Ball lost below H.

nearestBallDir(): compute for eye aim.

Draw ball as tomato:
```js
function drawBall(b){
 // trail
 for(let j=0;j<b.trail.length;j++){const t=b.trail[j];ctx.globalAlpha=(j+1)/b.trail.length*0.22;ctx.fillStyle='#ff8fb3';ctx.beginPath();ctx.arc(t.x,t.y,b.r*(0.4+0.6*(j+1)/b.trail.length),0,7);ctx.fill();}
 ctx.globalAlpha=1;
 const g=ctx.createRadialGradient(b.x-3,b.y-3,1,b.x,b.y,b.r);g.addColorStop(0,'#ff8a72');g.addColorStop(.6,'#f4443c');g.addColorStop(1,'#c92a30');
 ctx.fillStyle=g;ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,7);ctx.fill();
 // leaf
 ctx.fillStyle='#58c95f';ctx.beginPath();ctx.ellipse(b.x-2.5,b.y-b.r+1,3.4,1.8,-.6,0,7);ctx.fill();ctx.beginPath();ctx.ellipse(b.x+2.5,b.y-b.r+1,3.4,1.8,.6,0,7);ctx.fill();
 // shine
 ctx.fillStyle='rgba(255,255,255,.55)';ctx.beginPath();ctx.arc(b.x-3,b.y-3.5,2.2,0,7);ctx.fill();
}
```
0,7 as arc end (7>2π) fine.

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

Banner draw:
```js
if(banner){
 const first=banner.t>65;
 const txt=first?'LEVEL CLEAR!':'LEVEL '+(level+1);
 const sub=first?'♥ so many hearts ♥':'GET READY';
 const pr=first?(130-banner.t)/65: (65-banner.t)/65;
 ctx.save();ctx.globalAlpha=Math.min(1,pr*3, (1-pr)*3+0.2);ctx.textAlign='center';
 ctx.font='900 46px Impact,"Arial Black",sans-serif';
 ctx.fillStyle='#fff3e6';ctx.strokeStyle='#ff2e63';ctx.lineWidth=6;ctx.strokeText? use shadow: fill cream then stroke around. Do: lineWidth 8 strokeStyle 'rgba(255,46,99,.9)' strokeText then fillText.
 ctx.fillText(txt,W/2,H*0.42);
 ctx.font='bold 15px "Courier New",monospace';ctx.fillStyle='#ffc857';ctx.fillText(sub,W/2,H*0.42+30);
 ctx.restore();
}
```

Stuck hint draw:
```js
if(state==='play'&&balls.some(b=>b.stuck)){ctx.globalAlpha=0.6+0.4*Math.sin(frame*0.12);ctx.font='bold 12px "Courier New"';ctx.fillStyle='#ffe9c9';ctx.textAlign='center';ctx.fillText('CLICK OR SPACE TO LAUNCH ♥',paddle.x,paddle.y-30);ctx.globalAlpha=1;}
```
If paddle near edges might overflow canvas; clamp text x to [90,W-90].

Combo HUD updated in destroyBrick and on reset via comboUI().

Now over overlay HTML builder:
```js
function showOver(){
 modeTag.textContent='GAME OVER';
 ovInner.innerHTML=`<h2>GAME OVER</h2>
 <div class="final">SCORE ${score} ${score>=hi&&score>0?'<span class="nb">★ NEW BEST ★</span>':''}</div>
 <div class="start">▶ CLICK OR PRESS ANY KEY TO PLAY AGAIN ◀</div>
 <div class="ctl">the burger believes in you 🍔</div>`;
 ov.classList.remove('hidden');
}
```
hi updated live so score>=hi basically always true if equal to hi... track prevBest on game start: bestAtStart=hi. newBest=score>bestAtStart. Store in startGame: prevBest=hi. Show NEW BEST if score>prevBest && score>0.

startGame reset hi too? No — retain hi across sessions (in memory). hi display = max.

Initial HTML overlay content:
```html
<div id="ovInner">
 <div class="tag">● AUTO-DEMO — THE BURGER PLAYS ITSELF</div>
 <h2>BURGER BREAKOUT</h2>
 <p class="sub">the paddle is a juicy double burger — and every brick you smash explodes into a shower of tiny hearts</p>
 <div class="start">▶ CLICK OR PRESS ANY KEY TO START ◀</div>
 <div class="ctl">MOUSE / ← → move · SPACE or CLICK launch · catch falling ♥ for power-ups · M = sound</div>
</div>
```

Note: overlay must not block viewing demo: bg alpha moderate. ok.

Footer: `MOVE mouse or ← → · SPACE launch · catch falling hearts for MULTIBALL & WIDE BURGER · <b>M</b> mute — made with ♥ + extra pickles`

Check total canvas HUD fit at 1280×800: wrap max 1000 → cab width ~968; screen margin 14 → canvas ~936 wide → height 936*600/960=585. header ~66+hud~62+bulbs/margin ~30+footer 20+canvas 585 + padding ≈ 780 ✓.

One more consideration: pointermove on window? Only within canvas — use cv listener. Also during play, pointer only within canvas area; fine.

Touch too: pointermove fires on touch drag after pointerdown. Add CSS touch-action none ✓. Also prevent double-tap zoom: meta viewport user-scalable=no? Use `user-scalable=no, maximum-scale=1` — acceptable for game.

Keys set: const keys=new Set().

Also ensure Space keydown during 'play' launches stuck ball: pressStart covers (state play → launchStuck).

Edge: pressStart called from pointerdown anywhere including HUD button — excluded sndBtn.

visibilitychange: reset last time to avoid dt spike: document.addEventListener('visibilitychange',()=>{last=performance.now()}) plus clamp anyway.

Now potential subtle bug: on fit() set canvas.width, ctx transform; called once at start; also window resize → refit to update DPR maybe. Add listener.

Canvas CSS: aspect ratio via width/height attributes keeps intrinsic ratio (960x600) regardless of DPR scaling — width attr = W*DPR, height = H*DPR, ratio same ✓.

Gradient creation in buildLevel uses ctx — fine after setTransform.

Ensure heart emblem function reused: function heartPath(c,s){...} then heart(c,x,y,s,rot,sx). Implement:
```js
function heart(c,x,y,s,rot=0,sx=1){c.save();c.translate(x,y);c.rotate(rot);c.scale(sx,1);c.beginPath();c.moveTo(0,-s*0.35);c.bezierCurveTo(s*0.55,-s*1.05,s*1.35,-s*0.05,0,s*0.85);c.bezierCurveTo(-s*1.35,-s*0.05,-s*0.55,-s*1.05,0,-s*0.35);c.closePath();c.fill();c.restore();}
```

Particle draw: ctx.fillStyle=p.c; ctx.globalAlpha=a; heart(ctx,p.x,p.y,p.s,p.rot,flap); then globalAlpha=1.

Rings: ctx.globalAlpha=r.life/16; strokeStyle c; lw 3; arc r.r; stroke.

Powerup spawn:
```js
function spawnPower(b){ if(powerups.length>=2)return; powerups.push({x:b.x+b.w/2,y:b.y, type:Math.random()<0.5?'multi':'wide', t:Math.random()*6}); }
```
Draw: c = type multi? '#ff2e63':'#ffc857'; white label below heart.

applyPower(pu):
```js
if(pu.type==='multi'){ let spawned=0;
 const base=balls.find(b=>!b.stuck)||{x:paddle.x,y:paddle.y-20};
 for(const da of[-0.5,0.5]){ if(balls.length>=6)break; const a=da; balls.push({x:paddle.x,y:paddle.y-20,vx:Math.sin(a)*ballSpeed,vy:-Math.cos(a)*ballSpeed,r:8,stuck:false,trail:[],off:0}); spawned++; }
 popups.push({x:paddle.x,y:paddle.y-40,txt:'MULTIBALL!',...});
} else { paddle.wide=720; popups.push(...'WIDE BURGER!'); }
burst small: for 14 parts hearts around paddle top.
sPower();
```

Also small hearts on catch handled.

Crumb on paddle bounce: spawn 3 small gold hearts.

Now particle count: each brick ~30; multi-break fast → potential 500+; cap 1100 ✓. Perf ok.

Popups font size.

HUD combo element initial 'x1', box not hot.

Also handle hi element initial 0; setHi after game over.

Writing CSS for .val transform-origin: left bottom for pop.

I think ready to write final file. Things to be careful about: no stray references, all ids exist.

Also double-check: in update ball loop, after bouncePaddle setting b.y above paddle, subsequent substeps fine.

Brick hit nudge: after flipping velocity, push ball slightly outside: b.x+=Math.sign(b.vx||0)*1; b.y+=Math.sign(b.vy||0)*1 — may stick if still overlapping next frame, but brick dead/damaged and flash; if hp>0 (choco), ball might re-trigger hit next frame still overlapping → nudge ~2px + velocity flip should exit. Also add per-ball hit cooldown: b.cd frames; if(b.cd>0){b.cd-=dt} skip brick collisions when cd>0; set b.cd=6 after hit. Prevents multi-hit on same brick.

In demo, AI ensures ball doesn't drop: max speed 8.5 vs ball max horizontal at 63° with speed max 8.4: |vx| max ~7.5 <8.5 ✓ mostly catches; occasional miss → shows life-loss effect, then resets on lives 0. Actually lives 3 with 3 misses → demo resets; acceptable, still lively. Slightly raise AI speed to 9: fine.

Also for demo stuck auto-launch launchT increments only when stuck ball exists.

ballsEmptyT variable.

Note: during banner balls empty; we clear balls on levelClear and set banner — loseLife guard !banner ✓. After banner completes spawn new stuck ball.

Also during 'over': balls empty; ballsEmptyT logic would call loseLife in play... state 'over' → treat like demo? else branch in that condition triggers demoReset which would ruin the over screen. Guard: if balls empty && !banner: if(state==='play')... else if(state==='demo') demoReset(); over: do nothing. ✓ And in 'over' paddle AI drift: aiPaddle runs (state!=='play'). Balls none → target center. ok.

gameOver: maybe small heart burst on paddle. Fine skip.

Now let me actually write everything out. Also include title tag "Burger Breakout ♥".

Font stack: display: Impact,'Arial Black','Franklin Gothic Bold',sans-serif. Mono: 'Courier New',ui-monospace,monospace — Courier New bold looks decent for numbers.

Double-check oval arc end angle use 7 (≈2π). ok.

Now write the code, mindful of backticks in JS within template strings and HTML. Use template literals in JS carefully.

Also ensure ovInner default content is static HTML so first paint correct even before JS runs. Overlay visible, canvas behind runs after JS. Good.

Also `#ovInner h2` styling.

Also add slight CRT scanlines inside screen: .screen::after{content:'';position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.025) 0 1px,transparent 1px 3px);mix-blend? keep subtle opacity .5; border-radius inherit} plus ::before for inner vignette? Enough one. pointer-events none important so canvas gets events... overlay div is pointer-events none; canvas receives pointermove/down via window listener anyway (window pointerdown) — but cv pointermove needs event reach: scanlines are pointer-events:none so fine.

Z order: canvas z0, scanlines z2, modeTag z3, overlay z5.

Final check keydown listener: attach to window; e.repeat guard; ignore if typing in input — none exist.

Also ensure Space preventDefault so page doesn't scroll.

Mute button click also triggers window pointerdown → excluded via closest('#sndBtn') ✓. But on start? Fine.

Writing it now.

One more: initial paddle.vx=0, tilt 0, squash 0, wide 0.

demoReset also resets ballSpeed=5, launchT=0, banner null.

startGame likewise resets those.

buildLevel sets aliveCount, and also bricksGlow? Fine.

Ensure destroyBrick uses ballSpeed increase: ballSpeed=Math.min(8.4,ballSpeed+0.008) per destroy — subtle ramp. ok.

Popups draw with textAlign center; reset afterwards? ctx state set each frame in render: wrap text draws with save/restore to avoid leaking font settings.

HUD score element id 'score' etc.

Edge: paddle.wide>0 show glow around burger: if wide active draw gold sparkle ring — small add: draw pulsating outline ellipse gold alpha .3. ok add if(paddle.wide>0).

Time to write final. Careful with backticks in JS, template strings, and HTML.

One more check — hearts HUD: initial 3 <b>♥</b>. On life loss: element index `lives` (after decrement) gets pop+lost.

updHearts on reset: remove lost and pop from all.

Finalize sound button toggle: muted=!muted; textContent=muted?'🔇':'🔊'.

When muted, tone guards.

Also AC creation: `new (window.AudioContext||window.webkitAudioContext)()` inside try/catch for safety (zero console errors guarantee — try/catch prevents exceptions).

OK writing the full file.