Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
3 min 0 s
Cost
$0.017
Output tokens
28,370
Spent thinking
16,760 tokens
Speed
157.8 tok/s
Console errors
0
First token after
748 ms
First visible text after
1 min 60 s
Prompt tokens
541
Code size
31.1 KB
Finish reason
stop
Served by
Fireworks
Model id
minimax/minimax-m3.1-flash
Reasoning sent
model default
Max output
524,288 tokens
Generated
Nov 13, 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 · 850 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Heart Burger Breakout 🍔💖</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;height:100%;}
  body{
    background:
      radial-gradient(1100px 620px at 50% -12%, #3c1436 0%, rgba(0,0,0,0) 62%),
      radial-gradient(900px 560px at 50% 118%, #52190f 0%, rgba(0,0,0,0) 62%),
      #0a0511;
    color:#fff;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    display:flex; align-items:center; justify-content:center;
    padding:14px; overflow:hidden; user-select:none; -webkit-user-select:none;
  }
  #wrap{width:min(98vw,1080px); display:flex; flex-direction:column; gap:10px;}
  #stage{
    position:relative; width:100%; aspect-ratio:1000/640; max-height:calc(100vh - 92px);
    border-radius:20px; overflow:hidden; background:#170b22;
    box-shadow:0 26px 70px rgba(0,0,0,.65), 0 0 0 2px rgba(255,120,150,.16), 0 0 60px rgba(255,60,120,.10);
    touch-action:none;
  }
  canvas{display:block; width:100%; height:100%; cursor:none;}
  #overlay{
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:radial-gradient(600px 320px at 50% 45%, rgba(12,4,20,.72), rgba(8,3,14,.40) 70%);
    backdrop-filter:blur(1.5px); z-index:5; padding:12px;
  }
  .card{
    text-align:center; padding:20px 26px; border-radius:20px; max-width:min(92%,470px);
    background:linear-gradient(160deg, rgba(46,14,44,.94), rgba(20,8,28,.94));
    border:1px solid rgba(255,130,165,.42);
    box-shadow:0 18px 50px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.12);
  }
  .logo{font-size:46px; line-height:1; filter:drop-shadow(0 6px 12px rgba(255,60,120,.5)); animation:bob 1.6s ease-in-out infinite;}
  @keyframes bob{0%,100%{transform:translateY(0) rotate(-3deg)}50%{transform:translateY(-8px) rotate(3deg)}}
  h1{
    margin:6px 0 8px; font-size:clamp(19px,3.4vw,30px); letter-spacing:1.5px; font-weight:900;
    background:linear-gradient(90deg,#ffd36e,#ff6f9c 45%,#ff4d6d);
    -webkit-background-clip:text; background-clip:text; color:transparent;
    text-shadow:0 0 26px rgba(255,80,120,.25);
  }
  .press{
    margin:4px 0 6px; font-size:clamp(14px,2.3vw,19px); font-weight:800; color:#fff;
    animation:pulse 1.25s ease-in-out infinite;
  }
  @keyframes pulse{0%,100%{opacity:1; transform:scale(1)}50%{opacity:.62; transform:scale(.975)}}
  .hint{margin:2px 0 0; font-size:clamp(11px,1.8vw,13.5px); color:#ffc6d8; opacity:.92; line-height:1.5}
  #foot{display:flex; gap:8px; flex-wrap:wrap; justify-content:center; font-size:12.5px; color:#ffd7e4;}
  #foot span{
    padding:5px 11px; border-radius:999px; background:rgba(255,255,255,.07);
    border:1px solid rgba(255,150,180,.25); white-space:nowrap;
  }
  @media (max-width:640px){
    #stage{aspect-ratio:1000/1060; border-radius:14px;}
    .card{padding:14px 16px;}
    .logo{font-size:34px;}
    #foot{font-size:11px;}
    #foot span{padding:4px 8px;}
  }
</style>
</head>
<body>
<div id="wrap">
  <div id="stage">
    <canvas id="game"></canvas>
    <div id="overlay">
      <div class="card" id="card">
        <div class="logo">🍔</div>
        <h1>HEART BURGER BREAKOUT</h1>
        <p class="press">Click or press a key to start</p>
        <p class="hint">A demo bot is playing right now — every brick explodes into a burst of little hearts 💖<br>Click to take over the burger paddle.</p>
      </div>
    </div>
  </div>
  <div id="foot">
    <span>🖱️ Mouse / touch / ← → to move</span>
    <span>Space = serve</span>
    <span>P = pause</span>
    <span>R = restart</span>
  </div>
</div>
<script>
(function(){
  "use strict";
  const stage=document.getElementById('stage');
  const canvas=document.getElementById('game');
  const ctx=canvas.getContext('2d');
  const overlay=document.getElementById('overlay');
  const card=document.getElementById('card');

  /* ---------------- helpers ---------------- */
  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];
  const TAU=Math.PI*2;
  function rr(x,y,w,h,r){
    r=Math.min(r,Math.abs(w)/2,Math.abs(h)/2);
    ctx.beginPath();
    ctx.moveTo(x+r,y);
    ctx.arcTo(x+w,y,x+w,y+h,r);
    ctx.arcTo(x+w,y+h,x,y+h,r);
    ctx.arcTo(x,y+h,x,y,r);
    ctx.arcTo(x,y,x+w,y,r);
    ctx.closePath();
  }
  function hex(c){ return c; }

  /* ---------------- sizing ---------------- */
  let LW=1000, LH=640, scale=1;
  function fit(){
    const r=stage.getBoundingClientRect();
    const dpr=Math.min(window.devicePixelRatio||1,2);
    canvas.width=Math.max(2,Math.round(r.width*dpr));
    canvas.height=Math.max(2,Math.round(r.height*dpr));
    scale=canvas.width/LW;
    LH=canvas.height/scale;
    ctx.setTransform(scale,0,0,scale,0,0);
  }

  /* ---------------- palette ---------------- */
  const HEART_COLORS=['#ff5c8a','#ff3b6b','#ff89ab','#ffa9c4','#e8395f','#ffd3de','#ff477e'];
  const INGREDIENTS=[
    {name:'bun',    c:'#f0a850', d:'#b9712a', light:'#ffd79a'},
    {name:'lettuce',c:'#6fcf57', d:'#3d8b2f', light:'#b6f29c'},
    {name:'tomato', c:'#ef5350', d:'#a52a2a', light:'#ff9a8c'},
    {name:'cheese', c:'#ffd24f', d:'#c59a1a', light:'#fff0a8'},
    {name:'onion',  c:'#b084f0', d:'#6b45a6', light:'#e2ccff'},
    {name:'bacon',  c:'#ff8a65', d:'#c05236', light:'#ffcbb4'}
  ];
  const POWER_TYPES={
    wide:{c:'#7ee787', d:'#2f8f46'},
    multi:{c:'#79c0ff', d:'#1f6feb'},
    slow:{c:'#ffd479', d:'#c08a1c'}
  };

  /* ---------------- game state ---------------- */
  const S={
    mode:'demo', score:0, lives:3, level:1, best:0,
    combo:0, comboT:0, shake:0, paused:false, over:false,
    serveT:0, wideT:0, slowT:0, banner:'', bannerT:0, time:0
  };
  const paddle={x:400,y:560,w:132,h:44, baseW:132};
  const balls=[]; let bricks=[]; const parts=[]; const pops=[]; const powerups=[]; const bgHearts=[];
  let mouseX=null; const keys={};

  const COLS=9, ROWS=6;
  function buildBricks(){
    bricks=[];
    const mx=54, gapX=8, gapY=10, top=104, h=28;
    const w=(LW-mx*2-gapX*(COLS-1))/COLS;
    for(let r=0;r<ROWS;r++){
      for(let c=0;c<COLS;c++){
        const type=(r+S.level-1)%INGREDIENTS.length;
        const tough = r>=ROWS-2 || (r===ROWS-3 && c%3===0);
        bricks.push({x:mx+c*(w+gapX), y:top+r*(h+gapY), w:w, h:h, type:type,
                    hp:tough?2:1, maxhp:tough?2:1, dead:false, flash:0, seed:Math.random()*100});
      }
    }
  }

  function baseSpeed(){ return (5.3+S.level*0.30)*Math.min(1.4, LH/640); }

  function resetBall(){
    balls.length=0;
    S.serveT=0.55;
  }
  function launchBall(){
    const sp=baseSpeed();
    const b={x:paddle.x+paddle.w/2, y:paddle.y-16, r:11, vx:0, vy:0, speed:sp, trail:[]};
    const a=rand(-0.42,0.42);
    b.vx=Math.sin(a)*sp; b.vy=-Math.cos(a)*sp;
    balls.push(b);
  }
  function setBallSpeed(b,s){
    const a=Math.atan2(b.vy,b.vx);
    b.vx=Math.cos(a)*s; b.vy=Math.sin(a)*s; b.speed=s;
  }

  /* ---------------- particles ---------------- */
  function burstHearts(x,y,count,extraColor,power){
    power=power||1;
    const cols=extraColor?HEART_COLORS.concat([extraColor,extraColor]):HEART_COLORS;
    for(let i=0;i<count;i++){
      const a=rand(0,TAU), sp=rand(1.4,6.0)*power;
      const life=rand(1.7,3.1);
      parts.push({kind:'heart', x:x+rand(-10,10), y:y+rand(-8,8),
        vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-rand(0.6,2.4),
        rot:rand(0,TAU), vrot:rand(-0.28,0.28), size:rand(3.2,9),
        life:life, life0:life, color:pick(cols), g:0.055, bounce:true});
    }
  }
  function burstCrumbs(x,y,count,color){
    for(let i=0;i<count;i++){
      const a=rand(0,TAU), sp=rand(1,4.5), life=rand(0.4,1.1);
      parts.push({kind:'crumb', x:x, y:y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-1,
        rot:rand(0,TAU), vrot:rand(-0.4,0.4), size:rand(1.6,3.6),
        life:life, life0:life, color:color, g:0.14, bounce:false});
    }
  }
  function popText(x,y,txt,color,size){
    pops.push({x:x,y:y,txt:txt,color:color,life:1.05,life0:1.05,size:size||18});
  }

  /* ---------------- drawing: hamburger ---------------- */
  function drawBurger(cx,cy,w,h,seed){
    seed=seed||0;
    ctx.save();
    ctx.translate(cx-w/2, cy-h/2);
    const r=Math.min(w,h)*0.5;
    // --- bottom bun
    let bw=w*0.94, bx=(w-bw)/2, bh=h*0.20, by=h-bh;
    let g=ctx.createLinearGradient(0,by,0,by+bh);
    g.addColorStop(0,'#f0b464'); g.addColorStop(1,'#d98a3c');
    ctx.fillStyle=g; rr(bx,by,bw,bh,bh*0.42); ctx.fill();
    // --- patty
    let pw=w*1.0, px=(w-pw)/2, ph=h*0.21, py=by-ph*0.62;
    g=ctx.createLinearGradient(0,py,0,py+ph);
    g.addColorStop(0,'#7c442a'); g.addColorStop(.5,'#5d2f1b'); g.addColorStop(1,'#3f1d10');
    ctx.fillStyle=g; rr(px,py,pw,ph,ph*0.36); ctx.fill();
    ctx.fillStyle='rgba(255,220,170,.16)';
    for(let i=0;i<5;i++){
      const cx2=px+pw*(0.12+i*0.19)+Math.sin(seed+i*2.1)*3, cy2=py+ph*(0.3+0.35*Math.abs(Math.sin(seed*1.7+i)));
      ctx.beginPath(); ctx.ellipse(cx2,cy2,ph*0.13,ph*0.07,0.4,0,TAU); ctx.fill();
    }
    // --- cheese
    let cw=w*1.03, cx3=(w-cw)/2, ch=h*0.13, cy3=py-ph*0.55;
    ctx.fillStyle='#ffca35';
    ctx.beginPath();
    ctx.moveTo(cx3,cy3); ctx.lineTo(cx3+cw,cy3); ctx.lineTo(cx3+cw,cy3+ch);
    const nd=5;
    for(let i=nd;i>=0;i--){
      const xx=cx3+cw*i/nd;
      ctx.lineTo(xx-cw/nd*0.18, cy3+ch+ch*0.55);
      ctx.lineTo(xx-cw/nd, cy3+ch);
    }
    ctx.closePath(); ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.25)'; ctx.fillRect(cx3,cy3,cw,ch*0.3);
    // --- tomato
    let tw=w*0.95, tx=(w-tw)/2, th=h*0.10, ty=cy3-th*0.62;
    g=ctx.createLinearGradient(0,ty,0,ty+th);
    g.addColorStop(0,'#ff7b6b'); g.addColorStop(1,'#d3352f');
    ctx.fillStyle=g; rr(tx,ty,tw,th,th*0.45); ctx.fill();
    ctx.fillStyle='rgba(255,255,255,.28)';
    ctx.beginPath(); ctx.ellipse(tx+tw*0.3,ty+th*0.45,tw*0.11,th*0.22,0,0,TAU); ctx.fill();
    // --- lettuce
    let lw=w*1.05, lx=(w-lw)/2, lh=h*0.11, ly=ty-lh*0.55;
    ctx.fillStyle='#63c94e';
    ctx.beginPath();
    ctx.moveTo(lx,ly+lh);
    const seg=6;
    for(let i=0;i<=seg;i++){
      const xx=lx+lw*i/seg;
      ctx.quadraticCurveTo(xx-lw/seg*0.5, ly+lh*1.55, xx-lw/seg, ly+lh);
    }
    ctx.lineTo(lx+lw,ly); ctx.lineTo(lx,ly); ctx.closePath(); ctx.fill();
    ctx.fillStyle='#8ee87a'; ctx.fillRect(lx,ly,lw,lh*0.42);
    // --- top bun
    let tw2=w*0.92, tx2=(w-tw2)/2, th2=h*0.32, ty2=ly-th2*0.86;
    g=ctx.createLinearGradient(0,ty2-th2*0.5,0,ty2+th2*0.5);
    g.addColorStop(0,'#ffd79b'); g.addColorStop(.55,'#f2ab5c'); g.addColorStop(1,'#d98b3c');
    ctx.fillStyle=g;
    ctx.beginPath();
    ctx.moveTo(tx2,ty2+th2*0.5);
    ctx.bezierCurveTo(tx2,ty2-th2*0.08, tx2+tw2*0.22, ty2-th2*0.62, tx2+tw2*0.5, ty2-th2*0.62);
    ctx.bezierCurveTo(tx2+tw2*0.78, ty2-th2*0.62, tx2+tw2, ty2-th2*0.08, tx2+tw2, ty2+th2*0.5);
    ctx.closePath(); ctx.fill();
    // sesame seeds
    ctx.fillStyle='rgba(255,252,230,.92)';
    const seeds=[[0.22,0.34,-0.5],[0.40,0.18,0.25],[0.58,0.15,-0.2],[0.74,0.30,0.45],[0.50,0.40,0.1]];
    for(let i=0;i<seeds.length;i++){
      const sx=tx2+tw2*seeds[i][0], sy=ty2+th2*seeds[i][1];
      ctx.save(); ctx.translate(sx,sy); ctx.rotate(seeds[i][2]);
      ctx.beginPath(); ctx.ellipse(0,0,th2*0.075,th2*0.045,0,0,TAU); ctx.fill();
      ctx.restore();
    }
    // glossy highlight
    ctx.globalAlpha=.22; ctx.fillStyle='#fff';
    ctx.beginPath(); ctx.ellipse(tx2+tw2*0.32, ty2-th2*0.30, tw2*0.18, th2*0.10, -0.25,0,TAU); ctx.fill();
    ctx.globalAlpha=1;
    ctx.restore();
  }

  /* ---------------- drawing: heart ---------------- */
  function drawHeart(x,y,size,rot,color,alpha){
    ctx.save();
    ctx.translate(x,y); ctx.rotate(rot); ctx.globalAlpha=alpha;
    ctx.fillStyle=color;
    const s=size;
    ctx.beginPath();
    ctx.moveTo(0,s*0.95);
    ctx.bezierCurveTo(-s*1.3,s*0.1,-s*0.62,-s*0.95,0,-s*0.28);
    ctx.bezierCurveTo(s*0.62,-s*0.95,s*1.3,s*0.1,0,s*0.95);
    ctx.closePath(); ctx.fill();
    ctx.globalAlpha=alpha*0.55; ctx.fillStyle='#fff';
    ctx.beginPath(); ctx.ellipse(-s*0.36,-s*0.3,s*0.16,s*0.24,-0.5,0,TAU); ctx.fill();
    ctx.restore();
  }

  /* ---------------- drawing: bricks ---------------- */
  function drawBrick(b){
    const ing=INGREDIENTS[b.type];
    ctx.save();
    // body
    const g=ctx.createLinearGradient(0,b.y,0,b.y+b.h);
    g.addColorStop(0,ing.light); g.addColorStop(0.42,ing.c); g.addColorStop(1,ing.d);
    ctx.fillStyle=g; rr(b.x,b.y,b.w,b.h,7); ctx.fill();
    // top gloss
    ctx.globalAlpha=.5; ctx.fillStyle='#fff';
    rr(b.x+5,b.y+3.5,b.w-10,Math.max(2,b.h*0.18),4); ctx.fill();
    ctx.globalAlpha=1;
    // ingredient detail
    ctx.save();
    rr(b.x,b.y,b.w,b.h,7); ctx.clip();
    const cx=b.x+b.w/2, cy=b.y+b.h/2;
    if(ing.name==='bun'){
      ctx.fillStyle='rgba(255,252,225,.85)';
      for(let i=0;i<4;i++){
        const sx=b.x+b.w*(0.18+i*0.21), sy=cy+Math.sin(b.seed+i*1.7)*b.h*0.18;
        ctx.save(); ctx.translate(sx,sy); ctx.rotate(Math.sin(b.seed+i)*0.7);
        ctx.beginPath(); ctx.ellipse(0,0,b.w*0.022,b.h*0.09,0,0,TAU); ctx.fill(); ctx.restore();
      }
    } else if(ing.name==='lettuce'){
      ctx.strokeStyle='rgba(255,255,255,.45)'; ctx.lineWidth=2;
      ctx.beginPath();
      for(let i=0;i<=5;i++){ const xx=b.x+b.w*i/5; ctx.moveTo(xx,b.y+b.h*0.25); ctx.quadraticCurveTo(xx-8,b.y+b.h*0.75,xx-16,b.y+b.h*0.25); }
      ctx.stroke();
    } else if(ing.name==='tomato'){
      ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=2.5;
      ctx.beginPath(); ctx.arc(cx,cy,b.h*0.34,0,TAU); ctx.stroke();
    } else if(ing.name==='cheese'){
      ctx.fillStyle='rgba(255,255,255,.4)';
      ctx.beginPath(); ctx.moveTo(cx-b.w*0.2,cy); ctx.lineTo(cx+b.w*0.2,cy); ctx.lineTo(cx,cy+b.h*0.36); ctx.closePath(); ctx.fill();
    } else if(ing.name==='onion'){
      ctx.strokeStyle='rgba(255,255,255,.45)'; ctx.lineWidth=2;
      ctx.beginPath(); ctx.arc(cx,cy+b.h*0.9,b.h*0.85,Math.PI*1.15,Math.PI*1.85); ctx.stroke();
      ctx.beginPath(); ctx.arc(cx,cy+b.h*0.9,b.h*0.5,Math.PI*1.15,Math.PI*1.85); ctx.stroke();
    } else {
      ctx.strokeStyle='rgba(255,255,255,.4)'; ctx.lineWidth=3;
      for(let i=0;i<3;i++){
        ctx.beginPath();
        ctx.moveTo(b.x+6,cy-b.h*0.25+i*b.h*0.28);
        ctx.bezierCurveTo(cx-b.w*0.2,cy-b.h*0.4+i*b.h*0.28, cx+b.w*0.2,cy+b.h*0.12+i*b.h*0.28, b.x+b.w-6,cy-b.h*0.12+i*b.h*0.28);
        ctx.stroke();
      }
    }
    ctx.restore();
    // damage cracks
    if(b.hp<b.maxhp){
      ctx.strokeStyle='rgba(40,15,10,.75)'; ctx.lineWidth=2;
      ctx.beginPath();
      ctx.moveTo(b.x+b.w*0.3,b.y+3); ctx.lineTo(b.x+b.w*0.42,b.y+b.h*0.45); ctx.lineTo(b.x+b.w*0.3,b.y+b.h-3);
      ctx.moveTo(b.x+b.w*0.42,b.y+b.h*0.45); ctx.lineTo(b.x+b.w*0.66,b.y+b.h*0.55);
      ctx.stroke();
    }
    if(b.flash>0){
      ctx.globalAlpha=b.flash*0.8; ctx.fillStyle='#fff';
      rr(b.x,b.y,b.w,b.h,7); ctx.fill(); ctx.globalAlpha=1;
    }
    ctx.restore();
  }

  /* ---------------- drawing: powerups ---------------- */
  function drawPowerup(p){
    const t=POWER_TYPES[p.type];
    ctx.save();
    ctx.translate(p.x,p.y);
    ctx.fillStyle='rgba(0,0,0,.35)';
    ctx.beginPath(); ctx.ellipse(0,14,13,4,0,0,TAU); ctx.fill();
    const g=ctx.createLinearGradient(0,-14,0,14);
    g.addColorStop(0,t.c); g.addColorStop(1,t.d);
    ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,14,0,TAU); ctx.fill();
    ctx.strokeStyle='rgba(255,255,255,.65)'; ctx.lineWidth=2;
    ctx.beginPath(); ctx.arc(0,0,14,0,TAU); ctx.stroke();
    ctx.strokeStyle='rgba(20,10,20,.85)'; ctx.fillStyle='rgba(20,10,20,.85)';
    ctx.lineWidth=2.4; ctx.lineCap='round';
    if(p.type==='wide'){
      ctx.beginPath(); ctx.moveTo(-3,-6); ctx.lineTo(-10,0); ctx.lineTo(-3,6); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(3,-6); ctx.lineTo(10,0); ctx.lineTo(3,6); ctx.stroke();
    } else if(p.type==='multi'){
      ctx.beginPath(); ctx.arc(-7,2,3.6,0,TAU); ctx.fill();
      ctx.beginPath(); ctx.arc(1,-4,3.6,0,TAU); ctx.fill();
      ctx.beginPath(); ctx.arc(8,3,3.6,0,TAU); ctx.fill();
    } else {
      ctx.beginPath(); ctx.moveTo(0,-8); ctx.lineTo(0,8); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(-7,-4); ctx.lineTo(7,4); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(-7,4); ctx.lineTo(7,-4); ctx.stroke();
    }
    ctx.restore();
  }

  /* ---------------- background ---------------- */
  function drawBackground(t){
    const g=ctx.createLinearGradient(0,0,0,LH);
    g.addColorStop(0,'#1d0f2e'); g.addColorStop(0.55,'#150a20'); g.addColorStop(1,'#2c1016');
    ctx.fillStyle=g; ctx.fillRect(0,0,LW,LH);
    // checkerboard floor
    ctx.save(); ctx.globalAlpha=0.055; ctx.fillStyle='#ffd9a8';
    const s=62;
    for(let y=0;y<LH;y+=s) for(let x=0;x<LW;x+=s){ if((((x/s)|0)+((y/s)|0))%2===0) ctx.fillRect(x,y,s,s); }
    ctx.restore();
    // grill glow
    const rg=ctx.createRadialGradient(LW/2,LH,20,LW/2,LH,LH*0.62);
    rg.addColorStop(0,'rgba(255,120,55,.24)'); rg.addColorStop(1,'rgba(255,120,55,0)');
    ctx.fillStyle=rg; ctx.fillRect(0,0,LW,LH);
    // decorative floating hearts
    for(const h of bgHearts){
      ctx.globalAlpha=0.13;
      drawHeart(h.x,h.y,h.size,h.rot,'#ff6f9c',0.13);
    }
    ctx.globalAlpha=1;
  }

  /* ---------------- HUD ---------------- */
  function drawHUD(){
    ctx.save();
    ctx.textBaseline='alphabetic';
    // score
    ctx.font='700 12px system-ui, sans-serif'; ctx.fillStyle='rgba(255,190,215,.75)';
    ctx.fillText('SCORE', 26, 30);
    ctx.font='900 30px system-ui, sans-serif';
    ctx.fillStyle='#fff';
    ctx.shadowColor='rgba(255,80,130,.6)'; ctx.shadowBlur=14;
    ctx.fillText(String(S.score).replace(/\B(?=(\d{3})+(?!\d))/g,','), 26, 60);
    ctx.shadowBlur=0;
    // level (center)
    ctx.textAlign='center';
    ctx.font='800 13px system-ui, sans-serif'; ctx.fillStyle='rgba(255,190,215,.75)';
    ctx.fillText('LEVEL', LW/2, 28);
    ctx.font='900 26px system-ui, sans-serif'; ctx.fillStyle='#ffd36e';
    ctx.fillText(String(S.level), LW/2, 56);
    // demo badge
    if(S.mode==='demo'){
      ctx.font='800 11px system-ui, sans-serif';
      const label='AI DEMO';
      const w=ctx.measureText(label).width+18;
      ctx.fillStyle='rgba(255,110,150,.22)'; rr(LW/2-w/2,62,w,19,9); ctx.fill();
      ctx.strokeStyle='rgba(255,140,175,.6)'; ctx.lineWidth=1.2; ctx.stroke();
      ctx.fillStyle='#ffc0d4'; ctx.fillText(label,LW/2,76);
    }
    // combo
    if(S.combo>1){
      ctx.font='900 20px system-ui, sans-serif'; ctx.fillStyle='#ffd36e';
      ctx.fillText('COMBO x'+S.combo, LW/2, 96);
    }
    // lives (hearts)
    ctx.textAlign='right';
    ctx.font='700 12px system-ui, sans-serif'; ctx.fillStyle='rgba(255,190,215,.75)';
    ctx.fillText('LIVES', LW-26, 30);
    for(let i=0;i<S.lives;i++){
      drawHeart(LW-38-i*26, 50, 11, 0, i===0?'#ff3b6b':'#ff6f9c', 1);
    }
    ctx.restore();
  }

  /* ---------------- banner ---------------- */
  function drawBanner(){
    if(S.bannerT<=0) return;
    const a=Math.min(1,S.bannerT*1.2);
    ctx.save();
    ctx.globalAlpha=a; ctx.textAlign='center';
    ctx.font='900 44px system-ui, sans-serif';
    ctx.shadowColor='rgba(255,70,120,.8)'; ctx.shadowBlur=24;
    ctx.fillStyle='#fff';
    ctx.fillText(S.banner, LW/2, LH*0.52);
    ctx.restore();
  }

  /* ---------------- brick hit ---------------- */
  function hitBrick(br,b){
    const ing=INGREDIENTS[br.type];
    br.flash=1;
    if(br.hp>1){
      br.hp--;
      burstCrumbs(b.x+b.w/2,b.y+b.h/2,10,ing.c);
      burstHearts(b.x+b.w/2,b.y+b.h/2,7,ing.c,0.7);
      return;
    }
    br.dead=true;
    S.combo = S.comboT>0 ? S.combo+1 : 1;
    S.comboT=1.6;
    const mult=1+S.combo*0.25;
    const pts=Math.round((30+br.type*8)*mult);
    S.score+=pts;
    popText(b.x+b.w/2,b.y+b.h/2,'+'+pts, br.type%2? '#ff89ab':'#ffd36e', 16+Math.min(12,S.combo*2));
    // BIG heart explosion
    burstHearts(b.x+b.w/2,b.y+b.h/2,34,ing.c,1.15);
    burstHearts(b.x+b.w/2,b.y+b.h/2,10,ing.light,1.5);
    burstCrumbs(b.x+b.w/2,b.y+b.h/2,16,ing.d);
    S.shake=Math.min(9,S.shake+4.5);
    // powerup drop
    if(Math.random()<0.11){
      const t=pick(['wide','multi','slow']);
      powerups.push({x:clamp(b.x+b.w/2,24,LW-24), y:b.y+b.h/2, vy:2.1, type:t, life:12});
    }
    // level clear?
    if(bricks.every(b2=>b2.dead)){
      S.level++;
      S.banner='LEVEL '+S.level+'!';
      S.bannerT=1.8;
      buildBricks();
      resetBall();
      for(let i=0;i<3;i++) burstHearts(rand(LW*0.25,LW*0.75), rand(120,260), 18, null, 1.1);
    }
  }

  /* ---------------- collisions ---------------- */
  function collide(b){
    for(const br of bricks){
      if(br.dead) continue;
      if(b.x+b.r<br.x || b.x-b.r>br.x+br.w || b.y+b.r<br.y || b.y-b.r>br.y+br.h) continue;
      const ox=Math.min(b.x+b.r-br.x, br.x+br.w-(b.x-b.r));
      const oy=Math.min(b.y+b.r-br.y, br.y+br.h-(b.y-b.r));
      if(ox<oy){ b.vx=-b.vx; b.x += b.vx>0? -ox : ox; }
      else { b.vy=-b.vy; b.y += b.vy>0? -oy : oy; }
      hitBrick(br,b);
      return;
    }
  }

  /* ---------------- input ---------------- */
  function takeover(){
    if(S.mode==='demo' && !S.over){
      S.mode='player';
      overlay.style.display='none';
      S.banner=''; S.bannerT=0;
    }
  }
  overlay.addEventListener('pointerdown', function(e){ e.preventDefault(); takeover(); });
  stage.addEventListener('pointerdown', function(){ if(S.mode==='demo') takeover(); });

  canvas.addEventListener('pointermove', function(e){
    const r=canvas.getBoundingClientRect();
    mouseX=(e.clientX-r.left)/r.width*LW;
    if(S.mode==='demo') takeover();
  });
  canvas.addEventListener('pointerleave', function(){ mouseX=null; });

  window.addEventListener('keydown', function(e){
    const k=e.key.toLowerCase();
    if(['arrowleft','arrowright','arrowup','arrowdown',' ','spacebar'].indexOf(k)>=0) e.preventDefault();
    keys[k]=true;
    if(k===' '||k==='spacebar'){ if(S.mode==='demo') takeover(); if(balls.length===0) S.serveT=0; }
    if(k==='p'){ if(!S.over) S.paused=!S.paused; }
    if(k==='r'){ restart(); }
    if(S.mode==='demo') takeover();
  });
  window.addEventListener('keyup', function(e){ keys[e.key.toLowerCase()]=false; });

  function restart(){
    S.mode='player'; S.score=0; S.lives=3; S.level=1; S.combo=0; S.comboT=0;
    S.paused=false; S.over=false; S.wideT=0; S.slowT=0; S.serveT=0.3;
    paddle.w=paddle.baseW; paddle.x=LW/2-paddle.w/2;
    powerups.length=0; parts.length=0; pops.length=0;
    buildBricks(); resetBall();
    overlay.style.display='none';
  }

  /* ---------------- AI ---------------- */
  function predictLanding(b){
    let x=b.x,y=b.y,vx=b.vx,vy=b.vy,guard=0;
    if(vy<=0) return b.x;
    while(y<paddle.y && guard++<3000){
      x+=vx; y+=vy;
      if(x<b.r){x=b.r;vx=Math.abs(vx);}
      else if(x>LW-b.r){x=LW-b.r;vx=-Math.abs(vx);}
    }
    return x;
  }
  function aiControl(dt){
    let target=null, bestY=-1;
    for(const b of balls){
      if(b.vy>0 && b.y>bestY){ bestY=b.y; target=predictLanding(b); }
    }
    // chase nearby powerups
    for(const p of powerups){
      if(p.y>paddle.y-140 && (target===null || p.y>bestY)){ target=p.x; bestY=p.y; }
    }
    if(target===null) target=paddle.x+paddle.w/2;
    target += rand(-24,24);
    const desired=clamp(target-paddle.w/2,0,LW-paddle.w);
    const diff=desired-paddle.x;
    const spd=9.4*60*dt;
    paddle.x += clamp(diff,-spd,spd);
  }
  function playerControl(dt){
    let dx=0;
    if(keys['arrowleft']||keys['a']) dx-=1;
    if(keys['arrowright']||keys['d']) dx+=1;
    if(dx!==0){
      paddle.x=clamp(paddle.x+dx*12*60*dt,0,LW-paddle.w);
    } else if(mouseX!==null){
      const want=clamp(mouseX-paddle.w/2,0,LW-paddle.w);
      paddle.x += (want-paddle.x)*Math.min(1,14*dt);
    }
  }

  /* ---------------- update ---------------- */
  function update(dt){
    S.time+=dt;
    if(S.paused||S.over) return;

    if(S.mode==='demo') aiControl(dt); else playerControl(dt);
    paddle.x=clamp(paddle.x,0,LW-paddle.w);
    paddle.y=LH-66;

    // paddle timers
    if(S.wideT>0){ S.wideT-=dt; if(S.wideT<=0) paddle.w=paddle.baseW; }
    if(S.slowT>0){ S.slowT-=dt; }

    // serve
    if(balls.length===0){
      S.serveT-=dt;
      if(S.serveT<=0) launchBall();
    }

    // balls
    for(let i=balls.length-1;i>=0;i--){
      const b=balls[i];
      b.trail.push({x:b.x,y:b.y});
      if(b.trail.length>7) b.trail.shift();
      b.x+=b.vx*60*dt; b.y+=b.vy*60*dt;
      if(b.x<b.r){b.x=b.r;b.vx=Math.abs(b.vx);}
      if(b.x>LW-b.r){b.x=LW-b.r;b.vx=-Math.abs(b.vx);}
      if(b.y<b.r+74){b.y=b.r+74;b.vy=Math.abs(b.vy);}
      // paddle
      if(b.vy>0 && b.y+b.r>=paddle.y && b.y-b.r<=paddle.y+paddle.h &&
         b.x>=paddle.x-8 && b.x<=paddle.x+paddle.w+8){
        const rel=clamp((b.x-(paddle.x+paddle.w/2))/(paddle.w/2),-1,1);
        const ang=rel*1.05;
        const sp=b.speed;
        b.vx=Math.sin(ang)*sp; b.vy=-Math.abs(Math.cos(ang))*sp;
        b.y=paddle.y-b.r;
        S.combo=0; S.comboT=0;
        burstCrumbs(b.x,paddle.y,5,'#e8a24e');
        for(let k=0;k<4;k++) burstHearts(b.x,paddle.y-4,2,'#ff6f9c',0.55);
      }
      collide(b);
      if(b.y-b.r>LH){
        balls.splice(i,1);
        burstHearts(b.x,LH-10,10,null,0.8);
      }
    }
    if(balls.length===0){
      if(S.mode==='player'){
        S.lives--;
        if(S.lives<=0){ gameOver(); return; }
      }
      resetBall();
    }

    // powerups
    for(let i=powerups.length-1;i>=0;i--){
      const p=powerups[i];
      p.y+=p.vy*60*dt; p.life-=dt;
      if(p.y>LH+30||p.life<=0){ powerups.splice(i,1); continue; }
      if(p.y+paddle.h/2>paddle.y && p.y-paddle.h/2<paddle.y+paddle.h &&
         p.x>paddle.x-10 && p.x<paddle.x+paddle.w+10){
        applyPower(p);
        powerups.splice(i,1);
      }
    }

    // particles
    for(let i=parts.length-1;i>=0;i--){
      const p=parts[i];
      p.life-=dt;
      if(p.life<=0){ parts.splice(i,1); continue; }
      p.vy+=p.g*60*dt;
      p.vx*=0.993; p.vy*=0.993;
      p.x+=p.vx*60*dt; p.y+=p.vy*60*dt; p.rot+=p.vrot*60*dt;
      if(p.bounce && p.y>LH-12 && p.vy>0){ p.y=LH-12; p.vy*=-0.42; p.vx*=0.82; }
    }
    // pops
    for(let i=pops.length-1;i>=0;i--){
      const p=pops[i];
      p.life-=dt; p.y-=42*dt;
      if(p.life<=0) pops.splice(i,1);
    }
    // bricks flash decay
    for(const br of bricks) if(br.flash>0) br.flash=Math.max(0,br.flash-dt*3.6);
    // background hearts
    for(const h of bgHearts){
      h.y-=h.vy*60*dt; h.rot+=0.3*dt;
      if(h.y<-30){ h.y=LH+20; h.x=rand(0,LW); }
    }
    if(S.comboT>0){ S.comboT-=dt; if(S.comboT<=0) S.combo=0; }
    if(S.shake>0) S.shake=Math.max(0,S.shake-dt*26);
    if(S.bannerT>0) S.bannerT-=dt;
  }

  function applyPower(p){
    if(p.type==='wide'){
      paddle.w=Math.min(230,paddle.baseW+52); S.wideT=14;
      popText(p.x,p.y-16,'WIDE!','#7ee787',20);
    } else if(p.type==='multi'){
      const src=balls[0]||{x:paddle.x+paddle.w/2,y:paddle.y-16,vx:0,vy:0,speed:baseSpeed()};
      for(let i=0;i<2;i++){
        const sp=baseSpeed();
        const a=(-0.6+i*0.6);
        balls.push({x:src.x,y:src.y-6,r:11,vx:Math.sin(a)*sp,vy:-Math.cos(a)*sp,speed:sp,trail:[]});
      }
      popText(p.x,p.y-16,'MULTI!','#79c0ff',20);
    } else {
      for(const b of balls) setBallSpeed(b,Math.max(3.4,b.speed*0.72));
      S.slowT=12;
      popText(p.x,p.y-16,'SLOW!','#ffd479',20);
    }
    burstHearts(p.x,p.y,20,POWER_TYPES[p.type].c,0.9);
  }

  function gameOver(){
    S.over=true;
    if(S.score>S.best) S.best=S.score;
    card.innerHTML='<div class="logo">🍔</div><h1>GAME OVER</h1>'+
      '<p class="press">Click or press a key to restart</p>'+
      '<p class="hint">Score: <b>'+S.score+'</b> &nbsp;•&nbsp; Level '+S.level+' &nbsp;•&nbsp; Best '+S.best+'<br>Every brick you smashed burst into hearts 💖</p>';
    overlay.style.display='flex';
  }

  /* ---------------- render ---------------- */
  function render(dt){
    ctx.save();
    if(S.shake>0.3){
      ctx.translate(rand(-S.shake,S.shake)*0.6, rand(-S.shake,S.shake)*0.6);
    }
    drawBackground(S.time);

    for(const br of bricks) if(!br.dead) drawBrick(br);
    for(const p of powerups) drawPowerup(p);

    // particles
    for(const p of parts){
      const a=Math.min(1,p.life/0.65);
      if(p.kind==='heart') drawHeart(p.x,p.y,p.size,p.rot,p.color,a);
      else {
        ctx.save(); ctx.globalAlpha=a; ctx.fillStyle=p.color;
        ctx.translate(p.x,p.y); ctx.rotate(p.rot);
        ctx.fillRect(-p.size/2,-p.size/2,p.size,p.size);
        ctx.restore();
      }
    }

    // paddle shadow + burger
    ctx.save();
    ctx.globalAlpha=.30; ctx.fillStyle='#000';
    ctx.beginPath(); ctx.ellipse(paddle.x+paddle.w/2, paddle.y+paddle.h+6, paddle.w*0.45, 8, 0,0,TAU); ctx.fill();
    ctx.restore();
    drawBurger(paddle.x+paddle.w/2, paddle.y+paddle.h/2, paddle.w, paddle.h, S.time);

    // balls (mini burgers)
    for(const b of balls){
      for(let i=0;i<b.trail.length;i++){
        const t=b.trail[i], f=i/b.trail.length;
        ctx.globalAlpha=f*0.22; ctx.fillStyle='#ffd0dd';
        ctx.beginPath(); ctx.arc(t.x,t.y,b.r*f*0.9,0,TAU); ctx.fill();
      }
      ctx.globalAlpha=1;
      ctx.save();
      ctx.shadowColor='rgba(255,150,90,.85)'; ctx.shadowBlur=16;
      ctx.fillStyle='rgba(255,190,110,.28)';
      ctx.beginPath(); ctx.arc(b.x,b.y,b.r+3,0,TAU); ctx.fill();
      ctx.restore();
      drawBurger(b.x,b.y,24,24,b.x*0.01);
    }

    // popups
    ctx.save(); ctx.textAlign='center';
    for(const p of pops){
      ctx.globalAlpha=Math.min(1,p.life/0.5);
      ctx.font='900 '+p.size+'px system-ui, sans-serif';
      ctx.fillStyle=p.color; ctx.strokeStyle='rgba(60,10,30,.8)'; ctx.lineWidth=4;
      ctx.strokeText(p.txt,p.x,p.y); ctx.fillText(p.txt,p.x,p.y);
    }
    ctx.restore();

    ctx.restore();

    drawHUD();
    drawBanner();

    if(S.paused && !S.over){
      ctx.save(); ctx.fillStyle='rgba(8,4,14,.55)'; ctx.fillRect(0,0,LW,LH);
      ctx.textAlign='center'; ctx.fillStyle='#fff'; ctx.font='900 42px system-ui, sans-serif';
      ctx.fillText('PAUSED',LW/2,LH/2);
      ctx.font='600 16px system-ui, sans-serif'; ctx.fillStyle='#ffc6d8';
      ctx.fillText('press P to resume',LW/2,LH/2+34);
      ctx.restore();
    }
  }

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

  /* ---------------- init ---------------- */
  function init(){
    fit();
    paddle.baseW=132; paddle.w=132; paddle.x=LW/2-paddle.w/2; paddle.y=LH-66;
    for(let i=0;i<16;i++){
      bgHearts.push({x:rand(0,LW), y:rand(0,LH), size:rand(7,16), rot:rand(0,TAU), vy:rand(0.15,0.5)});
    }
    buildBricks();
    resetBall();
    S.serveT=0.15;
    // pre-break a few bricks so the demo looks alive from frame one
    setTimeout(function(){
      for(let i=0;i<3;i++){
        const br=bricks[Math.floor(Math.random()*Math.min(bricks.length,18))];
        if(br && !br.dead){
          br.hp=1;
          hitBrick(br,{x:br.x+br.w/2,y:br.y+br.h/2});
        }
      }
      launchBall();
    },120);
    requestAnimationFrame(function(t){ last=t; loop(t); });
  }

  window.addEventListener('resize',function(){
    fit();
    paddle.y=LH-66;
    paddle.x=clamp(paddle.x,0,Math.max(1,LW-paddle.w));
    for(const b of balls){ b.x=clamp(b.x,b.r,LW-b.r); b.y=clamp(b.y,0,LH); }
    for(const p of powerups) p.x=clamp(p.x,20,LW-20);
  });

  // handle the overlay restarting after game over
  overlay.addEventListener('pointerdown',function(){
    if(S.over){ restart(); }
  });
  window.addEventListener('keydown',function(e){
    if(S.over && (e.key===' '||e.key==='Enter'||e.key.toLowerCase()==='r')) restart();
  });

  init();
})();
</script>
</body>
</html>
Reasoning trace (16,760 tokens)
Let me build a breakout game with a hamburger paddle and bricks that explode into hearts.

Key requirements:
- Hamburger paddle (with bun, patty, cheese, lettuce, tomato - CSS or canvas drawn)
- Bricks break into tons of little hearts (particles)
- Breakout game, self-playing demo mode with AI paddle behind "Click or press a key to start"
- 1280x800 viewport, responsive down to 375px
- Canvas-based, inline everything

Let me write a solid canvas breakout game.

Design:
- Canvas with scaling to fit container
- Background: warm diner/checkered pattern maybe
- Paddle: hamburger drawn with canvas shapes (bun top with sesame seeds, lettuce, cheese, patty, bun bottom)
- Ball: a burger? or a frying patty... Let's make the ball a small burger or a fry. Maybe a golden fry/pepperoni. Let's make it a little burger too — fun.
- Bricks: rows with different colors (burger ingredients: lettuce green, tomato red, cheese yellow, patty brown, onion purple). Each brick shows a little icon maybe.
- When brick breaks: explode into tons of hearts (particle system). Hearts drawn as vector shapes (two circles + triangle) with pink/red colors, gravity, spin, fade.
- Power-ups maybe: some bricks drop power-ups (wide paddle, multi-ball, slow). Keep it simple but nice: power-ups as hearts too? Let's include: "wide" (extra patty), "multi" (extra ball), "slow" (slower ball). Actually keep simple: bricks randomly drop a powerup.

Let's implement:
- Canvas sized to window, with a wrapper. Use devicePixelRatio.
- Game state: 'waiting' → overlay "Click or press a key to start" with demo AI playing.
- Demo AI: paddle tracks ball with slight error; occasionally makes mistakes. The demo auto-starts when the user clicks? Requirement: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message, so the game can be judged in action without playing." So the game runs automatically in background (AI plays) while showing the message. Then when user clicks/presses key, the user takes control.

So: on load, start the simulation with AI controlling the paddle, and display an overlay "Click or press a key to start" plus maybe "Watch the AI demo". After interaction, overlay hides and player controls.

Important: "Something meaningful must be visible on first paint" — the overlay plus the game scene visible.

Let's carefully build the game loop with requestAnimationFrame.

Physics:
- Ball speed ~ 4.5 px/frame at 60fps, increases over time.
- Paddle bounce angle based on hit position.
- Bricks in a grid of maybe 9 columns x 6 rows.

Heart particles: on brick break, spawn ~18-30 hearts with random velocities, gravity, rotation, fading, scale. Also a small burst ring.

Also, a combo score popup.

Let me draw the hamburger paddle:
- Width ~ 110px, height ~ 34px (logical units). Draw:
  - Top bun: rounded arc (half-ellipse) in #F2A65A / gradient, with sesame seeds.
  - Lettuce: wavy green band
  - Cheese: yellow band with drips
  - Patty: dark brown rounded rect with texture
  - Bottom bun: rounded bottom in tan.
Actually order top→bottom: top bun, lettuce, tomato, cheese, patty, bottom bun. Keep within height.

Ball: maybe a mini burger (round patty with bun) or a frying-pit. Let's do a small round burger: top bun circle half, etc. Simpler: draw a "meatball" with bun arcs. I'll draw a mini burger with same drawing function scaled.

Bricks: rounded rects with a gloss and a small ingredient icon. Maybe draw bricks as "patty slabs" (brown) with different colored topping strips. Let's have brick colors per row: bun gold, lettuce green, tomato red, cheese yellow, onion purple, bacon pink. Each brick looks like a mini stacked ingredient, drawn with a strip of the ingredient color plus highlight.

Let's write the code.

Structure:

```html
<div id="stage">
  <canvas id="game"></canvas>
  <div id="overlay">
    <h1>HEART BURGER BREAKOUT</h1>
    <p>Click or press a key to start</p>
    <p class="sub">Demo AI is playing…</p>
  </div>
</div>
```

CSS: dark background with warm gradient, canvas centered, max width 1100px, aspect ratio 16:10 maybe. Actually let the canvas fill the window and use logical coordinate system 960x640 scaled. Simpler: canvas fixed internal resolution of e.g. 1000x640 (game units), scaled via CSS to fit the container while maintaining aspect ratio. That handles responsiveness well.

Let's use logical W=1000, H=640. Container aspect-ratio: 1000/640. Canvas CSS width 100%, height 100%, and internal buffer = W*dpr etc. But dpr scaling with CSS scaling gets tricky; use buffer = logical*dpr and scale ctx by dpr, then CSS scales. Fine.

Responsive: on narrow screens, stage width = 100vw - 24px, height auto via aspect-ratio.

Let's write the game logic.

```js
const W=1000,H=640;
const paddle = {x:400,y:580,w:120,h:36,vy:0};
```

Bricks: cols=10, rows=6? With W=1000 and margins, brick width ~ 90. Let's do cols=9, rows=6. Brick area from x=60 to W-60, top y=70.

brick w = (W-120)/9 - gap. Let's compute: available 880, 9 cols → 97.7 each, gap 6 → w=91.7. Height 28, gap 8. Rows 6 → 6*36=216, from y=80 to 296. Good.

Ball radius 9.

Particle system: hearts array. Each heart: x,y,vx,vy,rot,vrot,size,life,maxLife,color.

Heart drawing function: parametric heart path or two circles + triangle. Use bezier path for classic heart.

```js
function heartPath(ctx, s){
  ctx.beginPath();
  ctx.moveTo(0, s*0.35);
  ctx.bezierCurveTo(0, -s*0.05, -s*0.5, -s*0.3, -s*0.5, s*0.1);
  ...
}
```
Let me define a heart centered at origin with size s (half-width). Standard:
moveTo(0, 0.3s)
bezierCurveTo(0, -0.2s, -0.6s, -0.45s, -0.6s, -0.05s)  -> hmm, need symmetric shape.

Classic heart path (from many snippets), centered at origin, scale factor:
```
ctx.moveTo(0, 0.3);
ctx.bezierCurveTo(0, 0, -0.5, -0.15, -0.5, 0.2);
ctx.bezierCurveTo(-0.5, 0.6, 0, 0.9, 0, 1.1);
ctx.bezierCurveTo(0, 0.9, 0.5, 0.6, 0.5, 0.2);
ctx.bezierCurveTo(0.5, -0.15, 0, 0, 0, 0.3);
```
That's a heart with tip at bottom (y=1.1) and lobes at top (y≈-0.05). Scale by s/1.1 maybe. Fine, I'll tune.

Power-ups: drop from destroyed bricks occasionally. Types: 'wide' (paddle wider), 'multi' (extra ball), 'slow'. Draw as small icon: wide = burger with plus? Let's draw them as hearts with a symbol, or simple icons:
- wide: a horizontal double-chevron "«" 
- multi: three small balls
- slow: a clock / snowflake
Simplify: power-up as a small rounded badge with letter? Better to draw little food icons: "W" wide... Let me draw:
- Wide: a burger with side arrows (🍔 with arrows) — draw burger icon plus outward arrows.
- Multi: three mini balls.
- Slow: a fry with a clock? Use a snail? Let's use a "cool" blue circle with a snowflake-ish asterisk.

Keep it simple: draw badge circle with color and an inner icon drawn with lines.

Score and lives display at top of canvas (drawn in canvas) — score, level, hearts count (lives shown as hearts!). Nice: lives displayed as hearts. HUD drawn in canvas at top.

Actually the bricks start at y=80, so HUD at y<70 area. Good.

Level progression: when all bricks cleared, next level with more rows/speed, respawn bricks. Keep it so demo keeps going.

Ball lost → lose a life, reset ball onto paddle.

AI demo: paddle.x follows predicted ball landing. Simple: track ball with x-position of ball when falling; predict where ball will land at paddle y by simulating. Simple approach: track target x = ball.x when ball.vy>0 else paddle.x. Then move paddle towards target with max speed and slight lag. Add slight noise so it sometimes misses (fun). Also AI picks up power-ups.

For the player, mouse control (pointermove) and keyboard (arrow keys / A-D). Space to launch ball? Auto-launch with click. Let's auto-launch ball attached to paddle briefly then release.

Also: the "click or press a key to start" overlay. On click/key → player mode, hide overlay. Also add a small restart button "Restart demo" maybe. Let's include HUD buttons outside canvas? Keep canvas only for game; overlay div handles the start. Add a small footer with instructions that appears after start.

Let's be careful: overlay must not block pointer events after start (set display none).

Also on first paint, the demo must already be running and visible. So start loop immediately, with AI controlling, overlay on top showing "Click or press a key to start" — but the overlay shouldn't fully hide the game. Make overlay a semi-transparent panel at the top-center or a big centered translucent card with the game visible behind. I'll do a centered translucent card with backdrop blur, plus the "Click or press a key to start" text. Game still visible behind, so hearts visible. But blur may hide the action... Use a modest card centered with dark translucent bg (not covering everything) - fine, the bricks occupy the top area and the paddle the bottom, hearts in the middle. A centered card might cover the middle. Let's place the card near the bottom-center? Hmm, paddle at bottom. Let's place the message near the top-center over the HUD area... but HUD has score.

Option: overlay card positioned at vertical center but the game keeps playing and hearts fly everywhere. I'd rather position the card at ~68% height with translate. Actually let's make the message a banner across the top of the stage (a strip) with the title, and a semi-transparent full-stage dim that's light (rgba(0,0,0,0.25)) so the game is clearly visible. Then "click anywhere to take over". The dim ensures text readability. I think that's fine: "Something meaningful must be visible on first paint" — yes, game visible.

Let me do: full overlay with background rgba(10,6,20,0.35) and a centered card containing title "🍔 HEART BURGER BREAKOUT", subtitle "Click or press a key to take control", "Demo AI is playing now". Card centered. The game continues behind; particles hearts will be visible around the card. Card size moderate (max 460px wide). At 640 height logical, card ~200px tall, covers middle. Hearts explode at brick rows (top), so most action visible. Acceptable. Alternatively place card slightly lower (top: 62%). I'll place it at around 55-60% so top brick area is clear.

Hmm, let me instead make the overlay a bottom-anchored banner (like a "press start" bar) that sits below the brick field, over the empty middle area where hearts fly. I'd say a centered card at 50% is fine and most legible. I'll go with a card positioned at top: 42%.

Let's write the code now.

Detailed drawing functions:

drawBurger(ctx, x, y, w, h, opts): draws hamburger.
Layout (top to bottom), total height h:
- top bun: height 0.32h, rounded top ellipse-like. Draw as: path from (x, y+0.34h) up over arc to (x+w, y+0.34h), fill gradient #F5B963 → #E08B3C. Use quadratic/arc.
- sesame seeds: small ellipses on bun.
- lettuce: wavy strip at y+0.34h..0.45h, green #7BC96F with zigzag edges.
- tomato: red strip 0.45h..0.55h with lighter red
- cheese: yellow strip 0.55h..0.68h with drips
- patty: brown 0.68h..0.88h
- bottom bun: 0.88h..1.0h, rounded bottom, tan.

I'll implement with rounded rects & paths.

For the paddle, w=120,h=40. Ball as mini burger radius ~10.

Bricks: draw as rounded rect with ingredient color + a "sauce drip" and highlight. Each brick has an ingredient type; draw with a top highlight line and small detail. Maybe draw a mini icon in center (like a tiny symbol per type). Let's draw each brick as a "sandwich slab": base color row, with darker bottom edge and a highlight top edge, plus small random "sesame/seed" dots for bun rows.

Let me define brick colors:
row0: bun #F0A64A
row1: lettuce #6FCF57
row2: tomato #E94F4F
row3: cheese #FFD23F
row4: onion #A77BE0
row5: bacon #FF8A5B

Background: dark navy with subtle checkerboard tiles (like diner floor) and a warm glow. Add a faint "grill" gradient at bottom.

Let's add a "brick hit" flash and a floating score text.

Also add screen shake on big explosions (small).

Let me now write actual code.

```js
(function(){
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W=1000,H=640;
let dpr = Math.min(window.devicePixelRatio||1, 2);
function resize(){
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.round(rect.width*dpr);
  canvas.height = Math.round(rect.height*dpr);
  // scale factor to map logical W,H onto canvas
  scale = canvas.width / W;  // because aspect preserved by CSS
  ctx.setTransform(scale,0,0,scale,0,0);
}
```
Since CSS keeps aspect-ratio equal to W/H, canvas.width/W == canvas.height/H. Good.

Stage: div with aspect-ratio: 1000/640; width: min(100%, 1000px-ish). Let's do: 
```css
#stage{ width: min(96vw, 1120px); aspect-ratio: 1000/640; position:relative; }
```
On 375px wide: width ~360px, height 230px. That's small but OK. Maybe increase to 100vw-16px. Let's use width:min(98vw,1100px).

Hmm, at 375 the game will be tiny, with small text. Acceptable; maybe use a portrait-friendly layout? Keep simple; the requirement says adapt gracefully, no horizontal scroll.

Actually to improve mobile, I could make the stage aspect-ratio responsive: on narrow screens use 1000x760 (taller) logical? That complicates. Alternative: keep the same aspect but let the canvas fill more: on narrow screens, aspect-ratio: 3/4 and adjust logical H? Let's do a simpler approach: define logical size based on stage aspect ratio, keeping width 1000 fixed and height = 1000 / aspect. Hmm, that changes gameplay geometry.

Better: make the stage aspect-ratio adaptive: 
- Wide screens: 1000/640.
- Narrow (< 640px): 3/4 portrait, and we set logical W=1000, H=1333? Then bricks layout would need recompute. Alternatively keep logical W=1000 and H variable computed from aspect ratio; bricks positioned relative to top and paddle relative to bottom. That's actually flexible: bricks rows computed from available space.

Let me implement: logical W = 1000 always. H = 1000 / stageAspect. For stage aspect 1000/640 → H=640. For 3/4 → H=1333. That's a lot of vertical space; brick rows spread? Keep brick field same size (top area), paddle at H-60. The play area just gets taller - fine, ball travels more. Let's cap H to e.g. 900 max and instead scale differently.

Simplify: For narrow screens, use aspect-ratio 4/5 and logical W=1000, H=1250. Ball speed maybe scaled. Hmm.

Honestly, simplest graceful approach: keep fixed logical 1000x640 and let the canvas be scaled down to fit the width; on narrow screens height shrinks proportionally. 360px wide → 230px tall, tiny but visible and playable-ish. Many judges view at 1280x800 anyway. I'll add a media query to allow the stage to be taller on mobile using aspect-ratio: 5/6 and letterbox the game? That would distort.

Alternative approach for mobile: use a fixed logical height of 640 and let the logical width shrink on portrait? Then brick columns reduce. Let me just implement adaptive: compute logical W and H from stage's CSS size (the stage has a fixed aspect ratio, so mapping is exact). Choose stage aspect ratio via CSS media query:
- default (min-width: 700px): aspect-ratio: 1000/640
- below 700px: aspect-ratio: 62.5/100 (portrait-ish, e.g. 5/8)

And in JS, logical W=1000, H = W / aspect where aspect from actual rect. Then layout adapts: brick columns = 9, rows = 6 always at top; paddle at H-56; ball speed scaled relative to H maybe.

But the brick field (rows * 36 = 216) plus top HUD would occupy a big portion on a tall portrait screen. It's fine.

Ball speed: base speed 5.2 in logical units per frame at 60fps — on a taller screen, travel time longer, but fine. Maybe scale speed with sqrt(H/640).

Let's implement responsive logical size properly:
```js
let LW=1000, LH=640;
function resize(){
  const rect = stage.getBoundingClientRect();
  LW = 1000;
  LH = Math.round(1000 * (rect.height/rect.width));
  ...
}
```
But then the paddle/brick layout uses LH dynamically. Need to re-layout on resize (which happens rarely). On resize, reposition paddle to bottom, keep bricks.

Hmm, but if LH changes, the game state positions (ball etc.) may be out of bounds; clamp.

Alternatively: keep the canvas at fixed logical size 1000x640 and use CSS `object-fit`-like letterboxing on mobile. I think adaptive is nicer. Let's do adaptive but simple.

Actually let me reduce complexity: keep the stage aspect-ratio fixed at 1000/640 for all sizes, and on mobile just scale it down. Add `max-height` constraints so it fits vertically. At 375x800 viewport, stage width 360 → height 230. That's a small game. Judges might check mobile look. Meh, "adapt gracefully" - a scaled-down game is acceptable and common.

Compromise: use aspect-ratio: 1000/640 on desktop; on mobile (max-width:640px) use aspect-ratio: 1000/1100 → but then logical H must change...

OK let's do the adaptive logical H approach. It's not much extra work:

- stage CSS: `width: min(98vw, 1080px); aspect-ratio: var(--ar, 1000/640)`. Use media query to set --ar: 1000/1150 on small screens? But CSS custom property in aspect-ratio works: `aspect-ratio: var(--ar)` where --ar: 1000 / 640 → is a number ratio. Let's just use media queries directly on #stage.

JS: compute LH from rect, but keep it in range [640, 1150]. If the computed differs, we set canvas buffer to rect size and transform scale = rect.width*dpr/1000, and LH = 1000*rect.height/rect.width.

Game layout uses LH for paddle y and walls. Brick rows: keep 6 rows but maybe reduce to 5 on tall screens so bricks don't take half the screen. Actually on tall screen, bricks at top 216px of 1150 is fine.

Let me write the layout function:

```js
function layout(){
  paddle.w = 120; paddle.h = 40;
  paddle.y = LH - 70;
  paddle.x = clamp(paddle.x, paddle.w/2, LW-paddle.w/2);
  ball.x = clamp(...); ball.y = ...
}
```

Brick grid:
```js
const COLS=9, ROWS=6;
const marginX = 46, gapX=8, gapY=10;
brickW = (LW - marginX*2 - gapX*(COLS-1))/COLS;
brickH = 28;
brickTop = 96;
```
On tall screens bricks remain same size — good (consistent difficulty), maybe increase cols on wide? LW fixed at 1000 so same.

Alright.

HUD: draw at top: left "SCORE 1234", center level, right lives as hearts. Since LH varies, HUD at y=30..60, brickTop=96. Good.

Now the AI. Implementation:

```js
function aiUpdate(dt){
  // target: where ball will cross paddle level
  let target = LW/2;
  const b = balls[0]; // use primary ball
  if(balls.length){
    // simple: predict
    target = predictLanding(b);
  }
  // with some noise
  target += (Math.random()-0.5)*40;
  const desired = target - paddle.w/2;
  const diff = desired - paddle.x;
  const maxSpeed = 9;
  paddle.x += Math.sign(diff)*Math.min(Math.abs(diff), maxSpeed);
}
```
predictLanding: simulate ball x motion until y >= paddle.y assuming constant vx and vy (approx, ignoring wall bounces is fine for demo; but bouncing changes direction — for demo, approximate with reflections off walls).

Simple approach: copy ball, step with dt, reflect on side walls, stop when y >= paddle.y. Return x. Do it each frame (cheap for 1-3 balls).

AI sometimes misses: add noise increasing over time and reduce AI speed. Keep it reasonable.

Also demo AI should catch power-ups: since AI tracks the ball only, power-ups might be missed. That's fine; maybe the AI should also track the nearest falling powerup. Let's add: if a powerup is falling and within reach, and ball isn't in danger, target = powerup.x. Simple: target = min of landing prediction and powerup x if powerup is lower. I'll add: consider power-ups whose y > LH-300; if any, target = that powerup's x (weighted). Keep simple.

Player controls:
- Pointer move over canvas → paddle.x = pointer logical x - w/2.
- Touch: same via pointermove (works with touch as pointer events). 
- Keyboard: ArrowLeft/Right or A/D held → move paddle.
- Space/click → launch ball / start.

Game states: 'demo' (AI playing, overlay shown), 'player' (user playing).

The overlay text "Click or press a key to start" → when user clicks the overlay or presses any key, switch to player mode, hide overlay, reset ball onto paddle for a short "serve" (or immediately launch). To keep action continuous, on takeover: set state 'player', attach ball to paddle briefly and launch on next click/space? Better to launch immediately with a slight upward angle so play continues seamlessly.

I'll do: on takeover, keep current balls flying (AI→player), just switch control. That's seamless and immediately interactive.

Also add a "Restart" button? Add small DOM button "↺ Restart" top-right of stage maybe. Keep it minimal: after starting, show a hint bar under the stage: "Move with mouse / ← → • Space to serve • P pause". Let's include a DOM footer with instructions that appears once started.

Pause on 'p' or Escape. On pause show overlay "Paused".

Score popups, combo.

Power-ups:
- 'wide': paddle.w += 40 (max 220), duration permanent? Let's make timed 12s. Show timer bar on paddle.
- 'multi': add a new ball.
- 'slow': ball speed * 0.7 for 10s.

Powerup falls at speed 2.2, drawn as small circle badge with icon. When caught, effects.

Also "brick hp": some bricks have 2 HP (tougher), shown with crack overlay. On first hit, crack; on second, explode into hearts. Let's give rows 4-5 double HP? That adds fun. Actually the explosion into hearts should happen on every break. With 2HP bricks, first hit = chips (small particles, few hearts), second = big explosion with tons of hearts. Good.

Tons of little hearts: spawn 26 hearts per brick with color from brick color mixed with pink? Hearts should be pink/red mostly, with some colored ones matching the brick. Let's do 70% pink/red hearts, 30% in the brick's color. Size 4-9.

Also add heart "pop" sound? No audio needed; could use WebAudio for a "pop" but sandbox may block? WebAudio is fine but might need user gesture. Let's skip audio or add optional tiny synth on user interaction. I'll skip audio to be safe. Actually a subtle WebAudio blip on brick break is nice but risk. Skip.

Let's write the code carefully now.

```js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const stage = document.getElementById('stage');

let LW = 1000, LH = 640, scale = 1;

function fit(){
  const r = stage.getBoundingClientRect();
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  LW = 1000;
  LH = Math.max(560, Math.min(1200, Math.round(1000 * (r.height / r.width))));
  canvas.width = Math.round(r.width * dpr);
  canvas.height = Math.round(r.height * dpr);
  scale = canvas.width / LW;
  ctx.setTransform(scale, 0, 0, scale, 0, 0);
}
```
Wait: if LH is clamped, the aspect mapping breaks (distortion). Instead, compute scale from width and set LH = canvas.height/scale → exact. But then LH depends on CSS aspect, which we control via media query; so LH will be whatever. Let's just do: scale = canvas.width/LW; LH = canvas.height/scale. Exact mapping, no distortion. Then LH could be 1150 on mobile portrait. Fine.

But I want LH not too extreme. CSS: mobile aspect 1000/1250 → LH=1250. Play area tall; brick field 216 tall at top; ball travels far. Let's instead use mobile aspect 1000/950 → LH=950. Good compromise.

Then on resize, reposition: paddle.y = LH-64; clamp ball positions.

Now, brick layout with LH possibly 950: bricks still at top. Fine.

Let me write the render order:
1. background gradient + checker pattern + subtle vignette
2. bricks
3. powerups
4. particles (hearts)
5. paddle (burger)
6. balls (mini burgers)
7. score popups
8. HUD
9. flash effects

Now code the heart drawing:

```js
function drawHeart(x,y,size,rot,color,alpha){
  ctx.save();
  ctx.translate(x,y);
  ctx.rotate(rot);
  ctx.globalAlpha = alpha;
  ctx.fillStyle = color;
  ctx.beginPath();
  const s = size;
  ctx.moveTo(0, s*0.95);
  ctx.bezierCurveTo(-s*1.25, s*0.05, -s*0.62, -s*0.95, 0, -s*0.30);
  ctx.bezierCurveTo(s*0.62, -s*0.95, s*1.25, s*0.05, 0, s*0.95);
  ctx.closePath();
  ctx.fill();
  // highlight
  ctx.globalAlpha = alpha*0.5;
  ctx.fillStyle = 'rgba(255,255,255,0.8)';
  ctx.beginPath();
  ctx.ellipse(-s*0.34, -s*0.34, s*0.16, s*0.24, -0.4, 0, Math.PI*2);
  ctx.fill();
  ctx.restore();
}
```
Check the heart shape: start at bottom tip (0, 0.95s). Left lobe via control points to (0,-0.3s) top center dip, then right side back. Hmm the classic: two lobes at top. Let's verify points: from (0,0.95s) curve to (0,-0.30s) with controls (-1.25s, 0.05s) and (-0.62s,-0.95s). This creates left lobe bulging to x≈-0.6s at y≈-0.3. Then from (0,-0.30s) to (0,0.95s) with controls (0.62s,-0.95s) and (1.25s,0.05s) creating right lobe. Yes, classic heart. Good.

Now, the burger drawing. Let me write:

```js
function drawBurger(x, y, w, h, seed){
  // x,y = center-left-top? Let's use center (cx, cy) with width w, height h.
}
```
I'll implement with ctx.translate to (cx, cy - h/2) then draw in local coords from 0..w, 0..h.

Components:
- Bun top: path: moveTo(0, bunH) then quadratic to top: use ctx.ellipse? Let's do:
```
ctx.beginPath();
ctx.moveTo(0, bunH);
ctx.quadraticCurveTo(0, 0, w/2, 0);
ctx.quadraticCurveTo(w, 0, w, bunH);
ctx.closePath();
```
with bunH = h*0.34. Fill gradient from #F8C273 (top) to #E0933F.
- sesame seeds: 5 small ellipses along the top arc, rotated.
- lettuce: zigzag strip: from (0, y1) to (w, y1) with wavy bottom: draw a path with alternating up/down arcs.
- tomato: red rounded strip.
- cheese: yellow strip with triangular drips at bottom.
- patty: brown rounded rect with slightly irregular edges (use roundRect).
- bun bottom: tan rounded shape (bottom curve).

Total heights: bun 0.34, lettuce 0.12, tomato 0.10, cheese 0.12, patty 0.18, bottom bun 0.14 → 1.0. Good.

For the ball: a mini burger of width 22, height 22 drawn similarly (simplified: bun top circle half, patty, bun bottom). I'll just call drawBurger with w=24,h=24 for the ball — cute! Ball is a tiny burger.

But balls moving might look odd; it's cute and thematic. Also add a subtle glow/trail behind the ball.

Paddle: drawBurger(paddle.x + w/2, paddle.y + h/2, paddle.w, paddle.h).

Bricks: each brick drawn as a slab:
```
function drawBrick(b){
  // rounded rect with gradient of ingredient color, darker bottom, highlight top, and little detail dots
}
```
Maybe make bricks look like "patty slabs with sauce". I'll draw: rounded rect base with vertical gradient (lighter top), a 4px darker "edge" at bottom, and a glossy highlight ellipse at top-left, plus small seeds/toppings depending on type.

Let me add per-type decoration:
- type 'bun': sesame seeds
- 'lettuce': wavy top
- 'tomato': circle slices
- 'cheese': drip
- 'onion': concentric arcs
- 'bacon': wavy stripes
Simplify with generic: highlight + a small icon glyph per type drawn as simple shapes.

I'll implement small drawing per type quickly:

```js
function brickDecor(type, x, y, w, h){
  ctx.save();
  switch(type){
    case 0: // sesame bun
      for(let i=0;i<5;i++){ ... small ellipses }
      break;
    case 1: // lettuce waves
      ...
  }
}
```

Fine, moderate effort.

Cracks for damaged bricks: draw a couple of dark lines.

Now the AI: since in demo mode the ball may be lost, we handle: ball lost → lose life, respawn after short delay with ball attached to paddle (AI then serves automatically after 1s). In demo mode, lives shouldn't run out → in demo mode, just reset without losing a life (or infinite lives). Let's keep lives but in demo mode, don't decrement (or reset to 3). I'll decrement only in player mode; in demo mode just reset the ball.

Game over in player mode → show "Game Over — Click to restart" overlay; and auto-switch to demo? Let's show overlay with score and "Click or press a key to restart" and on click restart in player mode.

Levels: when cleared → level++, rebuild bricks (with more rows and faster ball), show "LEVEL 2" banner.

Let me manage time: use timestamps for dt, clamp dt to 1/30.

Let me now write the whole file.

Additional flair: background has floating subtle hearts drifting? Maybe a few decorative hearts floating upward in the background at low alpha. Nice touch — cheap.

Let's also add a "combo" counter when breaking multiple bricks quickly.

Write code:

```js
// ---------- state
const state = {
  mode: 'demo', // 'demo' | 'player'
  score: 0, lives: 3, level: 1, combo: 0, comboTimer: 0,
  paused: false, over: false,
  shake: 0, flash: 0,
  banner: null, bannerT: 0,
  serveTimer: 0
};
```

Balls array; each ball {x,y,vx,vy,r,trail:[]}.

Powerups array: {x,y,type,vy}.

Particles: hearts array; also generic "crumb" particles for non-broken hits.

Let me write the update:

```js
function update(dt){
  // paddle control
  if(state.mode==='demo'){ aiControl(dt); } else { playerControl(dt); }
  // balls
  for(let i=balls.length-1;i>=0;i--){
    const b=balls[i];
    b.vy += 0.0; // no gravity
    b.x += b.vx*dt*60; b.y += b.vy*dt*60;
    // walls
    if(b.x < b.r){ b.x=b.r; b.vx=Math.abs(b.vx); wallFx(); }
    if(b.x > LW-b.r){ b.x=LW-b.r; b.vx=-Math.abs(b.vx); wallFx(); }
    if(b.y < b.r + 40){ b.y = b.r+40; b.vy=Math.abs(b.vy); }
    // paddle collision
    if(b.vy>0 && b.y+b.r >= paddle.y && b.y-b.r <= paddle.y+paddle.h && b.x>=paddle.x-6 && b.x<=paddle.x+paddle.w+6){
      const rel = (b.x - (paddle.x+paddle.w/2))/(paddle.w/2);
      const angle = rel * 1.05; // max ~60deg
      const sp = Math.hypot(b.vx,b.vy);
      b.vx = Math.sin(angle)*sp; b.vy = -Math.abs(Math.cos(angle))*sp;
      b.y = paddle.y - b.r;
      state.combo=0;
      fx: crumbs
    }
    // bricks
    collideBricks(b);
    // bottom
    if(b.y - b.r > LH){ balls.splice(i,1); }
  }
  if(balls.length===0){ serveBall(); }
  ...
}
```

Speed scaling with dt: I'll define velocities in units/frame at 60fps and multiply by (dt*60).

Ball speed: base = 6.2 + level*0.4 (units/frame). At 60fps that's 372 px/s across a 1000 wide screen — fast but ok. Let's use 5.6 base.

Max angle: 1.05 rad (60°). Ensure vy never near zero: clamp |angle| ≤ 1.15.

Brick collision: standard AABB with reflect based on overlap.

```js
function collideBricks(b){
  for(const br of bricks){
    if(br.dead) continue;
    if(b.x+b.r < br.x || b.x-b.r > br.x+br.w || b.y+b.r < br.y || b.y-b.r > br.y+br.h) continue;
    // determine axis
    const overlapX = Math.min(b.x+b.r-br.x, br.x+br.w-(b.x-b.r));
    const overlapY = Math.min(b.y+b.r-br.y, br.y+br.h-(b.y-b.r));
    if(overlapX < overlapY){ b.vx = -b.vx; b.x += b.vx>0? overlapX : -overlapX; }
    else { b.vy = -b.vy; b.y += b.vy>0? overlapY : -overlapY; }
    hitBrick(br);
    break;
  }
}
```

Careful with the reflection displacement sign: after flipping vx, move ball out of the brick: if b.vx>0 (moving right) then the ball's left side is inside → push left by overlapX → b.x -= overlapX. Let me redo: 
- If overlapX<overlapY → horizontal collision: flip vx. To resolve, if b.vx > 0 (now moving right), the ball entered from the left, so push it left: b.x -= overlapX. If b.vx<0, push right: b.x += overlapX. Yes as written above (b.vx>0 ? -overlapX : +overlapX). Wait my code above says `b.x += b.vx>0? overlapX : -overlapX;` — that's the opposite. Fix: `b.x += b.vx>0 ? -overlapX : overlapX;`.

Similarly for Y: if b.vy>0 (moving down now), it entered from top → push up: b.y -= overlapY. So `b.y += b.vy>0 ? -overlapY : overlapY`.

hitBrick(br): br.hp--; spawn small crumbs + a few hearts (say 8) on damage; if hp<=0 → destroy: big explosion with 26-34 hearts, score += points*combo, maybe drop powerup, screen shake, flash, check level clear.

Level clear: if all bricks dead → next level after a short banner.

Let me write the "serve" logic:
```js
function serveBall(){
  state.serveTimer = 0.6;
  balls.length=0;
  const b = makeBall(paddle.x+paddle.w/2, paddle.y-20);
  b.serve = true; // attached to paddle
  balls.push(b);
}
```
While serving (state.serveTimer>0), ball follows paddle; when timer hits 0 (and mode player requires click? For demo auto), launch with random angle.

In player mode, require the player to click/space to launch (classic) — but that could stall if the judge plays. Let's auto-launch after 1.2s in player mode too, and also launch immediately on click/space. Good.

Ball speed on launch: baseSpeed.

Power-ups:
```js
const POWERS = {
  wide: {color:'#7ee787', ...},
  multi: {color:'#79c0ff'},
  slow:  {color:'#ffd479'}
};
```
Draw as a rounded badge with icon: for wide draw two chevrons pointing outward; multi draw 3 dots; slow draw a snowflake/clock.

Power-up catch → effect, particles.

Timers: paddle.wideT, ball.slowT.

Now hearts from bricks: also should be "tons of little hearts". Let's spawn 30 hearts per destroyed brick, sizes 3-8, with initial velocities spread in a circle, gravity 0.08, drag, rotation, fade after ~1.6-2.6s, and a slight upward initial burst.

Also make hearts bounce off the floor once? Cute: when heart hits bottom (LH-6), bounce with damping. Yes, that looks nice and fills the lower area.

Particles update:
```js
for(const p of parts){
  p.vy += 0.09*dt*60;
  p.vx *= 0.995;
  p.vy *= 0.995;
  p.x += p.vx*dt*60; p.y += p.vy*dt*60;
  p.rot += p.vrot*dt*60;
  p.life -= dt;
  if(p.y > LH-8 && p.vy>0){ p.y = LH-8; p.vy *= -0.45; p.vx *= 0.8; }
}
```
Life ~ 2.2s. Fade last 0.6s.

Also "crumb" particles (small circles in brick color) for damaged hits.

Score popups: floating text "+120" with color.

HUD: 
- Left: "SCORE" label small + value big.
- Center: "LEVEL n" 
- Right: hearts representing lives (draw small hearts) + "×3"? Draw 3 mini hearts and the number.
Also show combo meter when combo>1: "COMBO x3" in the center.

Also show a small "DEMO" badge in demo mode.

Background: 
- vertical gradient dark #140b1e → #0d0714
- subtle grid of tiles (diner floor) with low alpha
- a warm radial glow at the bottom center (grill)
- decorative floating hearts rising slowly at low alpha

Let's write CSS for page:
- body: display flex, center, background radial gradient, font-family system rounded, color #fff.
- #stage: position relative; border-radius 18px; overflow hidden; box-shadow big; border 2px rgba(255,255,255,.08).
- overlay: absolute inset 0; display flex; center; background rgba(8,4,14,.45); backdrop-filter blur(2px)? Blur might be heavy; use slight.
- card: background rgba(20,10,30,.82); border 1px solid rgba(255,120,150,.35); border-radius 18px; padding 22px 30px; text-align center; box-shadow.
- h1 with emoji, gradient text.
- .press: animated pulse.

Footer under stage: small hint text.

Media query for narrow: stage width 98vw, aspect-ratio 1000/950, font smaller.

Let me double check the aspect ratio effect on LH: with 1000/950 on mobile, LH = 950. HUD at top, bricks at 96..~300, paddle at LH-70=880. OK.

Now write the actual code. Let me be careful with correctness.

Also handle: clicking the overlay starts player mode; pressing any key (space, arrows, etc.) also starts. Add listeners on window for keydown.

In player mode: keys left/right move paddle; pointer move moves paddle; space launches ball; 'p' pause.

Prevent default for arrow keys to avoid page scroll.

Also the overlay for game over: reuse #overlay with different content.

Let's write the final HTML.

One more consideration: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". Our overlay shows that message and the AI plays behind. 

Let me write the code.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Heart Burger Breakout</title>
<style>...</style>
</head>
<body>
<div id="wrap">
  <div id="stage">
    <canvas id="game"></canvas>
    <div id="overlay">
      <div class="card">
        <div class="logo">🍔</div>
        <h1>HEART BURGER BREAKOUT</h1>
        <p class="press">Click or press a key to start</p>
        <p class="hint">Demo AI is playing right now — take over the burger paddle!</p>
      </div>
    </div>
  </div>
  <div id="foot">
    <span>🖱️ Move mouse or ← →</span><span>Space serve</span><span>P pause</span><span>R restart</span>
  </div>
</div>
<script>...</script>
</body>
</html>
```

Now, the demo should visibly break bricks within the first ~3 seconds so the screenshot shows hearts. To ensure that, start the demo with a ball already in flight and bricks pre-damaged? Better: make the AI serve immediately at load, and place some bricks already cracked (hp=1) so the first hit explodes them into hearts. Even better: on load, spawn a burst of hearts immediately (like a welcome explosion) so hearts are visible in the first screenshot. Let's: at init, run a "pre-break" of a few bricks? That'd look odd if bricks are already gone. Alternative: give a "demo attract" burst: spawn ~60 hearts at the top area as a celebratory effect at start (like the burger releases hearts). That guarantees visible hearts at first paint. Let's do that — a heart fountain at the top center at load, plus AI serving immediately.

Actually simpler: at load, destroy 3 random bricks in the top area with explosions (so there are gaps in the brick wall) and spawn hearts. That looks intentional (like the demo already started). Combined with continuous AI play, hearts will be around. I'll do both: initial 2 explosions + continuous play.

Also the screenshot is at ~3s; the AI should have broken a few bricks by then. Good.

Now let me write the code in full.

For the AI predict landing:

```js
function predictX(b){
  let x=b.x, y=b.y, vx=b.vx, vy=b.vy;
  if(vy<=0) return b.x;
  const target = paddle.y;
  let guard=0;
  while(y<target && guard++<2000){
    x+=vx; y+=vy;
    if(x<b.r){ x=b.r; vx=Math.abs(vx);}
    else if(x>LW-b.r){ x=LW-b.r; vx=-Math.abs(vx);}
  }
  return x;
}
```
Note: bricks could block, ignore.

AI target selection: consider all balls; choose the lowest one (closest to paddle) → target = predictX of that ball. Also powerups.

AI movement speed: 10 units/frame → can catch most balls. Add difficulty: reduce to ~7 and add noise.

Also AI should aim to hit bricks? Not needed.

Player paddle: mouse control smooth (lerp to target), keyboard overrides.

Let's write code for input:
```js
let mouseX=null, keys={};
canvas.addEventListener('pointermove', e=>{ const r=canvas.getBoundingClientRect(); mouseX = (e.clientX-r.left)/r.width*LW; });
```
Use stage/canvas rect. pointermove on the stage.

Keyboard: window keydown/keyup.

Takeover: any click on stage or keydown → if mode==='demo' → mode='player', hide overlay, show footer hint.

But the overlay covers the stage, so pointermove on canvas won't fire until overlay hidden. The overlay has pointer-events auto to catch the click. Add click handler on overlay → takeover.

Let me now write everything. I'll be thorough but keep the code manageable.

Drawing the brick field: also draw a "grill rack" behind the bricks? Maybe a subtle dark panel behind the brick area.

Let me write the final code.

```js
/* ---------- Setup ---------- */
const stage=document.getElementById('stage');
const canvas=document.getElementById('game');
const ctx=canvas.getContext('2d');
const overlay=document.getElementById('overlay');
const card=document.getElementById('card');

let LW=1000, LH=640, scale=1;

function fit(){
  const r=stage.getBoundingClientRect();
  const dpr=Math.min(window.devicePixelRatio||1,2);
  canvas.width=Math.max(1,Math.round(r.width*dpr));
  canvas.height=Math.max(1,Math.round(r.height*dpr));
  scale=canvas.width/LW;
  LH=canvas.height/scale;
  ctx.setTransform(scale,0,0,scale,0,0);
  ctx.imageSmoothingEnabled=true;
}
window.addEventListener('resize',()=>{fit(); layoutFix();});
```

layoutFix: reposition paddle to bottom, clamp.

Let me write bricks init:

```js
const COLS=9, ROWS=6;
const BRICK_TYPES=[{c:'#F2A65A',d:'#C9762A'},...];
let bricks=[];
function buildBricks(level){
  bricks=[];
  const marginX=54, gapX=8, top=100, gapY=10, h=28;
  const w=(LW-marginX*2-gapX*(COLS-1))/COLS;
  for(let r=0;r<ROWS;r++){
    for(let c=0;c<COLS;c++){
      const t=(r+level)%BRICK_TYPES.length;
      bricks.push({x:marginX+c*(w+gapX), y:top+r*(h+gapY), w, h, type:t, hp:(r>=ROWS-2?2:1), dead:false, flash:0});
    }
  }
}
```

Colors per type:
0 bun: '#F0A850' / dark '#B9712A'  (sesame)
1 lettuce: '#6FCF57' / '#3E8B2F'
2 tomato: '#EF5350' / '#A92A2A'
3 cheese: '#FFD54F' / '#C79A18'
4 onion: '#B084F0' / '#6E46A8'
5 bacon: '#FF8A65' / '#C25436'

Points: 50 * type? Let's do base 25 + row bonus. Combo multiplies.

Now, draw functions.

drawBackground(): 
```js
function drawBackground(t){
  const g=ctx.createLinearGradient(0,0,0,LH);
  g.addColorStop(0,'#1b0f2b'); g.addColorStop(0.55,'#140a1f'); g.addColorStop(1,'#2a0f18');
  ctx.fillStyle=g; ctx.fillRect(0,0,LW,LH);
  // tiles
  ctx.save(); ctx.globalAlpha=0.06; ctx.fillStyle='#ffd9a8';
  const s=60;
  for(let y=0;y<LH;y+=s) for(let x=0;x<LW;x+=s){ if(((x/s)+(y/s))%2===0) ctx.fillRect(x,y,s,s); }
  ctx.restore();
  // glow bottom
  const rg=ctx.createRadialGradient(LW/2,LH,10,LW/2,LH,LH*0.6);
  rg.addColorStop(0,'rgba(255,120,60,0.22)'); rg.addColorStop(1,'rgba(255,120,60,0)');
  ctx.fillStyle=rg; ctx.fillRect(0,0,LW,LH);
}
```
Performance: 100/60 * 640/60 ≈ 180 rects per frame — fine.

Floating decorative hearts: keep array of 14 with slow upward motion, alpha 0.12.

HUD drawing at top:
```js
function drawHUD(){
  ctx.save();
  // panel
  ...
  ctx.font='700 13px ...'; 
}
```
Use a font stack: "system-ui, 'Segoe UI', Roboto, sans-serif" — but canvas needs the font loaded; system fonts fine.

Let's write the code for HUD:
- Left: "SCORE" (12px, letterspacing simulated), value "12,340" (28px bold).
- Center: "LEVEL 3" and "DEMO" badge.
- Right: hearts for lives + "COMBO ×3" maybe center-right.

Top region 0..90; bricks start at 100. HUD baseline y=30/58.

Wait the top wall for the ball is at y=40+ ... I set ball ceiling at y= 88? Let's set ceiling = 84 so the ball doesn't go into the HUD. Actually let's place HUD in the top-left and top-right corners and the level in the middle top, and set the ball ceiling at y=76. Bricks start at 100. Fine.

Hmm, the ball ceiling at 76 and HUD at top might overlap visually with the center level text, but the ball is rarely there. Set ceiling=78.

OK, writing final code now. Also powerup icons.

Let me write the complete script.

```js
/* ---------- helpers ---------- */
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];
```

Now the code for explosion:

```js
function burstHearts(x,y,count,colors,power=1){
  for(let i=0;i<count;i++){
    const a=rand(0,Math.PI*2), sp=rand(1.2,5.2)*power;
    parts.push({
      x:x+rand(-8,8), y:y+rand(-6,6),
      vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-1.6,
      rot:rand(0,6.28), vrot:rand(-0.25,0.25),
      size:rand(3,8.5), life:rand(1.6,2.8), max:2.8,
      color: pick(colors)
    });
  }
}
```
max life should match each particle's life for fading; store `life0`.

Colors: hearts are pink: '#FF6B8A','#FF4D6D','#FF9EB5','#FFB3C1','#F0426B'. Plus brick color variant.

On brick destroy: 30 hearts, plus 12 crumbs.

On damaged brick: 6 hearts + crumbs.

Ball trail: store last few positions, draw small translucent circles.

Let's write the paddle draw with a "magnet" look.

Also draw the paddle's "bun" with sesame seeds; make it look good.

Time to write the final code. I'll write it out fully and check for errors mentally.

Additional: `state.banner` for "LEVEL 2!" text in center that fades.

Let me write the update for serve:
```js
function updateServe(dt){
  if(balls.length===0){
    state.serveTimer-=dt;
    if(state.serveTimer<=0){ launchBall(); }
  }
}
```
And the ball attached to paddle: I'll not attach; instead when balls empty, wait serveTimer then spawn at paddle position and launch.

So:
```js
function launchBall(){
  const b={x:paddle.x+paddle.w/2, y:paddle.y-14, r:10, vx:0, vy:0, trail:[]};
  const sp=baseSpeed();
  const ang=rand(-0.45,0.45);
  b.vx=Math.sin(ang)*sp; b.vy=-Math.cos(ang)*sp;
  balls.push(b);
}
```
And in update, if balls.length===0: state.serveTimer -= dt; if <=0 launchBall(); and paddle still controllable.

On player mode, also allow pressing space to launch immediately (set serveTimer=0).

baseSpeed = 5.4 + level*0.35, times slow factor.

Let me handle "slow" effect: state.slowT>0 → multiply ball velocities by 0.72 each frame? That would compound. Instead store ball.speed and recompute direction... Simpler: apply the factor at collision time only (when we recompute vx,vy from speed) and store `b.speed`. Let's store speed on the ball and recompute velocity from direction when bouncing:
- On paddle hit: keep speed b.speed, set angle.
- On brick hit: flip component, speed same.
- slow: b.speed = base * 0.75 applied globally by changing all balls' speed when the powerup is collected; and when speed changes, rescale the velocity vector.

Implement helper:
```js
function setBallSpeed(b,s){ const a=Math.atan2(b.vy,b.vx); b.vx=Math.cos(a)*s; b.vy=Math.sin(a)*s; b.speed=s; }
```
On launch, b.speed = base.

When slow powerup: for each ball setBallSpeed(b, b.speed*0.75) but clamp to min.

Careful with angle: atan2 with vy negative is fine.

For paddle bounce we compute angle then set vx = sin(angle)*speed, vy=-cos(angle)*speed.

Good.

Powerups list: 'wide' increases paddle.w by 40 for 15s then reverts (store base width). 'multi' adds a ball (split from the current ball with different angle). 'slow' slows balls.

Multi ball: add a new ball at the paddle with a different angle.

Let me write the code.

Also: when the player takes over, if the AI had wide paddle etc., keep it.

Now write the HTML/CSS.

CSS:

```css
*{box-sizing:border-box}
html,body{margin:0;height:100%;}
body{
  background:
    radial-gradient(1200px 700px at 50% -10%, #3a1430 0%, transparent 60%),
    radial-gradient(900px 600px at 50% 120%, #4a1a12 0%, transparent 60%),
    #0b0612;
  color:#fff; font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  display:flex; align-items:center; justify-content:center; padding:16px;
  overflow:hidden;
}
#wrap{width:min(98vw,1080px); display:flex; flex-direction:column; gap:10px;}
#stage{position:relative; width:100%; aspect-ratio: 1000/640; border-radius:20px; overflow:hidden; box-shadow:0 30px 80px rgba(0,0,0,.6), 0 0 0 2px rgba(255,120,150,.18) inset; background:#160b20;}
canvas{display:block;width:100%;height:100%;}
```

On mobile: `#stage{aspect-ratio:1000/1000}` maybe. Let's do media query max-width 640px: aspect-ratio: 1000/1080. And smaller fonts.

But careful: the stage with aspect-ratio 1000/640 at 1080 width = 691px tall + padding; viewport 800 tall — fits (16px padding top/bottom + 691 + footer ~30 = 753). OK. But maybe cap height: add `max-height: calc(100vh - 90px)` with width auto? If we constrain height, aspect ratio breaks the mapping but we handle it via scale computation (LW fixed 1000, LH derived from actual rect) — distortion may occur if aspect differs from the CSS aspect. Since we compute LH from the actual rect, we preserve aspect automatically (no distortion) as long as we let LH vary. Good — so we can use max-height and the game just gets a taller/wider logical area. But then LH might be huge (e.g., if height constrained, width shrinks → LH bigger). Fine.

Let's include `max-height:min(74vh, 700px)` on the stage? With aspect-ratio and max-height, the browser shrinks width accordingly (aspect-ratio is respected when height is constrained? With `aspect-ratio` and both width:100% and max-height, the box will be clamped in height and the width... Actually width:100% is definite, so max-height clamps height and the aspect ratio is violated (width stays). Then LH derived from rect → taller logical area. Fine, no distortion.

I'll set: #stage { width:100%; aspect-ratio: 1000/640; max-height: calc(100vh - 96px); } On a 1280x800 viewport: width = min(98vw,1080) = 1080 → height 691; max-height = 704 → no clamp. 

On 375x800: width 367 → aspect 1000/640 gives height 235; max-height 704 → no clamp. Hmm, on mobile I wanted taller. Media query: @media (max-width:640px){ #stage{aspect-ratio: 1000/1050;} } → height = 367*1.05=385. Good.

LH then = 1050 in logical units. Ball speed might need scaling: base speed 5.4 units/frame in a 1050-tall area means it takes longer to cross. Let's scale ball speed by factor sqrt(LH/640)? For LH=1050 → factor 1.28 → speed 6.9. Hmm, that makes it relatively faster in the tall field, fine. Let's use speedScale = clamp(LH/640, 1, 1.35)? Actually simpler: keep the speed the same; the ball crossing 1050 px at 5.4*60=324px/s takes 3.2s — that's a long wait between paddle hits. That's actually more time to react but boring. Let's scale: speedScale = Math.min(1.4, LH/640). For 1050 → 1.4 (capped). Use base 5.4*1.4=7.56 units/frame = 453px/s, crossing 1050-100=950px in 2.1s. Reasonable.

I'll implement baseSpeed() = (5.3 + level*0.3) * Math.min(1.4, LH/640).

OK now writing final code. Let me carefully write all the pieces.

Also the "click or press a key to start" message should be clearly visible.

Final check on required elements:
1. Breakout game ✓
2. Paddle is a hamburger ✓
3. Every brick broken explodes into tons of little hearts ✓
4. Self-playing demo mode behind "Click or press a key to start" ✓
5. No external resources ✓
6. No console errors ✓

Let me write it.

I'll now carefully compose the final file.

For the burger drawing function, let me write it in detail:

```js
function drawBurger(cx, cy, w, h, opts){
  opts=opts||{};
  const seed=opts.seed||0;
  ctx.save();
  ctx.translate(cx-w/2, cy-h/2);
  // ---- bottom bun
  const bh=h*0.15;
  let y=h-bh;
  ctx.fillStyle='#E8A24E';
  roundRectPath(0,y,w,bh,bh*0.45);
  ctx.fill();
  // patty
  const ph=h*0.19; y-=ph;
  ctx.fillStyle='#5A2E1C';
  roundRectPath(0,y,w,ph,ph*0.35); ctx.fill();
  // cheese
  const ch=h*0.13; y-=ch;
  ctx.fillStyle='#FFC93C';
  ctx.beginPath();
  ctx.moveTo(0,y);
  ctx.lineTo(w,y);
  ctx.lineTo(w,y+ch*0.8);
  // drips
  const nd=5;
  for(let i=nd;i>=0;i--){
    const x0=w*i/nd, x1=w*(i-1)/nd;
    ...
  }
```
Simpler cheese: draw a rect and then triangles hanging below:
```js
ctx.beginPath();
ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.lineTo(w,y+ch);
for(let i=nd;i>=0;i--){ const cx1=w*i/nd; ctx.lineTo(cx1-8, y+ch+6); ctx.lineTo(cx1-w/nd, y+ch); }
ctx.closePath(); ctx.fill();
```
Hmm, ordering: we go right to left along the bottom edge creating downward drips. Let's do: after lineTo(w, y+ch), for i from nd down to 0: x = w*i/nd; ctx.lineTo(x-6, y+ch+7); ctx.lineTo(x-w/nd, y+ch); then close. That gives triangular drips. Good.

- tomato: red strip with rounded ends: rect from y-ch2.. 
- lettuce: green wavy: rect plus scalloped bottom edge (arcs).
- top bun: dome.

Let me define the vertical layout explicitly:
```
const hTopBun = h*0.30;
const hLett   = h*0.10;
const hTom    = h*0.09;
const hCheese = h*0.11;
const hPatty  = h*0.20;
const hBotBun = h*0.20; // sums to 1.0
```
Order from top: top bun (0..0.30), lettuce (0.30..0.40), tomato (0.40..0.49), cheese (0.49..0.60), patty (0.60..0.80), bottom bun (0.80..1.0).

Let me just place by cumulative y.

Draw order (painter's algorithm): draw bottom bun first? Overlaps: the bottom bun should be drawn first (behind), then patty on top of it? In a burger, the patty is above the bottom bun. Draw from bottom to top so upper items overlap lower ones' tops: draw bottom bun, then patty (overlapping the bun's top edge slightly), then cheese, then tomato, then lettuce, then top bun. Or draw top to bottom with each subsequent overlapping the previous' bottom. Both work if we add a small overlap.

I'll draw in order: top bun (dome), lettuce, tomato, cheese, patty, bottom bun — with each item's top edge overlapping the previous item's bottom by 2px. That gives a stacked look. Since the dome is drawn first and lettuce is drawn over its bottom edge, the lettuce appears in front — correct for a side view (the filling is in front of the bun? Actually the bun is behind). Either is fine visually.

Let me draw: bottom bun first, then patty, cheese, tomato, lettuce, top bun — so the top bun's lower edge overlaps the lettuce top. Looks like a proper stack. I'll add shadows.

Actually simpler & reliable: draw from bottom to top:
1. bottom bun (rounded bottom shape, flat top)
2. patty (dark brown, slightly wider, rounded)
3. cheese (yellow with drips overlapping the patty)
4. tomato (red)
5. lettuce (green wavy)
6. top bun (dome with seeds)

Widths: bottom bun w*0.92 centered; patty w*1.0; cheese w*1.02; tomato w*0.96; lettuce w*1.03; top bun w*0.94.

Let's implement.

For the top bun dome:
```js
ctx.beginPath();
ctx.moveTo(x0, yTop+hTopBun*0.55);
ctx.bezierCurveTo(x0, yTop-hTopBun*0.12, x0+w*0.22, yTop-hTopBun*0.55, x0+w*0.5, yTop-hTopBun*0.55);
ctx.bezierCurveTo(x0+w*0.78, yTop-hTopBun*0.55, x0+w, yTop-hTopBun*0.12, x0+w, yTop+hTopBun*0.55);
ctx.closePath();
```
That creates a dome. Fill with a vertical gradient.

Sesame seeds: place 4-5 at positions along the dome, small ellipses rotated.

OK. Also add a soft shadow ellipse under the paddle.

Let's write the final code. I'll write it fully now, and double-check variables.

Also, note ctx.roundRect may not exist in older browsers but is fine in latest Chrome. I'll write a helper anyway.

Let me write the whole file.

Also I'll add a subtle "steam" effect above the paddle burger? Nice touch: 2-3 wavy translucent lines rising from the bun. Cute. Maybe skip for perf; it's cheap (3 paths). I'll add small steam puffs as particles rising from the paddle occasionally.

Final code writing time.

For the overlay card content, I'll also include the controls line.

Handling of "R" restart: resets the game to player mode.

Let me write:

```js
function showOverlay(html){ card.innerHTML=html; overlay.style.display='flex'; }
function hideOverlay(){ overlay.style.display='none'; }
```

Demo mode indicator: draw a small "DEMO" badge in the HUD.

Also, when the player takes over, if balls.length===0 (ball lost) we just continue.

I think I'm ready. Let me write it all out.

One more: to guarantee hearts visible in the first screenshot, at init I'll call `burstHearts` at a few positions and destroy a couple of bricks.

Actually better: at init, set a few bricks' hp=1 and immediately "explode" 3 of them (mark dead, spawn hearts, add score). The brick field will have a couple of gaps at the top — looks like the game is in progress. Good.

Let me also make the demo AI start with a ball already in flight (so action is immediate): create the ball at the paddle and launch immediately at init.

Now, writing.

Also handle visibilitychange: pause when tab hidden? Not needed.

Let me write out the code.

```js
// ===== Particles =====
let parts=[]; // hearts
let crumbs=[];
let pops=[];
```

I'll merge crumbs into parts with a type field ('heart' | 'crumb').

Let me write the update for parts:
```js
for(let i=parts.length-1;i>=0;i--){
  const p=parts[i];
  p.life-=dt;
  if(p.life<=0){ parts.splice(i,1); continue; }
  p.vy += (p.g||0.055)*60*dt;
  p.vx *= 0.992; p.vy*=0.992;
  p.x += p.vx*60*dt; p.y+=p.vy*60*dt;
  p.rot += p.vrot*60*dt;
  if(p.bounce && p.y > LH-10 && p.vy>0){ p.y=LH-10; p.vy*=-0.45; p.vx*=0.85; }
}
```

Hearts bounce; crumbs don't (or fade faster).

Alpha = min(1, life/0.7).

OK writing final answer now. I'll produce the complete HTML.

Let me make sure about the combo: when breaking a brick, combo increments if within 1.5s of the last break; score += (25 + row*5) * (1+combo*0.25) rounded.

Score popups: "+125" floating up.

Let me write the code text now, final.

Also, I want the paddle to visually be clearly a hamburger: width 130, height 44 at logical scale (LW=1000). On a 1080px-wide stage, scale ~1.08 → 140px wide. Good.

Ball radius 11, mini burger drawn 26 wide.

Let me write drawMiniBurger for the ball: just call drawBurger with w=26,h=26.

Actually a round mini burger looks better: use drawBurger(cx,cy,26,26) — it'll render as a tiny stacked burger. Cute. Add a glow circle behind.

Now the final code.

Also, prevent the paddle from overlapping the bottom HUD? The HUD is at the top; fine.

Let me write.

I also want the AI to be visibly "playing" — the paddle moves to the ball.

Writing final code:

```js
const AI = { speed: 9.2, noise: 26 };
```

In demo mode, AI target:
```js
let target = null;
// choose the lowest falling ball
let best=null;
for(const b of balls){ if(b.vy>0){ if(!best||b.y>best.y) best=b; } }
if(best) target = predictLanding(best);
// powerups
for(const p of powerups){ if(p.y>paddle.y-260){ if(target===null||p.y>bestY) target=p.x; } }
if(target===null) target=paddle.x+paddle.w/2;
target += rand(-AI.noise,AI.noise);
```
Powerup priority: if a powerup is close to the paddle (y > paddle.y - 120) prioritize it, else ball.

AI movement:
```js
const desired = clamp(target - paddle.w/2, 0, LW-paddle.w);
const diff = desired - paddle.x;
const step = clamp(diff, -AI.speed*60*dt, AI.speed*60*dt);
paddle.x += step;
```
AI.speed in units/frame: 9 → 540 units/s. Ball horizontal speed up to ~8 units/frame = 480/s. AI can keep up. Add noise so it occasionally misses.

Also in demo mode, the AI sometimes "fails" to catch → ball lost → respawn (no life lost). That's fine; it creates action.

Let me finalize. Writing the answer now.

For the foot hint bar, style as small pill chips.

I'll now produce the code.

Additional check: `ctx.setTransform(scale,0,0,scale,0,0)` then all drawing in logical coords. Canvas width = rect.width*dpr; scale = canvas.width/LW = rect.width*dpr/1000. LH = canvas.height/scale = rect.height*dpr/(rect.width*dpr/1000) = 1000*rect.height/rect.width. 

Everything consistent.

Let me write the code for real now.

Also: the overlay should have pointer-events so the click passes to us. Add overlay.addEventListener('click', takeover) and also stage pointerdown.

Note: after takeover, hideOverlay.

Potential issue: the very first click also might be intended to just start; fine.

Write the final answer.

Let me think about the hearts colors list:
const HEART_COLORS = ['#ff5c8a','#ff3b6b','#ff89ab','#ffa6c1','#e8395f','#ffd1dc'];

Brick destroy colors: mix with brick color: colors = HEART_COLORS.concat([brickColor, brickLight]).

Number of hearts per brick: 30 + random 10 → "tons".

Let me write the final code.

I should also add a slight screen shake when a brick breaks: state.shake = 4 → applied via ctx.translate(random) in the render, decaying.

And a white flash on the brick.

OK writing.

Final detail: the demo mode should keep the game interesting — when a level is cleared, restart with level+1 and a banner "LEVEL 2".

Let me write the code.

function nextLevel(){ state.level++; buildBricks(state.level); banner('LEVEL '+state.level); }

buildBricks with rows = Math.min(7, 5 + Math.floor(level/2))? Keep 6 rows, but add more HP on later levels.

Alright — final code.