Models 44

Mistral Large 4

by MistralReasoning20 console errors
Open in new window

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

How it went

Total time
8 min 58 s
Cost
$0.091
Output tokens
43,166
Spent thinking
33,853 tokens
Speed
80.6 tok/s
Console errors
20
First token after
2.6 s
First visible text after
7 min 38 s
Prompt tokens
372
Code size
24.3 KB
Finish reason
stop
Served by
Mistral
Model id
mistralai/mistral-large-4-0
Reasoning sent
{"effort":"high"}
Max output
262,144 tokens
Generated
Nov 12, 2026

Console errors

Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

Failed to execute 'arcTo' on 'CanvasRenderingContext2D': The radius provided (-4.25) is negative.

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 · 679 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🍔 Heartbreaker — Hamburger Breakout</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; background:#160a2b; }
  body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; user-select:none; -webkit-user-select:none; }
  #game { display:block; position:fixed; inset:0; touch-action:none; }
  .overlay { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:10;
    background: radial-gradient(ellipse at center, rgba(30,10,55,0.22), rgba(10,4,24,0.55));
    transition: opacity .5s ease; }
  .overlay.hidden { opacity:0; pointer-events:none; }
  .card { text-align:center; padding:30px 42px; border-radius:24px; max-width:min(92vw,580px);
    background: rgba(24,10,46,0.62); border:1px solid rgba(255,130,170,0.35);
    box-shadow:0 0 60px rgba(255,70,130,0.25), inset 0 0 40px rgba(255,90,150,0.06);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
  h1 { font-size: clamp(26px, 7vw, 52px); font-weight:900; letter-spacing:1px; margin-bottom:10px;
    background: linear-gradient(180deg,#ffe3ec,#ff5d8f 60%,#ff2e63);
    -webkit-background-clip:text; background-clip:text; color:transparent;
    filter: drop-shadow(0 4px 14px rgba(255,60,120,0.45)); }
  .tag { color:#ffd6e3; font-size: clamp(13px, 2.4vw, 16px); margin-bottom:18px; opacity:.9; line-height:1.45; }
  .start { color:#fff; font-weight:800; font-size: clamp(15px, 3vw, 20px); margin-bottom:12px;
    animation: pulse 1.4s ease-in-out infinite; }
  .hint { color:rgba(255,214,227,0.55); font-size:12px; }
  @keyframes pulse { 0%,100%{opacity:1; transform:scale(1);} 50%{opacity:.5; transform:scale(.96);} }
  @media (max-width:480px){ .card{ padding:20px 18px; } .hint{ display:none; } }
</style>
</head>
<body>
<canvas id="game"></canvas>

<div id="overlay" class="overlay">
  <div class="card">
    <h1>🍔 HEARTBREAKER</h1>
    <p class="tag">Your paddle is a hamburger. Smash every brick and it explodes into a shower of little hearts.</p>
    <p class="start">▶ &nbsp;Click or press any key to start</p>
    <p class="hint">move: mouse or ← → &nbsp;·&nbsp; space: launch &nbsp;·&nbsp; P: pause</p>
  </div>
</div>

<div id="over" class="overlay hidden">
  <div class="card">
    <h1>💔 GAME OVER</h1>
    <p class="tag" id="finalScore"></p>
    <p class="start">▶ &nbsp;Click or press any key to play again</p>
  </div>
</div>

<script>
(() => {
'use strict';

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const overEl = document.getElementById('over');
const finalScore = document.getElementById('finalScore');

let W = 0, H = 0, dpr = 1, topWall = 78;
let bgGrad = null, glowGrad = null;

const state = { mode:'demo', score:0, lives:3, level:1, combo:0, paused:false, over:false, clearing:false, clearTimer:0, cols:10, rows:4 };
const paddle = { cx:0, y:0, w:140, h:46, vx:0, tilt:0 };
const ball = { x:0, y:0, vx:0, vy:0, r:10, stuck:true };
let bricks = [];
let particles = [];
let popups = [];
let trail = [];
const bgHearts = [];
const keys = { left:false, right:false };
let mouseX = null, lastMove = -10, lastInput = -10;
let aiErr = 0, aiTimer = 0, stuckT = 0, time = 0, shake = 0;
let AC = null;
let overToken = 0;

const HEART_COLORS = ['#ff2e63','#ff5d8f','#ff8fab','#ffb3c6','#ffd6e0','#ffffff'];
const rand = (a,b) => a + Math.random()*(b-a);

/* ---------- shape helpers ---------- */
function heartPath(c, s){
  c.beginPath();
  c.moveTo(0, 10*s);
  c.bezierCurveTo(-6*s, 4*s, -10*s, 0, -10*s, -4*s);
  c.bezierCurveTo(-10*s, -7.5*s, -7.5*s, -9.5*s, -5*s, -9.5*s);
  c.bezierCurveTo(-3*s, -9.5*s, -1*s, -8*s, 0, -6*s);
  c.bezierCurveTo(1*s, -8*s, 3*s, -9.5*s, 5*s, -9.5*s);
  c.bezierCurveTo(7.5*s, -9.5*s, 10*s, -7.5*s, 10*s, -4*s);
  c.bezierCurveTo(10*s, 0, 6*s, 4*s, 0, 10*s);
  c.closePath();
}
function drawHeart(x, y, s, rot, color, alpha){
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(rot);
  ctx.globalAlpha = alpha;
  ctx.fillStyle = color;
  heartPath(ctx, s);
  ctx.fill();
  ctx.restore();
}
function rr(c, x, y, w, h, r){
  r = Math.min(r, w/2, h/2);
  c.beginPath();
  c.moveTo(x+r, y);
  c.arcTo(x+w, y, x+w, y+h, r);
  c.arcTo(x+w, y+h, x, y+h, r);
  c.arcTo(x, y+h, x, y, r);
  c.arcTo(x, y, x+w, y, r);
  c.closePath();
}

/* ---------- sizing / layout ---------- */
function resize(){
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  W = window.innerWidth; H = window.innerHeight;
  canvas.width = Math.round(W*dpr);
  canvas.height = Math.round(H*dpr);
  canvas.style.width = W + 'px';
  canvas.style.height = H + 'px';
  topWall = Math.max(66, Math.min(92, H*0.1));
  paddle.w = Math.max(96, Math.min(170, W*0.17));
  paddle.h = paddle.w * 0.33;
  paddle.y = H - 74;
  paddle.cx = Math.max(paddle.w/2+6, Math.min(W-paddle.w/2-6, paddle.cx || W/2));
  ball.x = Math.max(ball.r, Math.min(W-ball.r, ball.x));
  ball.y = Math.max(topWall+ball.r, Math.min(H-ball.r, ball.y));
  bgGrad = ctx.createLinearGradient(0, 0, 0, H);
  bgGrad.addColorStop(0, '#2a1150');
  bgGrad.addColorStop(0.5, '#3b1663');
  bgGrad.addColorStop(1, '#170a2e');
  glowGrad = ctx.createRadialGradient(W/2, -60, 0, W/2, -60, Math.max(W, H)*0.85);
  glowGrad.addColorStop(0, 'rgba(255,90,150,0.20)');
  glowGrad.addColorStop(1, 'rgba(255,90,150,0)');
  layoutBricks();
}
window.addEventListener('resize', resize);

function buildLevel(){
  state.cols = W < 520 ? 8 : 10;
  state.rows = Math.min(3 + state.level, 7);
  bricks = [];
  const palette = ['#ff4d6d','#ff7a59','#ffb84d','#ffe14d','#8ee06a','#4dd6c1','#6aa8ff','#b48cff'];
  for(let r = 0; r < state.rows; r++){
    for(let c = 0; c < state.cols; c++){
      const gold = (r === 0 && Math.random() < 0.16);
      const hp = gold ? 1 : (state.level >= 2 && r < 2 ? 2 : 1);
      bricks.push({ col:c, row:r, alive:true, gold, hp, maxHp:hp,
        color: gold ? '#ffd166' : palette[r % palette.length], x:0, y:0, w:0, h:0 });
    }
  }
  layoutBricks();
}
function layoutBricks(){
  if(!bricks.length || !state.cols) return;
  const side = Math.max(20, W*0.045);
  const gap = Math.max(4, W*0.006);
  const bw = (W - side*2 - gap*(state.cols-1)) / state.cols;
  const bh = Math.max(20, Math.min(28, H*0.032));
  const top = topWall + 36;
  for(const b of bricks){
    b.x = side + b.col*(bw+gap);
    b.y = top + b.row*(bh+gap);
    b.w = bw; b.h = bh;
  }
}

/* ---------- audio (lazy, gesture-only) ---------- */
function initAudio(){
  if(!AC){
    try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){ AC = null; }
  }
  if(AC && AC.state === 'suspended') AC.resume();
}
function tone(f, d, type, v, slide){
  if(!AC) return;
  try {
    const t = AC.currentTime;
    const o = AC.createOscillator(), g = AC.createGain();
    o.type = type || 'triangle';
    o.frequency.setValueAtTime(f, t);
    if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, f+slide), t+d);
    g.gain.setValueAtTime(v || 0.12, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t+d);
    o.connect(g); g.connect(AC.destination);
    o.start(t); o.stop(t+d+0.05);
  } catch(e){}
}
function sfxGold(){
  tone(660, 0.09, 'square', 0.09, 0);
  setTimeout(() => tone(880, 0.12, 'square', 0.09, 0), 70);
  setTimeout(() => tone(1320, 0.18, 'square', 0.08, 0), 140);
}
function sfxLevel(){
  [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => tone(f, 0.15, 'triangle', 0.11, 0), i*90));
}

/* ---------- particles / popups ---------- */
function burst(x, y, n, brickColor, power){
  for(let i = 0; i < n; i++){
    if(particles.length > 750) particles.shift();
    const a = Math.random()*Math.PI*2;
    const sp = (60 + Math.random()*380) * power;
    const life = 0.9 + Math.random()*1.0;
    particles.push({
      x: x + rand(-10,10), y: y + rand(-8,8),
      vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - 170,
      s: 0.15 + Math.random()*0.3,
      rot: Math.random()*Math.PI*2, vr: rand(-6,6),
      life, max: life,
      color: Math.random() < 0.55 ? HEART_COLORS[(Math.random()*HEART_COLORS.length)|0] : brickColor,
      alpha: 1
    });
  }
}
function heartRain(){
  for(let i = 0; i < 110; i++){
    const life = 2.5 + Math.random()*2;
    particles.push({
      x: Math.random()*W, y: -30 - Math.random()*H*0.6,
      vx: rand(-70,70), vy: 90 + Math.random()*230,
      s: 0.2 + Math.random()*0.35,
      rot: Math.random()*6.28, vr: rand(-4,4),
      life, max: life,
      color: HEART_COLORS[(Math.random()*HEART_COLORS.length)|0],
      alpha: 1
    });
  }
}
function addPopup(x, y, text, color, size, life){
  popups.push({ x, y, text, color, size, life, max: life });
}

/* ---------- game flow ---------- */
function ballSpeed(){ return Math.min(380 + (state.level-1)*40, 620); }
function serve(){ ball.stuck = true; stuckT = 0; }
function launch(){
  if(!ball.stuck) return;
  ball.stuck = false; stuckT = 0;
  const sp = ballSpeed();
  const a = rand(-0.3, 0.3);
  ball.vx = sp*Math.sin(a);
  ball.vy = -sp*Math.cos(a);
  tone(300, 0.08, 'triangle', 0.1, 150);
}
function loseLife(){
  state.lives--; state.combo = 0;
  burst(ball.x, Math.min(ball.y, H-40), 46, '#ff5d8f', 1.2);
  shake = 9;
  tone(320, 0.45, 'sawtooth', 0.12, -270);
  if(state.lives <= 0) gameOver();
  else { serve(); addPopup(W/2, H*0.4, '💔', '#ff8fab', 34, 1.1); }
}
function gameOver(){
  state.over = true;
  finalScore.textContent = 'SCORE ' + state.score + '  ·  LEVEL ' + state.level;
  overEl.classList.remove('hidden');
  if(state.mode === 'demo'){
    const tok = ++overToken;
    setTimeout(() => { if(tok === overToken && state.over){ resetGame(); overEl.classList.add('hidden'); } }, 3000);
  }
}
function resetGame(){
  state.score = 0; state.lives = 3; state.level = 1; state.combo = 0;
  state.over = false; state.clearing = false; state.paused = false;
  popups.length = 0;
  buildLevel();
  serve();
}

function hitBrick(b){
  b.hp--;
  if(b.hp <= 0){
    b.alive = false;
    state.combo++;
    const pts = (b.gold ? 50 : 10) + 5*state.combo;
    state.score += pts;
    addPopup(b.x + b.w/2, b.y, '+' + pts, '#ffffff', 15, 0.9);
    if(state.combo >= 3) addPopup(b.x + b.w/2, b.y - 24, 'COMBO ×' + state.combo, '#ff8fab', 14, 0.9);
    burst(b.x + b.w/2, b.y + b.h/2, b.gold ? 56 : Math.min(46, 22 + state.level*4), b.color, b.gold ? 1.4 : 1);
    shake = Math.min(7, 2.5 + state.combo*0.4);
    tone(300 + Math.min(state.combo,10)*35, 0.1, 'triangle', 0.12, 240);
    if(b.gold) sfxGold();
  } else {
    burst(b.x + b.w/2, b.y + b.h/2, 8, b.color, 0.7);
    tone(220, 0.07, 'square', 0.08, -60);
  }
}

/* ---------- ball physics ---------- */
function moveBall(dt){
  const sp = Math.hypot(ball.vx, ball.vy);
  const steps = Math.max(1, Math.ceil(sp*dt/6));
  const sdt = dt/steps;
  for(let i = 0; i < steps; i++){
    ball.x += ball.vx*sdt;
    ball.y += ball.vy*sdt;
    if(ball.x < ball.r){ ball.x = ball.r; ball.vx = Math.abs(ball.vx); tone(170, 0.05, 'square', 0.05); }
    else if(ball.x > W-ball.r){ ball.x = W-ball.r; ball.vx = -Math.abs(ball.vx); tone(170, 0.05, 'square', 0.05); }
    if(ball.y < topWall + ball.r){ ball.y = topWall + ball.r; ball.vy = Math.abs(ball.vy); tone(190, 0.05, 'square', 0.05); }

    // hamburger paddle bounce
    if(ball.vy > 0 && ball.y + ball.r >= paddle.y && ball.y - ball.r <= paddle.y + paddle.h &&
       Math.abs(ball.x - paddle.cx) <= paddle.w/2 + ball.r){
      const off = Math.max(-1, Math.min(1, (ball.x - paddle.cx)/(paddle.w/2)));
      const ang = off * 1.05;
      const nsp = Math.min(sp + 14, 940);
      ball.vx = nsp*Math.sin(ang);
      ball.vy = -nsp*Math.cos(ang);
      ball.y = paddle.y - ball.r - 0.5;
      state.combo = 0;
      tone(240, 0.09, 'triangle', 0.14, 160);
      burst(ball.x, paddle.y, 6, '#ff8fab', 0.45);
    }

    // bricks
    for(const b of bricks){
      if(!b.alive) continue;
      if(ball.x+ball.r < b.x || ball.x-ball.r > b.x+b.w || ball.y+ball.r < b.y || ball.y-ball.r > b.y+b.h) continue;
      const cx = Math.max(b.x, Math.min(ball.x, b.x+b.w));
      const cy = Math.max(b.y, Math.min(ball.y, b.y+b.h));
      let dx = ball.x-cx, dy = ball.y-cy;
      if(dx*dx + dy*dy <= ball.r*ball.r){
        if(dx === 0 && dy === 0) dy = -1;
        if(Math.abs(dx) > Math.abs(dy)){
          ball.vx = dx > 0 ? Math.abs(ball.vx) : -Math.abs(ball.vx);
          ball.x = cx + (dx > 0 ? ball.r+0.5 : -(ball.r+0.5));
        } else {
          ball.vy = dy > 0 ? Math.abs(ball.vy) : -Math.abs(ball.vy);
          ball.y = cy + (dy > 0 ? ball.r+0.5 : -(ball.r+0.5));
        }
        hitBrick(b);
        break;
      }
    }
  }
  // keep the ball from going too flat
  const sp2 = Math.hypot(ball.vx, ball.vy);
  if(Math.abs(ball.vy) < 70){
    ball.vy = (ball.vy >= 0 ? 1 : -1) * 70;
    ball.vx = (ball.vx >= 0 ? 1 : -1) * Math.sqrt(Math.max(sp2*sp2 - 4900, 1));
  }
  if(ball.y - ball.r > H) loseLife();
}

/* ---------- paddle control (demo AI / mouse / keys) ---------- */
function aiTarget(){
  if(ball.stuck || ball.vy < 0) return W/2 + Math.sin(time*1.3)*W*0.06;
  const t = (paddle.y - ball.y)/ball.vy;
  let x = ball.x + ball.vx*t;
  const per = 2*W;
  x = ((x % per) + per) % per;
  if(x > W) x = per - x;
  return Math.max(paddle.w/2+6, Math.min(W-paddle.w/2-6, x + aiErr));
}
function controlPaddle(dt){
  const now = performance.now()/1000;
  const prev = paddle.cx;
  if(keys.left) paddle.cx -= 780*dt;
  else if(keys.right) paddle.cx += 780*dt;
  else if(state.mode === 'demo' || now - lastInput > 3.5){
    aiTimer -= dt;
    if(aiTimer <= 0){ aiTimer = 0.22; aiErr = rand(-1,1)*Math.max(6, 36 - state.level*4); }
    const tgt = aiTarget();
    const sp = Math.min(560 + state.level*40, 860);
    const d = tgt - paddle.cx;
    paddle.cx += Math.max(-sp*dt, Math.min(sp*dt, d));
  } else if(now - lastMove < 3.5 && mouseX !== null){
    paddle.cx += (mouseX - paddle.cx)*Math.min(1, dt*20);
  }
  paddle.cx = Math.max(paddle.w/2+6, Math.min(W-paddle.w/2-6, paddle.cx));
  paddle.vx = (paddle.cx - prev)/Math.max(dt, 1e-4);
  const targetTilt = Math.max(-0.14, Math.min(0.14, paddle.vx*0.00045));
  paddle.tilt += (targetTilt - paddle.tilt)*Math.min(1, dt*10);
}

/* ---------- update ---------- */
function updateAmbient(dt){
  time += dt;
  for(const h of bgHearts){
    h.y -= h.vy*dt;
    h.rot += h.vr*dt;
    if(h.y < -40){ h.y = H + 40; h.fx = Math.random(); }
  }
  for(let i = particles.length-1; i >= 0; i--){
    const p = particles[i];
    p.life -= dt;
    if(p.life <= 0){ particles.splice(i,1); continue; }
    p.vy += 560*dt;
    const dr = Math.max(0, 1 - 1.5*dt);
    p.vx *= dr; p.vy *= Math.max(0, 1 - 0.55*dt);
    p.x += p.vx*dt; p.y += p.vy*dt;
    p.rot += p.vr*dt;
    p.alpha = Math.min(1, p.life/0.5);
  }
  for(let i = popups.length-1; i >= 0; i--){
    const p = popups[i];
    p.life -= dt; p.y -= 46*dt;
    if(p.life <= 0) popups.splice(i,1);
  }
  shake = Math.max(0, shake - 26*dt);
}
function update(dt){
  updateAmbient(dt);
  controlPaddle(dt);
  if(ball.stuck){
    ball.x = paddle.cx;
    ball.y = paddle.y - ball.r - 2;
    if(state.mode === 'demo' && !state.clearing){
      stuckT += dt;
      if(stuckT > 0.8) launch();
    }
  } else {
    moveBall(dt);
    trail.unshift({ x: ball.x, y: ball.y });
    if(trail.length > 11) trail.pop();
  }
  if(state.clearing){
    state.clearTimer -= dt;
    if(state.clearTimer <= 0){
      state.level++;
      state.clearing = false;
      buildLevel();
      serve();
      addPopup(W/2, H*0.42, 'LEVEL ' + state.level, '#ffd6e0', 38, 1.6);
      sfxLevel();
    }
  } else if(bricks.length && bricks.every(b => !b.alive)){
    state.clearing = true;
    state.clearTimer = 1.7;
    ball.stuck = true;
    heartRain();
    addPopup(W/2, H*0.45, 'LEVEL CLEAR!', '#ff8fab', 42, 1.5);
    sfxLevel();
  }
}

/* ---------- drawing ---------- */
function drawBrick(b){
  rr(ctx, b.x, b.y, b.w, b.h, 6);
  ctx.fillStyle = b.color; ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,0.30)';
  rr(ctx, b.x+2, b.y+2, b.w-4, b.h*0.4, 4); ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,0.20)';
  rr(ctx, b.x+2, b.y+b.h*0.62, b.w-4, b.h*0.34, 4); ctx.fill();
  if(b.gold) drawHeart(b.x + b.w/2, b.y + b.h/2 + 1, 0.34, 0, 'rgba(255,255,255,0.95)', 1);
  if(b.maxHp > 1 && b.hp < b.maxHp){
    ctx.strokeStyle = 'rgba(40,5,20,0.5)'; ctx.lineWidth = 1.5;
    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.55); ctx.lineTo(b.x+b.w*0.34, b.y+b.h-3);
    ctx.moveTo(b.x+b.w*0.66, b.y+4); ctx.lineTo(b.x+b.w*0.58, b.y+b.h*0.5); ctx.lineTo(b.x+b.w*0.72, b.y+b.h-4);
    ctx.stroke();
  }
}
function drawBurger(){
  const p = paddle, w = p.w, u = w/140, cx = p.cx, y0 = p.y;
  const x0 = cx - w/2, x1 = cx + w/2;
  // soft shadow
  ctx.fillStyle = 'rgba(0,0,0,0.30)';
  ctx.beginPath(); ctx.ellipse(cx, y0 + p.h + 5, w*0.46, 5, 0, 0, Math.PI*2); ctx.fill();
  ctx.save();
  ctx.translate(cx, y0 + p.h/2);
  ctx.rotate(p.tilt);
  ctx.translate(-cx, -(y0 + p.h/2));
  // bottom bun
  rr(ctx, x0+2*u, y0+39*u, w-4*u, 7*u, 3*u); ctx.fillStyle = '#e8a558'; ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,0.15)';
  rr(ctx, x0+2*u, y0+43.5*u, w-4*u, 2.5*u, 1.2*u); ctx.fill();
  // patty
  rr(ctx, x0+1.5*u, y0+30*u, w-3*u, 9*u, 4*u); ctx.fillStyle = '#6b3a1c'; ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  const dots = [[-40,33.5],[-18,36],[0,33],[22,36],[40,33.5]];
  for(const d of dots){ ctx.beginPath(); ctx.arc(cx+d[0]*u, y0+d[1]*u, 0.9*u, 0, Math.PI*2); ctx.fill(); }
  // cheese with drips
  ctx.fillStyle = '#ffc93c';
  rr(ctx, x0+3*u, y0+25*u, w-6*u, 5*u, 1.5*u); ctx.fill();
  const drips = [x0+16*u, cx-26*u, cx+26*u, x1-16*u];
  for(const dx of drips){
    ctx.beginPath();
    ctx.moveTo(dx, y0+29.5*u);
    ctx.lineTo(dx+6*u, y0+29.5*u);
    ctx.lineTo(dx+3*u, y0+34.5*u);
    ctx.closePath(); ctx.fill();
  }
  // tomato
  rr(ctx, x0+5*u, y0+20*u, w-10*u, 5*u, 2.5*u); ctx.fillStyle = '#e8483f'; ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,0.25)';
  rr(ctx, x0+8*u, y0+21*u, w-16*u, 1.4*u, 0.7*u); ctx.fill();
  // wavy lettuce
  ctx.beginPath();
  ctx.moveTo(x0-4*u, y0+20*u);
  const step = 12*u;
  for(let x = x0-4*u; x < x1+4*u; x += step){
    ctx.quadraticCurveTo(x+step/2, y0+12.5*u, Math.min(x+step, x1+4*u), y0+20*u);
  }
  ctx.lineTo(x1+4*u, y0+22*u);
  ctx.lineTo(x0-4*u, y0+22*u);
  ctx.closePath();
  ctx.fillStyle = '#7ed957'; ctx.fill();
  // top bun dome
  const g = ctx.createLinearGradient(0, y0, 0, y0+16*u);
  g.addColorStop(0, '#f8cd8a'); g.addColorStop(1, '#e0973f');
  ctx.beginPath();
  ctx.moveTo(x0, y0+15*u);
  ctx.lineTo(x0, y0+9*u);
  ctx.quadraticCurveTo(x0, y0, cx, y0);
  ctx.quadraticCurveTo(x1, y0, x1, y0+9*u);
  ctx.lineTo(x1, y0+15*u);
  ctx.closePath();
  ctx.fillStyle = g; ctx.fill();
  // shine
  ctx.fillStyle = 'rgba(255,255,255,0.30)';
  ctx.beginPath(); ctx.ellipse(cx - w*0.18, y0+5*u, 13*u, 3.2*u, -0.25, 0, Math.PI*2); ctx.fill();
  // sesame seeds
  ctx.fillStyle = '#fff3d6';
  const seeds = [[-34,8,-0.5],[-20,5,0.3],[-5,4,-0.2],[11,5,0.25],[27,8,-0.3]];
  for(const s of seeds){
    ctx.save();
    ctx.translate(cx + s[0]*u, y0 + s[1]*u);
    ctx.rotate(s[2]);
    ctx.beginPath(); ctx.ellipse(0, 0, 2.4*u, 1.3*u, 0, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
  // happy face on the patty
  const ex = 10*u, ey = y0 + 34*u;
  ctx.fillStyle = '#ffffff';
  ctx.beginPath(); ctx.arc(cx-ex, ey, 2.6*u, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(cx+ex, ey, 2.6*u, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = '#2a1508';
  ctx.beginPath(); ctx.arc(cx-ex+0.5*u, ey+0.4*u, 1.25*u, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(cx+ex+0.5*u, ey+0.4*u, 1.25*u, 0, Math.PI*2); ctx.fill();
  ctx.strokeStyle = '#2a1508'; ctx.lineWidth = 1.6*u; ctx.lineCap = 'round';
  ctx.beginPath(); ctx.arc(cx, y0+32.5*u, 4.6*u, Math.PI*0.25, Math.PI*0.75); ctx.stroke();
  ctx.restore();
}
function drawBall(){
  ctx.save();
  ctx.translate(ball.x, ball.y);
  ctx.rotate(Math.sin(time*3)*0.15);
  ctx.shadowColor = '#ff4d8d'; ctx.shadowBlur = 18;
  ctx.fillStyle = '#ff2e63';
  heartPath(ctx, 0.7); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,0.85)';
  ctx.beginPath(); ctx.ellipse(-3, -4, 2, 1.2, -0.6, 0, Math.PI*2); ctx.fill();
  ctx.restore();
}
function drawHUD(){
  const fs = Math.max(13, Math.min(17, W*0.018));
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'left';
  ctx.fillStyle = 'rgba(255,190,215,0.6)';
  ctx.font = '700 ' + fs + 'px system-ui, sans-serif';
  ctx.fillText('SCORE', 18, 26);
  ctx.fillStyle = '#ffffff';
  ctx.font = '800 ' + (fs+5) + 'px system-ui, sans-serif';
  ctx.fillText(String(state.score).padStart(6,'0'), 18, 46);
  ctx.textAlign = 'center';
  ctx.fillStyle = 'rgba(255,214,227,0.9)';
  ctx.font = '800 ' + (fs+2) + 'px system-ui, sans-serif';
  ctx.fillText('🍔 HEARTBREAKER', W/2, 22);
  ctx.fillStyle = 'rgba(255,255,255,0.75)';
  ctx.font = '700 ' + fs + 'px system-ui, sans-serif';
  ctx.fillText('LEVEL ' + state.level, W/2, 42);
  if(state.combo >= 2){
    ctx.fillStyle = '#ff8fab';
    ctx.font = '800 ' + (fs+4) + 'px system-ui, sans-serif';
    ctx.fillText('COMBO ×' + state.combo, W/2, 63);
  }
  const n = Math.min(Math.max(state.lives, 0), 5);
  for(let i = 0; i < 5; i++){
    drawHeart(W-28-i*28, 32, 0.55, 0, i < n ? '#ff4d8d' : 'rgba(255,255,255,0.14)', 1);
  }
}
function render(){
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = glowGrad; ctx.fillRect(0, 0, W, H);
  for(const h of bgHearts) drawHeart(h.fx*W, h.y, h.s, h.rot, h.color, h.alpha);
  ctx.save();
  if(shake > 0.3) ctx.translate(rand(-shake, shake), rand(-shake, shake));
  for(const b of bricks) if(b.alive) drawBrick(b);
  for(let i = trail.length-1; i >= 0; i--){
    const k = 1 - i/trail.length;
    drawHeart(trail[i].x, trail[i].y, 0.12 + 0.22*k, 0, '#ff8fab', 0.35*k);
  }
  for(const p of particles) drawHeart(p.x, p.y, p.s, p.rot, p.color, p.alpha);
  ctx.textAlign = 'center';
  ctx.textBaseline = 'alphabetic';
  for(const p of popups){
    ctx.globalAlpha = Math.max(0, p.life/p.max);
    ctx.fillStyle = p.color;
    ctx.font = '800 ' + p.size + 'px system-ui, sans-serif';
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
  drawBurger();
  if(!state.over) drawBall();
  ctx.restore();
  drawHUD();
  if(state.paused && !state.over){
    ctx.fillStyle = 'rgba(10,4,22,0.55)'; ctx.fillRect(0, 0, W, H);
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillStyle = '#ffffff'; ctx.font = '900 36px system-ui, sans-serif';
    ctx.fillText('PAUSED', W/2, H/2 - 12);
    ctx.font = '600 16px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.7)';
    ctx.fillText('press P to resume', W/2, H/2 + 22);
  }
}

/* ---------- input ---------- */
function startFromGesture(){
  initAudio();
  lastInput = performance.now()/1000;
  if(state.over){ resetGame(); overEl.classList.add('hidden'); }
  if(state.mode === 'demo'){ state.mode = 'play'; overlay.classList.add('hidden'); }
  if(ball.stuck && !state.clearing) launch();
}
window.addEventListener('pointerdown', e => {
  mouseX = e.clientX;
  lastMove = performance.now()/1000;
  startFromGesture();
});
window.addEventListener('pointermove', e => {
  mouseX = e.clientX;
  lastMove = performance.now()/1000;
  lastInput = performance.now()/1000;
});
window.addEventListener('keydown', e => {
  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].indexOf(e.key) !== -1) e.preventDefault();
  if(e.key === 'ArrowLeft') keys.left = true;
  if(e.key === 'ArrowRight') keys.right = true;
  if(e.key === 'p' || e.key === 'P'){ if(!state.over) state.paused = !state.paused; }
  if(e.key === ' ' && ball.stuck && !state.clearing) launch();
  startFromGesture();
});
window.addEventListener('keyup', e => {
  if(e.key === 'ArrowLeft') keys.left = false;
  if(e.key === 'ArrowRight') keys.right = false;
});

/* ---------- main loop ---------- */
let last = performance.now();
function frame(t){
  requestAnimationFrame(frame);
  let dt = (t - last)/1000;
  last = t;
  if(dt > 0.05) dt = 0.05;
  if(dt < 0) dt = 0;
  if(!state.paused && !state.over) update(dt);
  else updateAmbient(dt);
  render();
}

/* ---------- init ---------- */
for(let i = 0; i < 34; i++){
  bgHearts.push({
    fx: Math.random(), y: Math.random()*1200,
    vy: rand(8,26), s: rand(0.25,0.8),
    rot: Math.random()*6.28, vr: rand(-0.5,0.5),
    alpha: rand(0.04,0.11),
    color: HEART_COLORS[(Math.random()*HEART_COLORS.length)|0]
  });
}
resize();
paddle.cx = W/2;
buildLevel();
ball.x = paddle.cx;
ball.y = paddle.y - ball.r - 2;
serve();
requestAnimationFrame(frame);

})();
</script>
</body>
</html>
Reasoning trace (33,853 tokens)
The user wants a Breakout game where the paddle is a hamburger and bricks explode into heart particles. Let me create a self-contained HTML file with canvas-based breakout game.

Requirements:
- Self-contained, inline CSS/JS
- 1280×800 viewport, adapt to 375px
- Self-playing demo mode with "Click or press a key to start" message
- No localStorage etc.
- Zero console errors
- Interesting on first paint (particles/hearts exploding visible within first seconds)

Design:
- Canvas full width, dark background
- Paddle = hamburger (drawn with canvas: bun top with sesame seeds, lettuce, tomato, cheese, patty, bun bottom)
- Bricks in rows with colors
- Ball bounces, breaks bricks → hearts particle explosion (many small hearts, pink/red, with physics, gravity, fade)
- AI demo mode: paddle follows ball automatically
- Click/space to start; before start, show "Click or press a key to start" overlay, but game should be self-playing demo — so maybe start in demo mode automatically with AI playing, and clicking takes control? The instructions say: "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: show the message overlay, AI plays behind it; on click/key, message hides and player controls (mouse/keys), or maybe just continues. Let's do: demo mode AI plays; clicking or pressing key switches to player control (mouse move or arrow keys). Actually simpler: on click/key, hide message and let player control with mouse; AI still plays if no mouse movement? Let's make it: after start, mouse controls paddle; arrow keys also work. If no interaction, maybe AI takes over after some idle time. Keep simple: after click, player controls via mouse/arrows; also AI can take over if idle 3s. That's fine.

Hearts: draw heart shape with bezier curves, scale, rotate, alpha fade, gravity. Spawn ~30-60 hearts per brick with random velocity, colors (pink, red, hot pink, white).

Also score, lives, level progression. When all bricks cleared, next level with more rows / faster ball.

Hamburger paddle drawing:
- bottom bun: rounded rect, tan color (#e8a85c / #d9944a)
- patty: dark brown rounded rect
- cheese: yellow with drips (triangle drips)
- tomato: red slice
- lettuce: green wavy
- top bun: dome (arc) with sesame seeds (small white ellipses)
Add slight tilt based on velocity for juice. Maybe eyes? Cute. Add a face? Keep it clean — maybe little smile. Could be cute: hamburger with happy face. Let's add simple face on the top bun? Might look odd. Maybe eyes on the patty area... Let's keep the burger clean but add a subtle highlight. Actually a cute face makes it memorable — put two eyes and smile across the middle. Hmm, burgers with faces are meme-y and fun. I'll add small eyes on the top bun lower area... Let me just add eyes+smile on the patty? I'll do: eyes on the cheese/lettuce line. Simpler: draw face on the front of the burger centered vertically. I'll try eyes at the lettuce level. It could look goofy-fun. I'll include it subtly.

Ball: maybe a little meatball? Or a heart-shaped ball! Ball as a heart would fit theme. Yes — heart ball, pink/red.

Background: dark gradient with subtle floating hearts pattern. Maybe some emoji-like decorative hearts floating slowly.

UI: score top-left, lives (little hearts) top-right, level. Title "HEARTBREAKER" or "BURGER BREAKOUT". Title: "🍔 HEARTBREAKER". No external fonts — use system fonts.

Layout: canvas centered, responsive. Use devicePixelRatio scaling. Handle resize.

Game logic:
- bricks grid: cols ~10, rows 5-7, colors by row (gradient rainbow or pastel). Brick rounded rect with slight 3D highlight.
- ball speed increases per level.
- collision: ball vs bricks (AABB with circle), ball vs paddle (angle based on hit position), walls.
- paddle movement: mouse x, arrow keys, AI.
- lives 3; ball lost → reset ball on paddle.
- particles: hearts array with pos, vel, size, rotation, rotSpeed, life, color. Update: gravity slight, drag, fade. Draw heart path scaled.
- Also small confetti squares? Hearts suffice, "tons of little hearts" — spawn 40+ per brick.
- Screen shake on brick break? Small shake adds juice. Keep subtle.
- Combo text popups ("+10", "COMBO x3") floating.

Demo AI: paddle target = ball.x + offset prediction; move toward it with max speed slightly less than ball so it occasionally misses? For demo, make it good but not perfect: target ball.x with some noise; speed limited. Before start (overlay shown), AI plays so action visible behind translucent overlay. On click/keypress: overlay fades, mode = 'play', mouse/keys control. If no input for 4s, AI resumes (nice touch).

Pause on blur? Not necessary.

Sound: could add WebAudio bleeps — allowed (no external). Add simple oscillator sounds for bounce/break, with mute toggle? Keep it — small code. Must not error before user gesture: create AudioContext lazily on first gesture. Fine.

Let's write the code.

Canvas sizing: fit container with max width ~1100px, height ~ min(800-ish). Use CSS: canvas width 100%, aspect via JS. Simpler: set canvas to fill window area under header? I'll make a game area div with canvas sized to window (innerWidth, innerHeight minus nothing) — full-window canvas is easiest and responsive. HUD drawn in DOM overlay or on canvas. I'll draw HUD on canvas (score, lives hearts, level) — simpler, no DOM sync issues. But DOM HUD is crisper; canvas fine.

Full-window canvas: width = innerWidth, height = innerHeight. Game coordinates = canvas pixels with DPR scaling: set canvas.width = innerWidth*dpr, ctx.scale(dpr,dpr), use CSS pixel coords. Handle resize: recompute, reposition paddle/bricks proportionally (or just rebuild level keeping brick layout relative to width).

Bricks: margin from top 90, side margins 40. Brick width = (W - 2*margin - (cols-1)*gap)/cols, height 26.

Paddle: width ~ 140 (scale with width: clamp(110, W*0.14, 170)), height ~ burger height ~ 46. Position y = H - 70.

Ball radius 9.

Heart drawing function:
```js
function drawHeart(ctx, x, y, s, rot, color, alpha) {
  ctx.save();
  ctx.translate(x,y);
  ctx.rotate(rot);
  ctx.scale(s,s);
  ctx.globalAlpha = alpha;
  ctx.fillStyle = color;
  ctx.beginPath();
  // heart path centered, size ~1
  ctx.moveTo(0, 0.35);
  ctx.bezierCurveTo(-0.5, -0.05, -0.35, -0.55, 0, -0.25);
  ctx.bezierCurveTo(0.35, -0.55, 0.5, -0.05, 0, 0.35);
  ctx.fill();
  ctx.restore();
}
```
Let me get a decent heart path. Standard heart with unit size:
```
moveTo(0, 0.3)
bezierCurveTo(0, 0.1, -0.2, 0, -0.3, 0)  hmm
```
Common heart: 
```
ctx.moveTo(0, 3);
ctx.bezierCurveTo(-5, -3, -10, 2, 0, 10); // not symmetric
```
Let me define symmetric heart in a 20x20 box centered at origin, width ~16, height ~14:
Top dip at (0,-2). Left lobe center (-4,-5) radius ~4. Bottom point (0,7).

Path:
```
moveTo(0, 7)
bezierCurveTo(-8, 0, -7, -7, -3.5, -7)
bezierCurveTo(-1, -7, 0, -5, 0, -3.5)   // hmm dip
bezierCurveTo(0, -5, 1, -7, 3.5, -7)
bezierCurveTo(7, -7, 8, 0, 0, 7)
```
Let me reason: classic heart via two arcs + triangle. Alternative simpler with arcs:
```
moveTo(0, 6)
arc(-4, -2, 4.2, PI*0.75?, ...)
```
Bezier version widely used:
```
moveTo(0, 4)
c -1.5,-4 -6,-2 -6,1.5  ... 
```
I'll just use a known-good heart path (from memory, common one scaled to 24):
```
ctx.moveTo(12, 21);
ctx.bezierCurveTo(6, 15, 2, 11, 2, 7);
ctx.bezierCurveTo(2, 3.5, 4.5, 1.5, 7, 1.5);
ctx.bezierCurveTo(9, 1.5, 11, 3, 12, 5);
ctx.bezierCurveTo(13, 3, 15, 1.5, 17, 1.5);
ctx.bezierCurveTo(19.5, 1.5, 22, 3.5, 22, 7);
ctx.bezierCurveTo(22, 11, 18, 15, 12, 21);
```
This is a heart in 24x21 box, top-left origin. To center: translate(-12, -10.5) then scale. Good, use that. For the ball and lives too.

Ball as heart: draw that path scaled to radius ~10, fill pink with white shine? Add small white circle highlight.

Particles on brick break: count = 26 + level*4 capped ~50. Each: pos at brick center + random offset, vel random angle speed 1-7 (scaled by brick size?), up bias, gravity 0.12, drag 0.985, life 60-120 frames, size 0.5-1.4 (scale factor where base heart is 24px → s in [0.25..0.7] → 6-17px... "little hearts" so s 0.2-0.55 → 5-13px). Rotation random, rotSpeed random. Color from palette: ['#ff4d6d','#ff758f','#ff8fab','#ffc2d1','#e63950','#ff2e63','#fff0f3'] plus brick's own color maybe. Use brick color variants + pinks.

Also spawn hearts when ball hits paddle? Small puff of 6 hearts. And on losing a life, big burst? Maybe burst at ball loss. And on level clear, big heart rain.

Floating score popups: array of {x,y,text,life,color}.

Screen shake: shake magnitude decays; on brick break shake = 3 (more for bigger combos).

Bricks: each brick {x,y,w,h,hp,color,alive, row}. Some bricks need 2 hits (hp 2) shown with cracks/darker or a heart crack — draw crack lines when hp<max. Colors by row: rows palette e.g. top rows hot pinks/reds, then oranges, yellows, greens, blues, purples — rainbow pastel. Or all pink/red shades to match theme with occasional gold brick (extra points + more hearts). Let's do rainbow-ish pastel rows — looks nice against dark bg. Gold bricks (random 1-2 per level) worth more and burst bigger.

Collision ball-brick: circle vs rect — find closest point, check distance. On hit, determine side by comparing penetration: compute overlap on x and y from previous position; simpler: use ball previous position to determine which side. Standard: compute closest point; if collision, reflect based on which face: if |ball.x - closest.x| ... Use approach: dx = ball.x - closestX, dy = ball.y - closestY; if |dx| > |dy| reflect vx else reflect vy. That works reasonably.

Paddle collision: if ball moving down and ball.y + r >= paddle.y and ball.y - r <= paddle.y + h and |ball.x - paddle.cx| <= paddle.w/2 + r: reflect with angle based on offset: offset = (ball.x - paddle.cx)/(paddle.w/2) clamp [-1,1]; angle = offset * 60° from vertical; speed = current speed (increase slightly each hit up to max). Set vy negative.

Walls: left/right reflect vx, top reflect vy. Bottom: lose life.

Levels: level increments when all bricks cleared; rows = 4 + min(level, 4) capped 8? cols = 10. Ball speed = 5 + level*0.6 capped 11. Rebuild bricks, reset ball, show "LEVEL N" popup. Lives persist. Score +10*level*combo? Keep score = 10 per hp, gold 50.

Ball stuck to paddle at start of serve: ball sits on paddle, launches on click/key or auto after 1.5s in demo. In demo mode auto-launch.

Modes: 'demo' (AI, overlay visible) → on pointerdown/keydown: mode='play', overlay hides (fade via CSS class), lastInput = now. In 'play', paddle follows mouse x (if moved recently) or keys; if now-lastInput > 3500, AI assists again (mode stays 'play' but aiActive). Simplify: always compute aiTarget; if mode==='demo' or idle>3.5s → use AI; else use pointer/keys. Overlay message: "Click or press any key to start" with pulsing. Also show "Demo mode — AI playing" small text? The requirement: self-playing demo behind the message. Overlay should be semi-transparent so game visible. Good.

Also pressing key launches ball if waiting.

Pause when tab hidden: use requestAnimationFrame; on visibilitychange set paused to avoid huge dt. Clamp dt anyway. Use fixed timestep-ish: just clamp dt to 33ms and use per-frame units assuming 60fps; simpler: use dt in seconds and scale speeds. I'll do dt-based: speeds in px/sec. Let me use dt seconds, speed ball ~ 420 px/s * ... At 800px height, crossing in ~2s → speed ~ 400-600 px/s. OK use px/s.

Paddle speed: 700 px/s for keys/AI; mouse sets target directly (lerp fast).

Let me now write heart path helper:

```js
function heartPath(ctx, s){ // s = scale so heart ~ 24*s px tall
  ctx.beginPath();
  ctx.moveTo(0, 7.5*s);
  ctx.bezierCurveTo(-6, 1.5*s, -10, -2*s, -10, -6*s);
  ...
}
```
Hmm let me re-derive a clean symmetric heart centered at origin with height ~ 16 (from y=-8 to y=8), width ~ 16 (x=-8..8):

```
moveTo(0, 8)                                   // bottom tip
bezierCurveTo(-9, 1, -8, -8, -3.5, -8)         // left side up to left lobe top
bezierCurveTo(-1.5, -8, 0, -6, 0, -4.5)         // to center dip
bezierCurveTo(0, -6, 1.5, -8, 3.5, -8)          // right lobe
bezierCurveTo(8, -8, 9, 1, 0, 8)                // right side down
closePath
```
Check left lobe: from (0,8) control (-9,1) and (-8,-8) to (-3.5,-8): curve goes left and up — the leftmost extent around x≈-8, top y≈-8. Then to dip (0,-4.5) with controls (-1.5,-8),(0,-6): creates dip. Looks plausible. Bottom tip sharp at (0,8). Good enough; hearts will look fine at small sizes. Actually the bottom might be too pointy-wide; fine.

For the 24-based path I wrote earlier it's proven shape; I'll use centered version of it: subtract (12, 11):
```
moveTo(0, 10)
bezierCurveTo(-6, 4, -10, 0, -10, -4)
bezierCurveTo(-10, -7.5, -7.5, -9.5, -5, -9.5)
bezierCurveTo(-3, -9.5, -1, -8, 0, -6)
bezierCurveTo(1, -8, 3, -9.5, 5, -9.5)
bezierCurveTo(7.5, -9.5, 10, -7.5, 10, -4)
bezierCurveTo(10, 0, 6, 4, 0, 10)
```
That's the known heart (12,21)...(2,7)... translated by (-12,-11): moveTo(0,10); c(6,15)->(-6,4); (2,11)->(-10,0); (2,7)->(-10,-4); then (2,3.5)->(-10,-7.5); (4.5,1.5)->(-7.5,-9.5); (7,1.5)->(-5,-9.5); (9,1.5)->(-3,-9.5); (11,3)->(-1,-8); (12,5)->(0,-6); then (13,3)->(1,-8); (15,1.5)->(3,-9.5); (17,1.5)->(5,-9.5); (19.5,1.5)->(7.5,-9.5); (22,3.5)->(10,-7.5); (22,7)->(10,-4); (22,11)->(10,0); (18,15)->(6,4); (12,21)->(0,10). 

So heartPath(ctx, s): scale s multiplies all coords, heart is 20 wide × 19 tall centered-ish at origin. Use for ball (s≈0.55 → ~11px), particles (s 0.15–0.4), lives icons (s 0.6).

Hamburger paddle drawing (draw at cx, top y, w, h):
Layers (h total ~ 44, w ~ 140):
- top bun: dome from y to y+16: rounded top. Draw: path: moveTo(x0, y+16) line to... Use: ctx.beginPath(); ctx.moveTo(x0, y+16); ctx.lineTo(x0, y+10); ctx.quadraticCurveTo(x0, y, cx, y); ctx.quadraticCurveTo(x1, y, x1, y+10); ctx.lineTo(x1, y+16); close. Fill #f0b26b? Bun color gradient: #f2b56b to #d98e3f. Add sesame seeds: ~7 small ellipses rotated, color #fff3d6, random-ish fixed positions (precompute relative positions).
- lettuce: y+16..y+21: wavy green — draw with small arcs along width: for x steps, quadratic bumps, fill #7ec850 / #6abf4b. Slightly wider than bun (overhang): x0-3 to x1+3.
- tomato: y+21..y+26: red #e8483f rounded rect slightly narrower, maybe with lighter inner.
- cheese: y+26..y+31: yellow #ffc93c with drips: rect plus triangles hanging at a few x positions down to y+35.
- patty: y+31..y+39: brown #6b3f1d rounded rect, slightly narrower than bun? Patty usually same width. rounded corners 4. Add darker speckles? small dots.
- bottom bun: y+39..y+46: rounded rect #e8a558 with darker bottom edge.

Face: eyes at y+22 (on tomato/lettuce?) hmm. Put face on the patty? Eyes at (cx-14, y+35) and (cx+14, y+35), white circles r3 with dark pupils r1.5; smile: arc at (cx, y+37) radius 6 from 0.2π to 0.8π, stroke dark. On brown patty, white eyes visible. Cute. Let's do it.

Tilt: rotate slightly by paddle velocity: rot = clamp(vx * 0.0006, -0.12, 0.12). Draw with save/translate/rotate.

Also add a soft shadow ellipse under paddle? On canvas bg, skip.

Background: vertical gradient #1a1033 → #2b1055 → #12081f? Purple-night with pink glow. Plus subtle stars/hearts: precompute 40 floating background hearts drifting up slowly, very low alpha (0.05-0.12), drawn behind. Nice.

HUD on canvas top: left "SCORE 000123" with label; right hearts × lives (draw heart icons, lost ones dark/outline); center "LEVEL 2". Also small title top-center? Maybe title at top center "🍔💔 HEARTBREAKER" — emoji render fine. Use text with emoji: "🍔 HEARTBREAKER". Font: bold system-ui.

Overlay: DOM div centered: big text "🍔 HEARTBREAKER", subtitle "Break bricks, burst hearts!", and pulsing "Click or press any key to start". Semi-transparent dark backdrop with blur? backdrop-filter may be fine. Use rgba(10,5,25,0.45) so game visible. On start add class 'hidden' → opacity 0, pointer-events none, transition.

Also a small hint bottom: "Mouse or ← → to move • P pause"? Keep minimal: bottom center small text "move with mouse or arrow keys". Fades after start too (or keep). I'll include in overlay only.

Pause with 'p' or space? Space launches ball / pauses. Keep: Space = launch/pause toggle? Simplify: any key starts; space also launches ball when waiting; 'p' toggles pause. Mention in overlay: "P to pause".

Sounds (WebAudio): functions beep(freq, dur, type, gain). Sounds: paddle hit (220→ square short), wall (180), brick (freq by combo: 300+combo*40, triangle), gold (arpeggio), lose life (descending), level up (rising arpeggio). Init ctx on first user gesture; if demo autoplay before gesture, skip sound (ctx null). Guard everything.

Game loop with rAF and dt clamp.

Brick layout per level:
```
cols = 10 (maybe scale down on narrow: if W<560 cols=8, gap smaller)
rows = Math.min(4 + level, 7)
top = 96, side = Math.max(24, W*0.04)
gap = 6
bw = (W - side*2 - gap*(cols-1))/cols
bh = 24 (maybe H dependent: clamp)
```
Rows colors: palette array of 8: ['#ff5d8f','#ff8a5c','#ffc94d','#a8e063','#4dd0e1','#7c8cff','#c792ea','#ff6b9d']? Rainbow pastel. Gold brick chance: rows top 2 have 15% chance to be gold (#ffd700 with shine), hp 2? Gold hp1 but 50 pts and 60 hearts.

Brick hp: rows with row index < 2 (top rows) hp 2 on higher levels? Let's: hp = (level >= 2 && row < 2) ? 2 : 1. Draw cracks when damaged: two stroke lines.

Brick draw: rounded rect fill color, top highlight lighter (overlay white alpha 0.25 top half), bottom shadow darker (black alpha 0.2 bottom). If gold: gradient + sparkle. Rounded radius 6.

Ball-paddle angle: maxAngle 65°. speed = min(speed + 8, maxSpeed) each paddle hit. maxSpeed = 900.

Ball trail: store last N positions, draw fading hearts small? Draw fading circles or mini hearts — mini hearts trail is cute. Store positions each frame (cap 12), draw heart s=0.25 alpha decreasing. 

Particles update in dt: vy += g*dt (g=500 px/s²), pos += v*dt, v *= (1 - drag*dt)... use damping factor per second: v *= Math.pow(0.4, dt)? simpler v *= (1 - 1.8*dt) clamp. life -= dt; alpha = life/maxLife. Remove when life<=0 or y > H+50.

Cap particles at ~600; if exceed, remove oldest.

Popups: {x,y,vy,text,life,maxLife,color,size} rise and fade.

Shake: shakeT decays; offset = random * shakeAmp each frame; apply ctx.translate before drawing world (not HUD).

Level clear: when no alive bricks → level++, popup "LEVEL N!", heart rain: spawn 120 hearts from top across width, rebuild bricks after short delay (1.2s) — implement with state 'levelclear' timer, ball keeps bouncing? Simpler: freeze ball (ball.stuck=true), timer counts down, then rebuild & serve.

Serve state: ball.stuck = true, ball follows paddle; launch on input or after 1.2s in demo. On launch: angle random upward: vx = speed*sin(a), vy = -speed*cos(a), a in [-0.4,0.4].

Lose life: lives--, burst hearts at ball position (40 hearts red/pink), if lives>0 serve again, else game over: overlay "GAME OVER — click to restart", reset score/lives/level=1, rebuild. In demo mode auto-restart after 2s? If demo and game over, restart automatically after 2.5s so screenshot keeps action. Good.

AI: target = predict ball.x at paddle y: if ball moving down: t = (paddleY - ball.y)/ball.vy; x = ball.x + ball.vx*t; account wall bounces: reflect x into [0,W] via folding: period 2W; x = fold(x). Implement fold function. Add error noise per level: err = (level lower → bigger) e.g. maxErr = 30 - level*4 min 6, recompute every 0.3s. AI paddle speed = min(600 + level*30, 800). Move paddle toward target with that speed. Also AI only when ball coming down or random; else drift to center.

Mouse control: on pointermove store mouseX; in play mode with recent input (<3.5s) and pointer active: paddle target = mouseX (clamp), move instantly (set cx = mouseX with lerp 0.35). Keys: left/right set keyDir; paddle cx += dir*speed*dt. Priority: if keys pressed use keys; else if recent mouse move use mouse; else AI.

Track lastPointerMove time.

Edge: on touch devices pointermove works with touch drag; also touchstart counts as start.

Resize: recompute canvas size, keep paddle y = H-64, clamp paddle cx, rebuild bricks only if W changed significantly? Rebuild bricks preserving alive state is complex; simplest: on resize, recompute brick geometry by scaling positions (store bricks with relative coords?). Store bricks with normalized x,y (fractions of playfield width) — simpler: recompute layout positions each resize from brick's col/row indices. Store col,row in brick; layout() sets x,y,w,h from cols/rows. Call layout on resize and level build. 

Playfield: use full window; HUD top 0-70.

DPR: canvas.width = innerWidth*dpr etc., ctx.setTransform(dpr,0,0,dpr,0,0) each frame start (after resize). Draw in CSS px.

Font sizes scale: HUD font 16-20px.

Now write code carefully.

Structure:

```html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>🍔 Heartbreaker — Burger Breakout</title>
<style> ... </style>
</head>
<body>
<canvas id="game"></canvas>
<div id="overlay"> ... </div>
<script> ... </script>
</body>
</html>
```

CSS: html,body margin 0, height 100%, overflow hidden, background #14092b; canvas display block. Overlay: position fixed inset 0, display flex column center, background radial-gradient(rgba(...)), transition opacity .5s, &.hidden {opacity:0; pointer-events:none}. Title big with gradient text? Use color #ff8fab with text-shadow glow. Pulse animation on start message.

JS outline:

```js
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W=0,H=0,dpr=1;
function resize(){ dpr=Math.min(window.devicePixelRatio||1,2); W=window.innerWidth; H=window.innerHeight; canvas.width=W*dpr; canvas.height=H*dpr; canvas.style.width=W+'px'; canvas.style.height=H+'px'; paddle.y=H-70; clampPaddle(); layoutBricks(); }
window.addEventListener('resize', resize);
```

State:
```js
const state = { mode:'demo', score:0, lives:3, level:1, combo:0, paused:false, over:false, clearTimer:0 };
let paddle, ball, bricks=[], particles=[], popups=[], bgHearts=[];
```

Paddle: {cx, y, w, h, vx, face...} w = clamp(W*0.16, 110, 170); h = w*0.32? burger h ~ 46 at w 140 → h = w*0.33.

Ball: {x,y,vx,vy,r:9,speed,stuck:true}.

Init bgHearts: 36 items {x,y,s,vy,rot,vr,alpha,hue}.

buildLevel(): rows = min(3+level, 7); cols = W<520?8:10; bricks=[]; for r,c create. colors palette[ r % palette.length ] — better: color by row from palette top-to-bottom reversed so top rows red/pink. palette = ['#ff4d6d','#ff7a59','#ffb84d','#ffe14d','#8ee06a','#4dd6c1','#6aa8ff','#b48cff'] top row index 0 = red. Gold: if r===0 && random<0.18 → gold. hp: level>=2 && r<2 ? 2:1; gold hp=1, gold=true.

layoutBricks(): compute metrics; for each brick set x,y,w,h from col,row. metrics stored.

Collision functions.

update(dt):
- if paused or over-with-timer? handle timers.
- paddle control (AI/keys/mouse), move, compute vx for tilt.
- ball: if stuck → position on paddle top; else move with substeps to avoid tunneling: steps = ceil(speed*dt / (r)) maybe 2-4 substeps; do simple: move then collide; speed max ~900px/s, dt max 1/30 → 30px per frame > brick height 24 → tunneling possible. Use substeps: n = Math.ceil(dist/8); loop.
- collisions: walls, paddle, bricks.
- particles, popups, bgHearts update.
- shake decay.
- level clear check.

render(): clear, bg gradient, bgHearts, apply shake translate, bricks, particles, popups, paddle (burger), ball (heart) + trail, HUD.

Loop with rAF, dt = clamp((t-last)/1000, 0, 1/30).

Input:
- pointerdown on window: start(); also set mouseX.
- pointermove: mouseX, lastMove=now.
- keydown: start(); if key ArrowLeft/Right set keys; if ' ' launch or pause? space: if ball.stuck launch; else toggle pause? Let's: Space/Enter → if stuck launch; else nothing. 'p' toggles pause. Prevent default for arrows/space to avoid scroll.
- start(): if mode==='demo'{ mode='play'; overlay.classList.add('hidden'); initAudio(); } lastInput=now; if ball.stuck launchBall(); if over restart.

Audio: 
```js
let AC=null;
function initAudio(){ if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(AC&&AC.state==='suspended') AC.resume(); }
function sfx(freq,dur,type='triangle',vol=0.15,slide=0){ if(!AC) return; const t=AC.currentTime; const o=AC.createOscillator(), g=AC.createGain(); o.type=type; o.frequency.setValueAtTime(freq,t); if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,freq+slide), t+dur); g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(0.0001,t+dur); o.connect(g).connect(AC.destination); o.start(t); o.stop(t+dur+0.02); }
```

Sounds: wall: sfx(160,0.06,'square',0.06); paddle: sfx(240,0.08,'triangle',0.12, 120); brick: sfx(320+combo*30,0.1,'triangle',0.12, 200); gold: sfx(660...) + delayed; lose: sfx(300,0.4,'sawtooth',0.12,-250); level: few notes via setTimeout — but setTimeout fine.

Heart burst function:
```js
function burst(x,y,n,colors,power=1){ for(i<n){ angle=rand*2PI; sp=(1+rand*6)*60*power? use px/s: sp = 80 + rand*420; ... } }
```
Let me define particle: {x,y,vx,vy,s,rot,vr,life,maxLife,color}. Update: vy += 520*dt; vx *= (1-1.6*dt); vy *= (1-0.6*dt)? Hearts flutter: add slight horizontal sine? Keep simple with drag. Also cap: if particles.length>700 splice oldest.

Colors: ['#ff2e63','#ff5d8f','#ff8fab','#ffb3c6','#ffd6e0','#e63950','#fff'] plus brick color maybe mixed: 50% pink palette, 50% brick color. "every brick explodes into tons of little hearts" — make hearts pink/red mostly but tie to brick: use brick color for half. Nice.

n per brick = 24 + level*3 (cap 48); gold 60.

Popup on brick: "+10" small; combo increments per consecutive brick without paddle touch? combo resets when ball hits paddle. popup "COMBO ×N" when combo>=3 at brick pos.

Ball trail: array trail of {x,y}; unshift each frame when moving; cap 10; draw hearts alpha i/10*0.35, s=0.22.

HUD: 
- left: "SCORE" small label + value; 
- center: "LEVEL n" and title small? Put title top-center small: "🍔 HEARTBREAKER" 14px alpha .7.
- right: lives hearts: draw heart icons s=0.55 at spacing 26, filled pink for remaining, dark outline for lost (globalAlpha .25 gray fill).
Use font: `bold ${Math.max(14, W*0.016)}px system-ui...`. Score text align left at x=18,y=30; label above? Single line: "SCORE 001230" with SCORE in dim. Do two draws.

Also combo display near top center-right if combo>1: "×N COMBO".

Game over overlay: reuse DOM overlay? Create second div #gameover hidden; show with score and "click to play again". In demo mode, auto restart after 2.5s (setTimeout → resetGame()). But overlay start message only initially. Manage: overlay element for start; gameover element separate. In demo, after gameover show gameover div briefly then auto reset (hide, reset). Fine.

Pause: draw "PAUSED" center with dim.

Now burger drawing details:

```js
function drawBurger(p){
  const w=p.w, h=p.h, cx=p.cx, y=p.y;
  ctx.save();
  ctx.translate(cx,y+h/2);
  ctx.rotate(p.tilt);
  ctx.translate(-cx,-(y+h/2)); // rotate around center
  const x0=cx-w/2, x1=cx+w/2;
  // layers as fractions of h: total h; bun top 0..0.36h, lettuce .36-.46, tomato .46-.56, cheese .56-.68, patty .68-.86, bottom .86-1
  ...
}
```
Let me set explicit layer heights relative: let u = h/46 (unit). Layers (in units of 46): top bun 0-15, lettuce 15-20, tomato 20-25, cheese 25-30 (+drips to 33), patty 30-39, bottom bun 39-46. So h = 46u, w = 140u → u = w/140. h = 46/140*w ≈ 0.3286w. paddle.h = w*0.33.

Draw:
```js
const u=w/140, y0=p.y;
// bottom bun
rr(x0+2u, y0+39u, w-4u, 7u, 3u); fill '#e59a4f'; plus darker bottom line.
// patty
rr(x0+1u, y0+30u, w-2u, 9u, 4u); fill '#5d3a1a'; speckles: few darker dots alpha .3
// face on patty: eyes at y0+34u: white circles r 2.6u at cx±11u, pupils r1.3u dark offset by tilt? static. smile: stroke arc cx, y0+33.5u? draw arc radius 5u from 0.15π..0.85π at center (cx, y0+32u) stroke '#2a1508' lw 1.6u. Hmm eyes and smile both on patty (30-39u): eyes at y 34u, smile centered y 34u radius 5u would overlap eyes? smile from angle 0.2π to 0.8π centered at (cx, y0+33u) r 4.5u → bottom at 33+4.5*sin? arc points y = 33u + r*sin(a) for a in [0.2π,0.8π] → max y at π/2: 33+4.5=37.5u within patty. Eyes at y 33.5u x ±10u. OK.
// cheese
fill '#ffc93c': rr(x0+2u, y0+25u, w-4u, 5u, 2u) plus drips: triangles at x0+18u, cx+2u, x1-20u: moveTo(ax, y0+30u) lineTo(ax+5u, y0+30u) lineTo(ax+2.5u, y0+34u) close — draw as part of path or separate fills.
// tomato
rr(x0+4u, y0+20u, w-8u, 5u, 2.5u) fill '#e8483f'; inner lighter line.
// lettuce: wavy: path across width at y0+15u..20u with bumps: 
ctx.beginPath(); ctx.moveTo(x0-3u, y0+20u);
for(let x=x0-3u; x<x1+3u; x+=10u){ ctx.quadraticCurveTo(x+5u, y0+13u, x+10u, y0+20u); }
ctx.lineTo(x1+3u, y0+21u); ... simpler: fill band: after wave top, line to (x1+3u, y0+21u), (x0-3u, y0+21u) close. fill '#7ed957'; darker edge? fine.
// top bun: dome
path: moveTo(x0, y0+15u); lineTo(x0, y0+9u); quadraticCurveTo(x0, y0, cx, y0); quadraticCurveTo(x1, y0, x1, y0+9u); lineTo(x1, y0+15u); close; fill gradient vertical '#f6c07a'→'#e09a4a'; 
// shine: ellipse alpha .25 white at (cx-w*0.18, y0+5u) rx 12u ry 3.5u rotated -0.3
// sesame: seeds positions precomputed rel: [[-30,-? ]] use array of [dx,dy,rot] in units: (-28,7),( -12,4),( 6,3),( 24,6),( -38? out of dome?) dome at y0..15u; seeds at y 4..10u, x within ±34u but must be under dome curve: at dy=8u, half-width? dome approx: at y, x extent... just place seeds at (dx,dy): (-24,8),(-8,5),(8,6),(26,8),(-34? no),(-14,10),(14,10),(0,9) with dy such that inside: dome edge at height dy (0..15): half width ≈ w/2 * sqrt(1-((15-dy)/15)^2)? For quadratic dome roughly ellipse-ish. seeds at dy 5: |dx|<~40u ok since w/2=70u. dy 10: half ≈ 70*sqrt(1-(5/15)^2)=70*0.943=66. fine. All within. Seed: ellipse rx 2.2u ry 1.2u rotated, fill '#fff3d6'.
```
rounded rect helper rr(x,y,w,h,r).

Ball heart: drawHeart at ball pos s=0.5 (heart 20*0.5=10px wide... want radius 9 → s≈0.9 gives 18px wide). Heart shape wider than tall; r=9 → s=0.5 gives 10px wide, ~9.5 tall. Slightly small; s=0.55. Add glow: shadowBlur? shadow is expensive but one ball fine: ctx.shadowColor pink, shadowBlur 12. Also white shine dot.

Actually make ball bigger: r=10, s=0.6.

Collision circle-rect:
```js
function collideBrick(b, br){
  const cx = Math.max(br.x, Math.min(b.x, br.x+br.w));
  const cy = Math.max(br.y, Math.min(b.y, br.y+br.h));
  const dx=b.x-cx, dy=b.y-cy;
  if(dx*dx+dy*dy <= b.r*b.r){
    // reflect
    if(Math.abs(dx)>Math.abs(dy)) b.vx = dx>0? Math.abs(b.vx): -Math.abs(b.vx);
    else b.vy = dy>0? Math.abs(b.vy): -Math.abs(b.vy);
    // push out
    ...
    return true;
  }
  return false;
}
```
Corner case both zero (center inside) — then |dx|>|dy| false → reflect vy; acceptable; push ball out along min penetration. Add push-out: if dx===0&&dy===0 set b.y = br.y - b.r, vy=-|vy|. Fine.

Damage brick: hp--; if hp<=0 → alive=false, burst, score, combo++, popup; else crack sound lower, small burst (8 hearts).

Check all cleared: bricks.every(b=>!b.alive) → levelClear().

levelClear(): state.clearTimer=1.4; ball.stuck=true; big heart rain: spawn 100 hearts from top; popup "LEVEL "+(level+1)+"!" center; sfx arpeggio; then in update when timer<=0: level++, buildLevel(), serve.

Wait: level++ then build. popup shows "LEVEL N".

Serve: ball.stuck=true; ball.x=paddle.cx; ball.y=paddle.y - ball.r - 2.

launch(): angle = (Math.random()*0.7-0.35); speed = baseSpeed; vx=speed*Math.sin(angle); vy=-speed*Math.cos(angle); stuck=false.

baseSpeed = Math.min(380 + (level-1)*45, 640). maxSpeed cap 950 on paddle hits.

Paddle hit:
```js
if(b.vy>0 && b.y+b.r>=p.y && b.y-b.r<=p.y+p.h && Math.abs(b.x-p.cx)<=p.w/2+b.r){
  const off = Math.max(-1,Math.min(1,(b.x-p.cx)/(p.w/2)));
  const ang = off*1.1; // rad from vertical, max ~63°
  const sp = Math.min(Math.hypot(b.vx,b.vy)+12, 950);
  b.vx = sp*Math.sin(ang); b.vy = -sp*Math.cos(ang);
  b.y = p.y - b.r - 0.5;
  combo=0; sfx paddle; small heart puff (6 hearts at contact);
}
```

Walls: x<r → x=r, vx=|vx|; x>W-r → reflect; y< HUD? top wall at y=8? Use y< r+? HUD occupies top; bounce at y=64? Let's bounce at y= 58 (below HUD) — or just top of canvas y=r but HUD overlaps visually; set topWall = 64 on desktop, scale: topWall = Math.max(56, H*0.08). Hmm bricks start at top= topWall+26. Let's topWall=70, bricksTop=100.

Actually make bricks top = 110, topWall = 78.

Bottom: y - r > H → loseLife().

loseLife(): lives--; combo=0; burst at (ball.x, H-40) 50 hearts; sfx lose; if lives<=0 → gameOver() else serve(). shake=8.

gameOver(): state.over=true; show gameover div with final score; if mode==='demo' → setTimeout(()=>{ if(state.over){ resetGame(); hide div; } }, 2600). resetGame(): score=0;lives=3;level=1;buildLevel();serve(); state.over=false.

In play mode gameover: click/keydown → if over → resetGame + hide div (and keep mode play).

AI details:
```js
function aiTarget(){
  if(ball.vy<0) return W/2 + Math.sin(performance.now()/900)*W*0.08; // drift
  const t=(paddle.y - ball.y)/ball.vy;
  let x=ball.x + ball.vx*t;
  // fold into [0,W]
  const period=2*W; x=((x%period)+period)%period; if(x>W) x=period-x;
  return x + aiErr;
}
```
aiErr recomputed every 0.25s: (Math.random()*2-1)*Math.max(6, 34 - state.level*4).

Control in update:
```js
const nowS = performance.now()/1000;
let target=null;
if(keys.left) {paddle.cx -= pSpeed*dt; target=null;}
else if(keys.right){paddle.cx += pSpeed*dt;}
else if(mode==='demo' || nowS-lastInput>3.5){ target=aiTarget(); moveToward(target, aiSpeed*dt); }
else if(nowS-lastMove<3.5 && mouseX!=null){ paddle.cx += (mouseX-paddle.cx)*Math.min(1,dt*18); }
clamp; vx = (cx-prevCx)/dt for tilt (smooth: p.vx = lerp).
```
pSpeed = 750.

moveToward(target, maxStep): d=target-cx; step=clamp(d,-maxStep,maxStep); cx+=step.

Tilt: p.tilt = clamp(p.vx*0.0004, -0.14, 0.14), smooth via lerp.

lastInput updated on any key/pointer.

Demo mode: also auto-launch when stuck after 0.8s: in update if ball.stuck: if mode==='demo' → stuckTimer+=dt; if >0.9 launch. In play: wait for input (click/space/arrow?) — click anywhere launches (start handler launches). Also any keydown launches. Good: keydown handler: if ball.stuck launchBall().

Also pointermove shouldn't launch; only click/keys.

Particles also emitted passively from ball? Trail covers it.

Background hearts: bgHearts init on resize? init once with random pos fractions; update: y -= vy*dt; wrap to bottom. Draw heart alpha 0.05-0.1, s 0.3-0.9, color from pink palette, slow rotation.

Render order: bg gradient → bgHearts → (shake) bricks, particles, popups, paddle, ball+trail → HUD (no shake).

Gradient: create each frame? Cache on resize: bgGrad = ctx.createLinearGradient(0,0,0,H) with stops '#241040','#3b1663' 45%, '#1a0b2e'. Also a radial pink glow top? Add radial at (W/2, -100) rgba(255,80,140,0.15). Create on resize.

Vignette? skip.

HUD drawing:
```js
ctx.textBaseline='alphabetic';
const fs=Math.max(13, Math.min(18, W*0.02));
ctx.font=`700 ${fs}px ui-rounded, system-ui, sans-serif`;
// score
ctx.textAlign='left'; ctx.fillStyle='rgba(255,255,255,0.55)'; ctx.fillText('SCORE', 18, 30);
ctx.fillStyle='#fff'; ctx.font=`800 ${fs+4}px ...`; ctx.fillText(String(score).padStart(6,'0'), 18, 50);
// level center
ctx.textAlign='center'; fillText('LEVEL '+level, W/2, 30)? plus title? Put title small above? Title: at W/2, y=30: '🍔 HEARTBREAKER' alpha .8; level below? Space limited; do: center top: 'LEVEL n' at y=30 fs, and tiny title at y=50 alpha .5 fs-3: '🍔 burger breakout 💔'. 
// lives right: textAlign right: '♥' hearts drawn at W-18 - i*26, y=34, s=0.55, filled if i<lives else alpha .18 gray.
```
Draw hearts with drawHeartIcon(x,y,s,filled).

Combo: if combo>=2: center under level: 'COMBO ×'+combo pink, pulsing scale.

Paused: dim rect + 'PAUSED — press P'.

Game over handled by DOM div.

DOM overlay content:
```html
<div id="overlay">
  <div class="card">
    <h1>🍔 HEARTBREAKER</h1>
    <p class="tag">The paddle is a hamburger. Every brick bursts into a shower of hearts.</p>
    <p class="start" id="startMsg">▶ Click or press any key to start</p>
    <p class="hint">move with mouse or ← → · space to launch · P to pause</p>
  </div>
</div>
<div id="over" class="hidden">
  <div class="card">
    <h1>💔 GAME OVER</h1>
    <p id="finalScore"></p>
    <p class="start">Click or press any key to play again</p>
  </div>
</div>
```
CSS card: background rgba(20,10,40,.55), backdrop-filter blur(6px), border 1px rgba(255,140,170,.35), border-radius 20px, padding 28px 36px, text-align center, box-shadow. h1 gradient text: background linear-gradient(#ffd6e0,#ff5d8f); -webkit-background-clip:text; color transparent; font-size clamp(28px, 6vw, 54px). .start pulse animation opacity/scale. overlay backdrop: rgba(10,4,24,0.35).

Note: emoji in title fine.

Ensure first paint shows action: demo mode starts immediately, ball auto-launches after ~0.9s, bricks breaking with hearts within first seconds. Bricks at top, ball goes up quickly. Good. Screenshot at 3s: overlay visible + gameplay behind + particles. 

Mobile 375px: canvas full width; overlay card fits (clamp font, padding smaller via media query). cols=8, paddle w=clamp → 110 min might be wide on 375: W*0.16=60 → clamp min 100? set w = clamp(W*0.3, 96, 170) for small screens? Let's w = Math.max(96, Math.min(170, W*0.17)); at 375 → 96? W*0.17=64 → max(96,64)=96. OK 96px paddle on 375 fine.

Brick height: bh = clamp(H*0.035, 20, 28).

Now, substeps for ball movement:
```js
function moveBall(dt){
  const dist = Math.hypot(ball.vx,ball.vy)*dt;
  const steps = Math.max(1, Math.ceil(dist/6));
  const sdt = dt/steps;
  for(let i=0;i<steps;i++){
    ball.x += ball.vx*sdt; ball.y += ball.vy*sdt;
    // walls
    if(ball.x < ball.r){ball.x=ball.r; ball.vx=Math.abs(ball.vx); sfxWall();}
    else if(ball.x > W-ball.r){ball.x=W-ball.r; ball.vx=-Math.abs(ball.vx); sfxWall();}
    if(ball.y < topWall+ball.r){ball.y=topWall+ball.r; ball.vy=Math.abs(ball.vy); sfxWall();}
    // paddle
    ...
    // bricks: loop bricks, if alive && collide → damage, break (only one brick per substep to avoid multi) → break out
  }
  if(ball.y - ball.r > H){ loseLife(); }
}
```
sfx on wall every substep could spam; only if not already played this frame — minor, sounds overlap fine but limit: play only when reflection actually changes velocity and track a flag per frame. Simple: wallSfx flag reset each frame.

Brick loop: iterate all bricks (≤80) fine.

Combo logic: combo increments on brick destroyed; resets on paddle hit. Score per brick = 10*level? Let's: points = (gold?50:10) * (hpMax>1?2:1)? keep: 10 per hit, 20 bonus on destroy? Simple: destroy → 10 + 5*combo; gold → 50 + 10*combo. Popup "+pts".

Shake on destroy: shake = Math.min(6, 2+combo*0.5).

render bricks with hp cracks: if maxHp>1 && hp<maxHp → draw 2 crack lines dark alpha .5.

Gold brick draw: gradient gold, draw small heart icon center? Draw white heart s=0.3 center alpha .9. Cute — indicates bonus.

Popups update: y -= 40*dt; life-=dt; alpha=life/max.

Now the code. Also add `visibilitychange` → paused=true when hidden (in play mode) to avoid dt jump (dt clamped anyway). Skip.

Prevent arrow/space scroll: e.preventDefault() in keydown for those keys.

Also prevent context menu? not needed.

One more: "Click or press a key to start" must be clear — done in overlay.

Let me also add slight paddle squash on hit? skip.

Write drawHeart:
```js
function heartPath(c,s){
  c.beginPath();
  c.moveTo(0,10*s);
  c.bezierCurveTo(-6*s,4*s,-10*s,0,-10*s,-4*s);
  c.bezierCurveTo(-10*s,-7.5*s,-7.5*s,-9.5*s,-5*s,-9.5*s);
  c.bezierCurveTo(-3*s,-9.5*s,-1*s,-8*s,0,-6*s);
  c.bezierCurveTo(1*s,-8*s,3*s,-9.5*s,5*s,-9.5*s);
  c.bezierCurveTo(7.5*s,-9.5*s,10*s,-7.5*s,10*s,-4*s);
  c.bezierCurveTo(10*s,0,6*s,4*s,0,10*s);
  c.closePath();
}
function drawHeart(c,x,y,s,rot,color,alpha){
  c.save(); c.translate(x,y); c.rotate(rot); c.globalAlpha=alpha; c.fillStyle=color; heartPath(c,s); c.fill(); c.restore();
}
```

Particles draw: heartPath directly with transforms.

Ball: shadowBlur glow: 
```js
ctx.save(); ctx.shadowColor='#ff4d8d'; ctx.shadowBlur=16; ctx.fillStyle='#ff2e63'; heartPath(ctx,0.62); translate first... 
```
Implement: save, translate(ball.x,ball.y), rotate slight wobble, shadow, fill path, restore; then shine: small white ellipse at (-3,-3) alpha .7 within rotated frame. Then no shadow for shine.

Lives icons: drawHeart at (x, y) with s=0.5, color '#ff4d8d' or 'rgba(255,255,255,0.15)'.

Trail: for i from trail.length-1..0: alpha=(1-i/trail.length)*0.4; s=0.3*(1-i/trail.length)+0.12; color '#ff8fab'.

Push trail each frame when !stuck: trail.unshift({x,y}); if length>12 pop.

Now HUD hearts positions: right aligned: start x = W-24, spacing 26: for i in 0..lives-1 (max 5? lives=3) draw at W-24-i*26, y=32 center. Draw filled for i<lives else dim. Cap display lives at 5.

Score popup etc fine.

Edge: bricks layout when cols changes on resize — bricks store col,row so layoutBricks recomputes; alive preserved. buildLevel on level change uses current W.

layoutBricks():
```js
function layoutBricks(){
  const cols = state.cols, rows = state.rows;
  const side = Math.max(20, W*0.045);
  const gap = Math.max(4, W*0.006);
  const bw = (W - side*2 - gap*(cols-1))/cols;
  const bh = Math.max(20, Math.min(28, H*0.032));
  const top = topWall + 34;
  for(const b of bricks){ b.x = side + b.col*(bw+gap); b.y = top + b.row*(bh+gap); b.w=bw; b.h=bh; }
}
```
topWall = Math.max(64, H*0.085).

buildLevel():
```js
function buildLevel(){
  state.cols = W<520?8:10;
  state.rows = Math.min(3+state.level, 7);
  bricks.length=0;
  const palette=['#ff4d6d','#ff7a59','#ffb84d','#ffe14d','#8ee06a','#4dd6c1','#6aa8ff','#b48cff'];
  for(r...){ for(c...){ const gold = r===0 && Math.random()<0.16; bricks.push({col:c,row:r,alive:true, gold, hp: gold?1:(state.level>=2&&r<2?2:1), maxHp:..., color: gold?'#ffd166':palette[r%palette.length]}); } }
  layoutBricks();
}
```
maxHp set same as hp.

Level clear check in update after brick loop: if(bricks.length && bricks.every(b=>!b.alive) && !state.clearing){ state.clearing=true; state.clearTimer=1.5; ... }

In update: if state.clearing { clearTimer-=dt; particles update; if clearTimer<=0 { state.level++; state.clearing=false; buildLevel(); serve(); popup center 'LEVEL '+state.level; } return-ish (skip ball movement? ball stuck anyway). }

Heart rain on clear: for i<90: spawn at random x, y=-20-rand*200, vy 60+rand*160, vx rand±60, big s, pink colors, life long.

Popup big center: popups.push({x:W/2,y:H/2,text:'LEVEL '+state.level, size:34, life:1.4,...}) — handle size in popup draw.

Popup struct: {x,y,text,life,max,color,size}. draw font bold size px, alpha life/max, align center.

OK, also initial state: buildLevel(); serve(); mode='demo'.

serve(): ball.stuck=true; ball.speed=baseSpeed(); position at paddle. ball.x=paddle.cx etc in update when stuck.

baseSpeed(): Math.min(380+(state.level-1)*40, 620).

Launch sets vx,vy from speed.

Paddle hit speed increase: sp=Math.min(hypot+14, 940).

Also ensure ball doesn't go too horizontal: after paddle hit clamp ang ±1.05 rad. Also if |vy| < sp*0.25 after wall bounces? Could get near-horizontal infinite; add: if Math.abs(ball.vy) < 60 → vy = ±60 adjust sign (keep vx). Do after wall/brick collisions: normalize: if |vy|<70, vy = 70*sign(vy||-1)... but vy could be 0 after pure horizontal brick hit? vy reflect keeps sign. Add safeguard in moveBall end: if(Math.abs(ball.vy)<80){ ball.vy = (ball.vy>=0?1:-1)*80; renormalize speed? just set vy and scale vx to keep speed: const sp=hypot; ball.vx = Math.sign(ball.vx||1)*Math.sqrt(Math.max(sp*sp-6400,0)); } fine.

Audio init on first gesture only; demo before gesture silent — good (autoplay policy).

Keydown handler:
```js
window.addEventListener('keydown', e=>{
  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(e.key)) e.preventDefault();
  startFromGesture();
  if(e.key==='ArrowLeft') keys.left=true;
  if(e.key==='ArrowRight') keys.right=true;
  if(e.key==='p'||e.key==='P'){ if(!state.over) state.paused=!state.paused; }
  if(e.key===' '){ if(ball.stuck) launch(); }
  lastInput=performance.now()/1000;
});
keyup clears keys.
pointerdown: startFromGesture(); mouseX=e.clientX; lastMove=now; if(ball.stuck) launch();
pointermove: mouseX=e.clientX; lastMove=now; lastInput=now;
function startFromGesture(){ initAudio(); if(state.over){ resetGame(); hideOver(); } if(state.mode==='demo'){ state.mode='play'; overlay.classList.add('hidden'); } lastInput=now; if(ball.stuck) launch(); }
```
Wait: clicking to start also launches ball immediately — good.

But pointerdown also fires when clicking game over overlay → resets. Good.

resetGame(): score=0;lives=3;level=1;combo=0; state.over=false; state.clearing=false; buildLevel(); serve(); particles.length=0? keep some. popups clear.

gameOver(): state.over=true; overDiv.classList.remove('hidden'); finalScore.textContent='Score '+score+' · Level '+level; if(state.mode==='demo'){ setTimeout(()=>{ if(state.over){ resetGame(); overDiv.classList.add('hidden'); } }, 2800); }

Pause only meaningful in play; in demo ignore pause? If user presses P in demo before starting... startFromGesture runs first (mode→play) then toggles pause. Order: call startFromGesture then handle p. Fine.

Render paused overlay: if(state.paused && !state.over){ dim + text }.

Now write render():

```js
function render(){
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.fillStyle=bgGrad; ctx.fillRect(0,0,W,H);
  // glow
  ctx.fillStyle=glowGrad... fill rect? radial gradient circle: create radialGradient each resize at (W*0.5, -80, 0, W*0.5,-80, H*0.9) rgba(255,90,150,.18)→0; fillRect.
  // bg hearts
  for(const h of bgHearts){ drawHeart(ctx,h.x,h.y,h.s,h.rot,h.color,h.alpha); }
  ctx.save();
  if(shake>0.3){ ctx.translate((Math.random()*2-1)*shake,(Math.random()*2-1)*shake); }
  // bricks
  for(const b of bricks) if(b.alive) drawBrick(b);
  // particles
  for(const p of particles){ drawHeart(ctx,p.x,p.y,p.s,p.rot,p.color,p.alpha); }
  // popups
  ctx.textAlign='center';
  for(const p of popups){ const a=p.life/p.max; ctx.globalAlpha=a; ctx.fillStyle=p.color; ctx.font=`800 ${p.size}px system-ui,sans-serif`; ctx.fillText(p.text,p.x,p.y); ctx.globalAlpha=1; }
  // trail
  ...
  // paddle burger
  drawBurger();
  // ball
  if(!state.over) drawBall();
  ctx.restore();
  drawHUD();
  if(state.paused&&!state.over){ ... }
}
```

drawBrick(b):
```js
const r=6;
rr(ctx,b.x,b.y,b.w,b.h,r); ctx.fillStyle=b.color; ctx.fill();
// top highlight
ctx.save(); rr clip? simpler: ctx.fillStyle='rgba(255,255,255,0.28)'; rr(ctx,b.x+2,b.y+2,b.w-4,b.h*0.42,r*0.6); ctx.fill();
// bottom shade
ctx.fillStyle='rgba(0,0,0,0.22)'; rr(ctx,b.x+2,b.y+b.h*0.62,b.w-4,b.h*0.34,r*0.6); ctx.fill();
if(b.gold){ drawHeart small center white alpha .9 s .32 } 
if(b.maxHp>1&&b.hp<b.maxHp){ cracks: strokeStyle rgba(0,0,0,.45) lw1.5: lines }
ctx.restore? no save needed if no clip.
```
Round rect path helper:
```js
function rr(c,x,y,w,h,r){ r=Math.min(r,w/2,h/2); c.beginPath(); c.moveTo(x+r,y); c.arcTo(x+w,y,x+w,y+h,r); c.arcTo(x+w,y+h,x,y+h,r); c.arcTo(x,y+h,x,y,r); c.arcTo(x,y,x+w,y,r); c.closePath(); }
```

drawBurger(p): as planned. Include drop shadow: ctx.save(); shadowColor rgba(0,0,0,.35) blur 8 offsetY 4 when drawing? Apply shadow to whole burger group: set before drawing layers, reset after. Shadow on path fills works per shape; fine.

Face: eyes white with pupils; smile stroke. Also rosy cheeks? skip.

Sesame seeds: fixed array:
```js
const seeds=[[-30,7,-0.4],[-14,4.5,0.2],[2,3.5,-0.15],[18,5,0.35],[32,8,0.1],[-24? ...]];
```
in units (u): [[-32,8,-0.5],[-16,5,0.3],[-2,4,-0.2],[14,5.5,0.25],[30,8,-0.3],[-24? maybe 6 seeds enough: add [ -8? ]]. Use 7: (-33,8),(-19,5),( -4,4),(12,5),(27,7.5),( -11? ) fine: [[-33,8,-.5],[-19,5,.3],[-4,4,-.2],[12,5,.25],[28,8,-.3],[0? overlap] ok 5-6 seeds.

Draw seed: save translate(x0+70u+dx*u, y0+dy*u) rotate(rot) ellipse(0,0,2.4u,1.3u) fill '#fff3d6' alpha .95.

Wait dome top at y0, center cx; seeds y around 4-8u below top. Dome height 15u, half-width 70u at base. At dy=8u, dome half-width ≈ 70*sqrt(1-((15-8)/15)^2)=70*sqrt(1-0.218)=70*0.884=62u → dx ±33 fine.

Lettuce wave: from x0-4u to x1+4u step 12u bumps up to y0+12.5u, base y0+20u:
```js
ctx.beginPath();
ctx.moveTo(x0-4u, y0+20u);
const step=12u;
for(let x=x0-4u; x<x1+4u; x+=step){ ctx.quadraticCurveTo(x+step/2, y0+12.5u, Math.min(x+step,x1+4u), y0+20u); }
ctx.lineTo(x1+4u, y0+21.5u); ctx.lineTo(x0-4u, y0+21.5u); ctx.closePath();
ctx.fillStyle='#7ed957'; ctx.fill();
```
Hmm wave top should be above tomato top (20u) — lettuce spans 15-21u, tomato 20-25: lettuce drawn after tomato? Order top→bottom drawing doesn't matter much with overlap; draw bottom-up: bottom bun, patty, cheese, tomato, lettuce, top bun — later drawn on top. Lettuce wave bottom at 21.5 overlaps tomato top 20 — lettuce on top looks right (lettuce above tomato visually). But we draw lettuce after tomato → covers tomato top edge. Good.

Actually order: draw top bun LAST so it overlaps lettuce top? Lettuce at 15-21, bun bottom at 15 — bun drawn last covers lettuce top bumps partially — natural. Draw order: bottom bun → patty → cheese → tomato → lettuce → top bun. Face after patty (before cheese? face on patty y30-39u; cheese at 25-30 with drips to 34u could cover smile at 37.5? drips at few x positions; smile center cx — place a drip not at center. Or draw face last after everything (on top) — but face should be on patty layer; drawing last puts it over cheese drips if overlap — drips at ±? place drips at x0+16u, x1-16u, and cx-30u? avoid center. Draw face after all layers for clarity. Eyes at (cx±10u, y0+34u), smile arc center (cx, y0+32.5u) r 4.5u from 0.25π to 0.75π. Eyes r 2.6u white, pupil r 1.2u '#3a2008' offset (0.6u,0.4u). Smile stroke '#3a2008' lw 1.7u, lineCap round.

Cheese drips: at positions [x0+14u, cx-6u? no avoid smile center... smile spans cx±~3.9u at y up to 37u; drips end y 34u; overlap possible at cx±? Put drips at x0+14u, x1-14u, cx+24u, cx-24u. 4 drips.

Cheese base rect: x0+3u..x1-3u, y 25u..30u, r 1.5u, fill '#ffc93c'. Drips triangles from y30u down to 33.5u width 6u.

Tomato: x0+5u..x1-5u, y 20u..25u r 2.5u fill '#e8483f'; highlight: inner rect lighter '#ff6b5e' at y 21u h 1.6u? skip, add small shine.

Patty: x0+1.5u..x1-1.5u, y 30u..39u, r 4u, fill '#6b3a1c'; speckles: 5 dots rgba(0,0,0,.25) r 0.9u at fixed rel positions.

Bottom bun: x0+2u..x1-2u, y 39u..46u, r 3u, fill '#e8a558'; bottom darker: rect y 44u..46u rgba(0,0,0,.15).

Top bun: path dome as described, gradient: create linear gradient (0,y0,0,y0+15u): '#f7c983' → '#e59a4a'. Shine ellipse.

Burger shadow: before layers: ctx.shadowColor='rgba(0,0,0,0.35)'; ctx.shadowBlur=10; ctx.shadowOffsetY=5; after all: ctx.shadowColor='transparent'; shadowBlur=0; shadowOffsetY=0.

Paddle y: p.y = H-72. h = w*0.33 → at w=140, h=46. p.y is top of burger. Ball rests at p.y - r - 1.

drawBall:
```js
if(ball.stuck){ ball.x=paddle.cx + Math.sin(t*2)*? keep centered; ball.y=paddle.y-ball.r-2; }
save; translate(ball.x,ball.y); rotate(Math.sin(time*3)*0.15);
ctx.shadowColor='#ff4d8d'; ctx.shadowBlur=18; ctx.fillStyle='#ff2e63'; heartPath(ctx,0.62); ctx.fill();
ctx.shadowBlur=0; ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.beginPath(); ctx.ellipse(-2.5,-3.5,1.8,1.1,-0.6,0,7); ctx.fill();
restore;
```

Trail drawn before paddle.

HUD:
```js
function drawHUD(){
  const fs=Math.max(13,Math.min(17,W*0.018));
  ctx.textBaseline='middle';
  ctx.textAlign='left';
  ctx.fillStyle='rgba(255,200,220,0.6)'; ctx.font=`700 ${fs}px system-ui,sans-serif`;
  ctx.fillText('SCORE',18,26);
  ctx.fillStyle='#fff'; ctx.font=`800 ${fs+5}px system-ui,sans-serif`;
  ctx.fillText(String(state.score).padStart(6,'0'),18,46);
  ctx.textAlign='center';
  ctx.fillStyle='rgba(255,200,220,0.85)'; ctx.font=`800 ${fs+2}px system-ui,sans-serif`;
  ctx.fillText('🍔 HEARTBREAKER', W/2, 24);
  ctx.fillStyle='rgba(255,255,255,0.75)'; ctx.font=`700 ${fs}px system-ui,sans-serif`;
  ctx.fillText('LEVEL '+state.level, W/2, 44);
  if(state.combo>=2){ ctx.fillStyle='#ff8fab'; ctx.font=`800 ${fs+3}px ...`; ctx.fillText('COMBO ×'+state.combo, W/2, 64); }
  // lives
  const n=Math.min(state.lives,5);
  for(let i=0;i<5;i++){ const x=W-26-i*28; drawHeart(ctx,x,30,0.55,0, i<state.lives?'#ff4d8d':'rgba(255,255,255,0.14)',1); }
  if(state.lives>5) text? skip.
}
```
Emoji in canvas text renders fine.

topWall: bricks top = topWall+34; topWall = 76. Let topWall=78, bricks top=112.

On small screens H maybe 600+: fine.

Update function full:

```js
let last=performance.now();
function frame(t){
  requestAnimationFrame(frame);
  let dt=(t-last)/1000; last=t;
  if(dt>0.05) dt=0.05;
  if(!state.paused && !state.over) update(dt);
  else { updateAmbient(dt); } // still animate particles/bg
  render();
}
```
Simplify: always update particles/bg/popups; game logic only when !paused && !over && !clearing(handled separately).

update(dt):
```js
time+=dt;
// bg hearts
for(const h of bgHearts){ h.y-=h.vy*dt; h.rot+=h.vr*dt; if(h.y<-30){h.y=H+30; h.x=Math.random()*W;} }
// particles
for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.life-=dt; if(p.life<=0){particles.splice(i,1);continue;} p.vy+=560*dt; const dr=Math.max(0,1-1.4*dt); p.vx*=dr; p.vy*=Math.max(0,1-0.5*dt); p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt; p.alpha=Math.min(1,p.life/0.6); }
// popups
for(let i=popups.length-1;i>=0;i--){const p=popups[i]; p.life-=dt; p.y-=46*dt; if(p.life<=0)popups.splice(i,1);}
shake=Math.max(0,shake-30*dt);
// demo auto-launch
if(ball.stuck){ ball.x=paddle.cx; ball.y=paddle.y-ball.r-2; if(state.mode==='demo'){ stuckT+=dt; if(stuckT>0.8){launch();} } }
else { moveBall(dt); trail update }
// paddle control
controlPaddle(dt);
// clearing
if(state.clearing){ state.clearTimer-=dt; if(state.clearTimer<=0){ state.level++; state.clearing=false; buildLevel(); serve(); addPopup(W/2,H*0.4,'LEVEL '+state.level,'#ffd6e0',36,1.5); sfxLevel(); } }
// check clear
if(!state.clearing && bricks.length && bricks.every(b=>!b.alive)){ state.clearing=true; state.clearTimer=1.6; heartRain(); addPopup(W/2,H/2,'LEVEL CLEAR!','#ff8fab',40,1.4); sfxLevel(); }
```
Wait order: check clear after moveBall. Fine.

controlPaddle(dt):
```js
const now=performance.now()/1000;
const prev=paddle.cx;
let pSpeed=760;
if(keys.left){paddle.cx-=pSpeed*dt;}
else if(keys.right){paddle.cx+=pSpeed*dt;}
else if(state.mode==='demo' || now-lastInput>3.5){
  aiTimer-=dt; if(aiTimer<=0){ aiTimer=0.22; aiErr=(Math.random()*2-1)*Math.max(6,36-state.level*4); }
  const tgt=aiTarget();
  const aiSpeed=Math.min(560+state.level*40,860);
  const d=tgt-paddle.cx; const step=Math.max(-aiSpeed*dt,Math.min(aiSpeed*dt,d));
  paddle.cx+=step;
} else if(now-lastMove<3.5 && mouseX!=null){
  paddle.cx += (mouseX-paddle.cx)*Math.min(1,dt*20);
}
paddle.cx=Math.max(paddle.w/2+6,Math.min(W-paddle.w/2-6,paddle.cx));
paddle.vx=(paddle.cx-prev)/Math.max(dt,1e-4);
paddle.tilt += (Math.max(-0.14,Math.min(0.14,paddle.vx*0.00045)) - paddle.tilt)*Math.min(1,dt*10);
```

aiTarget:
```js
function aiTarget(){
  if(ball.stuck || ball.vy<0){ return W/2 + Math.sin(time*1.3)*W*0.06; }
  const t=(paddle.y-ball.y)/ball.vy;
  let x=ball.x+ball.vx*t;
  const per=2*W; x=((x%per)+per)%per; if(x>W)x=per-x;
  return Math.max(paddle.w/2,Math.min(W-paddle.w/2,x+aiErr));
}
```

moveBall with substeps as above; brick collision loop:
```js
for(const b of bricks){
  if(!b.alive) continue;
  // quick AABB reject
  if(ball.x+b.r<b.x||ball.x-b.r>b.x+b.w||ball.y+b.r<b.y||ball.y-b.r>b.y+b.h) continue;
  const cx=Math.max(b.x,Math.min(ball.x,b.x+b.w));
  const cy=Math.max(b.y,Math.min(ball.y,b.y+b.h));
  let dx=ball.x-cx, dy=ball.y-cy;
  if(dx*dx+dy*dy<=ball.r*ball.r){
    if(dx===0&&dy===0){ dy=-1; }
    if(Math.abs(dx)>Math.abs(dy)){ ball.vx=dx>0?Math.abs(ball.vx):-Math.abs(ball.vx); ball.x += dx>0? ball.r : -ball.r; hmm push: ball.x = cx + (dx>0? ball.r+0.1 : -(ball.r+0.1)); }
    else { ball.vy=dy>0?Math.abs(ball.vy):-Math.abs(ball.vy); ball.y = cy + (dy>0? ball.r+0.1 : -(ball.r+0.1)); }
    hitBrick(b);
    break;
  }
}
```
Wait pushing ball to cx±r might teleport inside adjacent brick; acceptable.

hitBrick(b):
```js
function hitBrick(b){
  b.hp--;
  sfxBrick();
  if(b.hp<=0){
    b.alive=false;
    state.combo++;
    const pts=(b.gold?50:10)+5*state.combo;
    state.score+=pts;
    addPopup(b.x+b.w/2,b.y,'+'+pts,'#fff',15,0.9);
    if(state.combo>=3) addPopup(b.x+b.w/2,b.y-22,'COMBO ×'+state.combo,'#ff8fab',14,0.9);
    const n=b.gold?56:Math.min(46,22+state.level*4);
    burst(b.x+b.w/2,b.y+b.h/2,n,b.color,b.gold?1.4:1);
    shake=Math.min(7,2.5+state.combo*0.4);
    if(b.gold) sfxGold();
  } else {
    burst(b.x+b.w/2,b.y+b.h/2,8,b.color,0.7);
    addPopup(b.x+b.w/2,b.y,'crack','#ffd6e0',12,0.6);
  }
}
```

burst(x,y,n,color,power):
```js
const palette=['#ff2e63','#ff5d8f','#ff8fab','#ffb3c6','#ffd6e0','#ffffff'];
for(let i=0;i<n;i++){
  if(particles.length>750) particles.shift();
  const a=Math.random()*Math.PI*2;
  const sp=(60+Math.random()*380)*power;
  particles.push({
    x:x+(Math.random()*2-1)*10, y:y+(Math.random()*2-1)*8,
    vx:Math.cos(a)*sp, vy:Math.sin(a)*sp-160,
    s:0.16+Math.random()*0.3,
    rot:Math.random()*Math.PI*2, vr:(Math.random()*2-1)*6,
    life:0.9+Math.random()*0.9, max:1.8,
    color: Math.random()<0.55? palette[(Math.random()*palette.length)|0] : color,
    alpha:1
  });
}
```
max set to life initial: set max=p.life.

heartRain():
```js
for(let i=0;i<110;i++){ particles.push({x:Math.random()*W, y:-30-Math.random()*H*0.5, vx:(Math.random()*2-1)*70, vy:80+Math.random()*220, s:0.2+Math.random()*0.35, rot:Math.random()*6.28, vr:(Math.random()*2-1)*4, life:2.5+Math.random()*2, max:4, color:palette random, alpha:1}); }
```

sfx functions with guards.

launch():
```js
function launch(){ if(!ball.stuck)return; ball.stuck=false; stuckT=0; const sp=ballSpeed(); const a=(Math.random()*0.6-0.3); ball.vx=sp*Math.sin(a); ball.vy=-sp*Math.cos(a); sfx(300,0.08,'triangle',0.1,150); }
ballSpeed=()=>Math.min(380+(state.level-1)*40,620);
```

loseLife():
```js
function loseLife(){ state.lives--; state.combo=0; burst(ball.x,Math.min(ball.y,H-30),46,'#ff5d8f',1.2); shake=9; sfxLose(); if(state.lives<=0){ gameOver(); } else { serve(); addPopup(W/2,H/2-40,'💔','',...) } }
```
Popup with emoji text works. Or text 'OUCH!'. Use '💔'.

serve(): ball.stuck=true; stuckT=0; ball.speed=ballSpeed();

gameOver(): state.over=true; overEl.classList.remove('hidden'); finalEl.textContent=`SCORE ${state.score} · LEVEL ${state.level}`; if demo → setTimeout auto reset 3000ms guarded by token.

resetGame(): state.score=0;state.lives=3;state.level=1;state.combo=0;state.over=false;state.clearing=false;state.paused=false; popups.length=0; buildLevel(); serve();

Init:
```js
resize(); buildLevel(); serve(); requestAnimationFrame(frame);
```
resize calls layoutBricks which needs bricks — guard if bricks empty. Also paddle init in resize before buildLevel? paddle object defined with w computed in resize.

bgHearts init: 34 items with x=Math.random()*W etc — but W changes; store fx (fraction): h={fx, y, vy, s, rot, vr, alpha, color}; render x=h.fx*W. Init once with fx random.

Also add subtle grid? no.

Overlay HTML/CSS with pulse animation:
```css
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.97)}}
```

hidden class: `#overlay.hidden, #over.hidden{opacity:0;pointer-events:none}` with transition .45s.

Media query max-width 480px: .card padding 18px 20px; h1 font clamp handles; .hint display none maybe.

Also `touch-action:none` on canvas to prevent scroll while dragging.

Prevent double-firing pointerdown on overlay buttons — overlay covers screen, pointerdown on window still fires (bubbles). Good.

iOS audio resume: initAudio on gesture; fine.

Check zero errors: careful with undefined vars. Let me now write the full file mentally consistent.

Variables: canvas, ctx, W,H,dpr, bgGrad, glowGrad, paddle, ball, bricks, particles, popups, bgHearts, keys={left:false,right:false}, mouseX=null, lastMove=-10, lastInput=-10, aiErr=0, aiTimer=0, stuckT=0, time=0, shake=0, state, overlay, overEl, finalEl, AC.

topWall: function or const updated in resize: topWall=78; make it variable set in resize: topWall=Math.max(70,Math.min(90,H*0.1)).

Paddle: {cx:W/2, y:0, w:140, h:46, vx:0, tilt:0}; in resize: paddle.w=Math.max(96,Math.min(170,W*0.17)); paddle.h=paddle.w*0.33; paddle.y=H-72-paddle.h? Wait p.y is top of burger; define p.y as TOP. paddle.y=H-70; ball rests at paddle.y - r - 2. Burger drawn from p.y to p.y+h. Bottom at H-70+h ≈ H-24. Good.

In resize: paddle.y=H-72; clamp cx.

buildLevel uses topWall → call after resize sets topWall. Init order: resize() (sets W,H,paddle,topWall, layoutBricks guarded) then buildLevel().

layoutBricks guard: if(!bricks.length) return? It iterates bricks; fine even empty. But needs state.cols — guard: if(!state.cols) return.

Resize during play: ball position clamp: ball.x=Math.min(W-ball.r,Math.max(ball.r,ball.x)); same y.

Now sfx:
```js
function tone(f,d,type,v,slide){ if(!AC)return; try{ const t=AC.currentTime; const o=AC.createOscillator(),g=AC.createGain(); o.type=type||'triangle'; o.frequency.setValueAtTime(f,t); if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+d); g.gain.setValueAtTime(v||0.12,t); g.gain.exponentialRampToValueAtTime(0.0001,t+d); o.connect(g); g.connect(AC.destination); o.start(t); o.stop(t+d+0.05);}catch(e){} }
const sfxWall=()=>tone(170,0.05,'square',0.05);
const sfxPaddle=()=>tone(240,0.09,'triangle',0.14,160);
const sfxBrick=()=>tone(300+Math.min(state.combo,10)*35,0.1,'triangle',0.12,240);
const sfxGold=()=>{tone(660,0.09,'square',0.1,0); setTimeout(()=>tone(880,0.12,'square',0.1,0),70); setTimeout(()=>tone(1320,0.16,'square',0.09,0),140);};
const sfxLose=()=>tone(320,0.45,'sawtooth',0.12,-270);
const sfxLevel=()=>{[523,659,784,1046].forEach((f,i)=>setTimeout(()=>tone(f,0.14,'triangle',0.11,0),i*90));};
```
setTimeout ok (not blocked).

Paddle hit also small burst: burst(ball.x, paddle.y, 6, '#ff8fab', 0.5).

Wall hit flag: let wallHitThisFrame; in moveBall set true on reflect; after moveBall, if flag sfxWall(). Simpler: play sfx directly but only when reflection occurs — could be multiple per frame rarely; acceptable but substeps with tiny dt... reflect happens once per crossing. Fine, call directly.

Ball-paddle check inside substep loop after walls.

Also brick loop `break` after one hit per substep.

Vertical safeguard after substeps:
```js
if(!ball.stuck){ const sp=Math.hypot(ball.vx,ball.vy); if(Math.abs(ball.vy)<70){ ball.vy=(ball.vy>=0?1:-1)*70; const nx=Math.sqrt(Math.max(sp*sp-70*70,1)); ball.vx=(ball.vx>=0?1:-1)*nx; } }
```

Trail: if(!ball.stuck){ trail.unshift({x:ball.x,y:ball.y}); if(trail.length>11)trail.pop(); } else trail.length=0.

Render trail before paddle:
```js
for(let i=trail.length-1;i>=0;i--){ const k=1-i/trail.length; drawHeart(ctx,trail[i].x,trail[i].y,0.14+0.2*k,0,'#ff8fab',0.35*k); }
```

drawBurger implementation with p=paddle:

```js
function drawBurger(){
  const p=paddle, w=p.w, u=w/140, x0=p.cx-w/2, x1=p.cx+w/2, y0=p.y, cx=p.cx;
  ctx.save();
  ctx.translate(cx, y0+p.h/2); ctx.rotate(p.tilt); ctx.translate(-cx, -(y0+p.h/2));
  ctx.shadowColor='rgba(0,0,0,0.35)'; ctx.shadowBlur=12; ctx.shadowOffsetY=5;
  // bottom bun
  rr(ctx,x0+2*u,y0+39*u,w-4*u,7*u,3*u); ctx.fillStyle='#e8a558'; ctx.fill();
  ctx.shadowColor='transparent'; // only first layer shadow? simpler: keep shadow off after; set at start and clear after all
  ...
}
```
Shadow per-shape: set once before all fills; each fill casts shadow — overlapping shadows darken interior. Better: draw shadow as separate ellipse under burger before layers:
```js
ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.beginPath(); ctx.ellipse(cx, y0+p.h+4, w*0.45, 5, 0,0,7); ctx.fill();
```
Then layers without shadow. Good, and it shouldn't rotate — draw before rotate. Restructure: save, draw shadow, translate/rotate, layers, restore.

Layers code as planned.

drawBrick with gold: gradient? simple fill '#ffd166' + inner lighter + white heart center s=0.3.

Cracks: 
```js
ctx.strokeStyle='rgba(60,10,25,0.5)'; ctx.lineWidth=1.5; 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.55); ctx.lineTo(b.x+b.w*0.35,b.y+b.h-3);
ctx.moveTo(b.x+b.w*0.65,b.y+4); ctx.lineTo(b.x+b.w*0.58,b.y+b.h*0.5); ctx.lineTo(b.x+b.w*0.7,b.y+b.h-4);
ctx.stroke();
```

HUD combo pulse: size*(1+0.1*sin(time*8)).

Paused render:
```js
if(state.paused && !state.over){ ctx.fillStyle='rgba(10,4,20,0.55)'; ctx.fillRect(0,0,W,H); ctx.textAlign='center'; ctx.fillStyle='#fff'; ctx.font='800 34px system-ui'; ctx.fillText('PAUSED',W/2,H/2); ctx.font='600 16px system-ui'; ctx.fillStyle='rgba(255,255,255,0.7)'; ctx.fillText('press P to resume',W/2,H/2+30); }
```

Over state: keep rendering last frame + particles (update runs ambient). In frame: if(!state.paused && !state.over) updateGame(dt); updateAmbient(dt); render(). updateAmbient handles bgHearts, particles, popups, shake. updateGame calls controlPaddle, ball, clearing, plus updateAmbient.

Structure:
```js
function updateAmbient(dt){ time+=dt; bg; particles; popups; shake decay; }
function updateGame(dt){ updateAmbient(dt); ...paddle/ball/clear... }
```

Demo overlay remains until gesture; game runs behind (mode demo → AI). 

One concern: pointerdown listener on window with overlay: overlay has pointer-events auto; click passes to window via bubbling — yes.

Also add `window.addEventListener('pointermove'...)` for mouse.

Prevent text selection: user-select none on body.

Now sizes on 1280×800: W=1280 → paddle w=170 (W*0.17=217→clamp 170). cols=10, side=57.6, gap=7.68, bw=(1280-115.2-69.1)/10≈109.5. bh=min(28, 800*0.032=25.6)→25.6. rows level1=4 → bricks occupy 112..112+4*33≈244. Good.

Ball speed 380 → reaches bricks in ~ (paddle.y≈728-10-10=708; brick bottom 244) 464px / ~350 vertical ≈1.3s. Good, breaks brick ~2-3s in. Screenshot at 3s: likely mid-action with hearts. To ensure hearts visible at 3s, maybe start ball already moving up at load in demo (launch after 0.5s). Set stuckT initial 0.5 so launch at 0.3s? stuckT>0.8 → launch at 0.8s. First brick hit ~2s, particles live ~1.8s → visible at 3s. Good. Even better: pre-warm: at init, spawn a burst somewhere? Not needed.

Maybe make demo ball launch instantly: stuckT=0.75 initially. Eh, set stuckT=0.7 at serve in demo... serve() sets stuckT=0; then in update demo adds dt; launch at 0.8. Fine.

Font stack: use `system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif`.

Write final HTML. Also meta viewport. Title.

Double-check heart path renders heart correctly: The classic path I translated — should be fine.

Potential issue: `ctx.ellipse` end angle 7 > 2π fine.

Popup emoji '💔' font ok.

Let me also handle `state.cols` used in layoutBricks before buildLevel on first resize — resize called first, bricks empty, state.cols undefined → guard `if(!state.cols||!bricks.length) return;` Actually just iterate bricks (empty) — but computing bw uses state.cols undefined → NaN, loop doesn't execute since bricks empty. Safe but compute anyway; guard anyway.

Order init: define everything, then resize(), buildLevel(), serve(), rAF.

resize also repositions bgHearts? They use fx so fine.

Edge: H small (landscape phone) — fine.

Now write it all out carefully. Also add small "demo" badge? The overlay covers; after start, maybe tiny text bottom-right "AI demo until you move" — skip.

One more requirement check: "No horizontal scrollbar" — overflow hidden on body. "adapt down to 375px" — canvas full width, HUD scales, overlay card clamp. Test mentally at 375×667: paddle 96, cols 8, side max(20,16.9)=20, gap max(4,2.25)=4, bw=(375-40-28)/8=38.4 — small bricks but ok. bh=max(20, min(28,21))=21. HUD fs=max(13,min(17,6.75))=13. Score at 18, lives at W-26.. ok. Overlay h1 clamp(26px,7vw,54px) → 26px. Good.

Write the code now. I'll be careful about typos.

Also: `performance.now()/1000` for lastInput etc.

Let me write:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🍔 Heartbreaker — Hamburger Breakout</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:#160a2b; }
  body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; user-select:none; -webkit-user-select:none; }
  #game { display:block; position:fixed; inset:0; touch-action:none; }
  .overlay { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:10;
    background: radial-gradient(ellipse at center, rgba(30,10,55,0.25), rgba(10,4,24,0.55));
    transition: opacity .5s ease; }
  .overlay.hidden { opacity:0; pointer-events:none; }
  .card { text-align:center; padding:30px 42px; border-radius:24px; max-width:min(92vw,560px);
    background: rgba(24,10,46,0.62); border:1px solid rgba(255,130,170,0.35);
    box-shadow:0 0 60px rgba(255,70,130,0.25), inset 0 0 40px rgba(255,90,150,0.06);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
  h1 { font-size: clamp(26px, 7vw, 52px); font-weight:900; letter-spacing:1px;
    background: linear-gradient(180deg,#ffe3ec,#ff5d8f 60%,#ff2e63);
    -webkit-background-clip:text; background-clip:text; color:transparent;
    filter: drop-shadow(0 4px 14px rgba(255,60,120,0.45)); margin-bottom:10px; }
  .tag { color:#ffd6e3; font-size: clamp(13px, 2.4vw, 16px); margin-bottom:18px; opacity:.9; }
  .start { color:#fff; font-weight:800; font-size: clamp(15px, 3vw, 20px); margin-bottom:12px;
    animation: pulse 1.4s ease-in-out infinite; }
  .hint { color:rgba(255,214,227,0.55); font-size:12px; }
  @keyframes pulse { 0%,100%{opacity:1; transform:scale(1);} 50%{opacity:.5; transform:scale(.96);} }
  @media (max-width:480px){ .card{ padding:20px 18px; } .hint{ display:none; } }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="overlay" class="overlay">
  <div class="card">
    <h1>🍔 HEARTBREAKER</h1>
    <p class="tag">Your paddle is a hamburger. Smash every brick and it explodes into a shower of little hearts.</p>
    <p class="start">▶ &nbsp;Click or press any key to start</p>
    <p class="hint">move: mouse or ← → &nbsp;·&nbsp; space: launch &nbsp;·&nbsp; P: pause</p>
  </div>
</div>
<div id="over" class="overlay hidden">
  <div class="card">
    <h1>💔 GAME OVER</h1>
    <p class="tag" id="finalScore"></p>
    <p class="start">▶ &nbsp;Click or press any key to play again</p>
  </div>
</div>
<script>
...
</script>
</body>
</html>
```

Now the JS. Write it fully:

```js
(() => {
'use strict';
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const overEl = document.getElementById('over');
const finalScore = document.getElementById('finalScore');

let W = 0, H = 0, dpr = 1, topWall = 78;
let bgGrad = null, glowGrad = null;

const state = { mode:'demo', score:0, lives:3, level:1, combo:0, paused:false, over:false, clearing:false, clearTimer:0, cols:10, rows:4 };
const paddle = { cx:0, y:0, w:140, h:46, vx:0, tilt:0 };
const ball = { x:0, y:0, vx:0, vy:0, r:10, stuck:true };
let bricks = [];
let particles = [];
let popups = [];
let trail = [];
const bgHearts = [];
const keys = { left:false, right:false };
let mouseX = null, lastMove = -10, lastInput = -10;
let aiErr = 0, aiTimer = 0, stuckT = 0, time = 0, shake = 0;
let AC = null;

const HEART_COLORS = ['#ff2e63','#ff5d8f','#ff8fab','#ffb3c6','#ffd6e0','#ffffff'];

/* helpers */
function heartPath(c, s){
  c.beginPath();
  c.moveTo(0, 10*s);
  c.bezierCurveTo(-6*s, 4*s, -10*s, 0, -10*s, -4*s);
  c.bezierCurveTo(-10*s, -7.5*s, -7.5*s, -9.5*s, -5*s, -9.5*s);
  c.bezierCurveTo(-3*s, -9.5*s, -1*s, -8*s, 0, -6*s);
  c.bezierCurveTo(1*s, -8*s, 3*s, -9.5*s, 5*s, -9.5*s);
  c.bezierCurveTo(7.5*s, -9.5*s, 10*s, -7.5*s, 10*s, -4*s);
  c.bezierCurveTo(10*s, 0, 6*s, 4*s, 0, 10*s);
  c.closePath();
}
function drawHeart(x, y, s, rot, color, alpha){
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(rot);
  ctx.globalAlpha = alpha;
  ctx.fillStyle = color;
  heartPath(ctx, s);
  ctx.fill();
  ctx.restore();
}
function rr(c, x, y, w, h, r){
  r = Math.min(r, w/2, h/2);
  c.beginPath();
  c.moveTo(x+r, y);
  c.arcTo(x+w, y, x+w, y+h, r);
  c.arcTo(x+w, y+h, x, y+h, r);
  c.arcTo(x, y+h, x, y, r);
  c.arcTo(x, y, x+w, y, r);
  c.closePath();
}
const rand = (a,b) => a + Math.random()*(b-a);
```

resize:
```js
function resize(){
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  W = window.innerWidth; H = window.innerHeight;
  canvas.width = Math.round(W*dpr); canvas.height = Math.round(H*dpr);
  canvas.style.width = W+'px'; canvas.style.height = H+'px';
  topWall = Math.max(66, Math.min(92, H*0.1));
  paddle.w = Math.max(96, Math.min(170, W*0.17));
  paddle.h = paddle.w * 0.33;
  paddle.y = H - 74;
  paddle.cx = Math.max(paddle.w/2+6, Math.min(W-paddle.w/2-6, paddle.cx || W/2));
  ball.x = Math.max(ball.r, Math.min(W-ball.r, ball.x));
  ball.y = Math.max(topWall+ball.r, Math.min(H-ball.r, ball.y));
  bgGrad = ctx.createLinearGradient(0,0,0,H);
  bgGrad.addColorStop(0,'#2a1150');
  bgGrad.addColorStop(0.5,'#3b1663');
  bgGrad.addColorStop(1,'#170a2e');
  glowGrad = ctx.createRadialGradient(W/2,-60,0,W/2,-60,Math.max(W,H)*0.85);
  glowGrad.addColorStop(0,'rgba(255,90,150,0.20)');
  glowGrad.addColorStop(1,'rgba(255,90,150,0)');
  layoutBricks();
}
window.addEventListener('resize', resize);
```

buildLevel / layoutBricks:
```js
function buildLevel(){
  state.cols = W < 520 ? 8 : 10;
  state.rows = Math.min(3 + state.level, 7);
  bricks = [];
  const palette = ['#ff4d6d','#ff7a59','#ffb84d','#ffe14d','#8ee06a','#4dd6c1','#6aa8ff','#b48cff'];
  for(let r=0;r<state.rows;r++){
    for(let c=0;c<state.cols;c++){
      const gold = (r===0 && Math.random()<0.16);
      const hp = gold ? 1 : (state.level>=2 && r<2 ? 2 : 1);
      bricks.push({ col:c, row:r, alive:true, gold, hp, maxHp:hp, color: gold ? '#ffd166' : palette[r%palette.length], x:0,y:0,w:0,h:0 });
    }
  }
  layoutBricks();
}
function layoutBricks(){
  if(!bricks.length || !state.cols) return;
  const side = Math.max(20, W*0.045);
  const gap = Math.max(4, W*0.006);
  const bw = (W - side*2 - gap*(state.cols-1)) / state.cols;
  const bh = Math.max(20, Math.min(28, H*0.032));
  const top = topWall + 36;
  for(const b of bricks){
    b.x = side + b.col*(bw+gap);
    b.y = top + b.row*(bh+gap);
    b.w = bw; b.h = bh;
  }
}
```

audio, serve/launch, burst, popups, control, moveBall, collisions, gameOver/reset, input, update, render, draw functions, init.

Let me write remaining JS in final answer directly, being careful.

moveBall:
```js
function moveBall(dt){
  const sp = Math.hypot(ball.vx, ball.vy);
  const steps = Math.max(1, Math.ceil(sp*dt/6));
  const sdt = dt/steps;
  for(let i=0;i<steps;i++){
    ball.x += ball.vx*sdt;
    ball.y += ball.vy*sdt;
    if(ball.x < ball.r){ ball.x = ball.r; ball.vx = Math.abs(ball.vx); tone(170,0.05,'square',0.05); }
    else if(ball.x > W-ball.r){ ball.x = W-ball.r; ball.vx = -Math.abs(ball.vx); tone(170,0.05,'square',0.05); }
    if(ball.y < topWall+ball.r){ ball.y = topWall+ball.r; ball.vy = Math.abs(ball.vy); tone(190,0.05,'square',0.05); }
    // paddle
    if(ball.vy > 0 && ball.y + ball.r >= paddle.y && ball.y - ball.r <= paddle.y + paddle.h && Math.abs(ball.x - paddle.cx) <= paddle.w/2 + ball.r){
      const off = Math.max(-1, Math.min(1, (ball.x - paddle.cx)/(paddle.w/2)));
      const ang = off * 1.05;
      const nsp = Math.min(sp + 14, 940);
      ball.vx = nsp*Math.sin(ang);
      ball.vy = -nsp*Math.cos(ang);
      ball.y = paddle.y - ball.r - 0.5;
      state.combo = 0;
      tone(240,0.09,'triangle',0.14,160);
      burst(ball.x, paddle.y, 6, '#ff8fab', 0.45);
    }
    // bricks
    for(const b of bricks){
      if(!b.alive) continue;
      if(ball.x+ball.r < b.x || ball.x-ball.r > b.x+b.w || ball.y+ball.r < b.y || ball.y-ball.r > b.y+b.h) continue;
      const cx = Math.max(b.x, Math.min(ball.x, b.x+b.w));
      const cy = Math.max(b.y, Math.min(ball.y, b.y+b.h));
      let dx = ball.x-cx, dy = ball.y-cy;
      if(dx*dx+dy*dy <= ball.r*ball.r){
        if(dx===0 && dy===0) dy = -1;
        if(Math.abs(dx) > Math.abs(dy)){
          ball.vx = dx>0 ? Math.abs(ball.vx) : -Math.abs(ball.vx);
          ball.x = cx + (dx>0 ? ball.r+0.5 : -(ball.r+0.5));
        } else {
          ball.vy = dy>0 ? Math.abs(ball.vy) : -Math.abs(ball.vy);
          ball.y = cy + (dy>0 ? ball.r+0.5 : -(ball.r+0.5));
        }
        hitBrick(b);
        break;
      }
    }
  }
  // keep ball from going too flat
  const sp2 = Math.hypot(ball.vx, ball.vy);
  if(Math.abs(ball.vy) < 70){
    ball.vy = (ball.vy >= 0 ? 1 : -1) * 70;
    const nx = Math.sqrt(Math.max(sp2*sp2 - 70*70, 1));
    ball.vx = (ball.vx >= 0 ? 1 : -1) * nx;
  }
  if(ball.y - ball.r > H) loseLife();
}
```

hitBrick, burst, addPopup, heartRain as planned.

controlPaddle as planned.

update:
```js
function update(dt){
  updateAmbient(dt);
  controlPaddle(dt);
  if(ball.stuck){
    ball.x = paddle.cx;
    ball.y = paddle.y - ball.r - 2;
    if(state.mode === 'demo'){
      stuckT += dt;
      if(stuckT > 0.8) launch();
    }
  } else {
    moveBall(dt);
    trail.unshift({x:ball.x, y:ball.y});
    if(trail.length > 11) trail.pop();
  }
  if(state.clearing){
    state.clearTimer -= dt;
    if(state.clearTimer <= 0){
      state.level++;
      state.clearing = false;
      buildLevel();
      serve();
      addPopup(W/2, H*0.42, 'LEVEL ' + state.level, '#ffd6e0', 38, 1.6);
      sfxLevel();
    }
  } else if(bricks.length && bricks.every(b => !b.alive)){
    state.clearing = true;
    state.clearTimer = 1.7;
    heartRain();
    addPopup(W/2, H*0.45, 'LEVEL CLEAR!', '#ff8fab', 42, 1.5);
    sfxLevel();
  }
}
function updateAmbient(dt){
  time += dt;
  for(const h of bgHearts){
    h.y -= h.vy*dt;
    h.rot += h.vr*dt;
    if(h.y < -40){ h.y = H + 40; h.fx = Math.random(); }
  }
  for(let i=particles.length-1;i>=0;i--){
    const p = particles[i];
    p.life -= dt;
    if(p.life <= 0){ particles.splice(i,1); continue; }
    p.vy += 560*dt;
    const dr = Math.max(0, 1 - 1.5*dt);
    p.vx *= dr; p.vy *= Math.max(0, 1 - 0.55*dt);
    p.x += p.vx*dt; p.y += p.vy*dt;
    p.rot += p.vr*dt;
    p.alpha = Math.min(1, p.life/0.5);
  }
  for(let i=popups.length-1;i>=0;i--){
    const p = popups[i];
    p.life -= dt; p.y -= 46*dt;
    if(p.life <= 0) popups.splice(i,1);
  }
  shake = Math.max(0, shake - 26*dt);
}
```

render + drawBurger + drawBrick + HUD as planned.

drawBurger:
```js
function drawBurger(){
  const p = paddle, w = p.w, u = w/140, cx = p.cx, y0 = p.y;
  const x0 = cx - w/2, x1 = cx + w/2;
  // shadow
  ctx.fillStyle = 'rgba(0,0,0,0.30)';
  ctx.beginPath(); ctx.ellipse(cx, y0 + p.h + 5, w*0.46, 5, 0, 0, Math.PI*2); ctx.fill();
  ctx.save();
  ctx.translate(cx, y0 + p.h/2);
  ctx.rotate(p.tilt);
  ctx.translate(-cx, -(y0 + p.h/2));
  // bottom bun
  rr(ctx, x0+2*u, y0+39*u, w-4*u, 7*u, 3*u); ctx.fillStyle = '#e8a558'; ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,0.15)';
  rr(ctx, x0+2*u, y0+43.5*u, w-4*u, 2.5*u, 1.2*u); ctx.fill();
  // patty
  rr(ctx, x0+1.5*u, y0+30*u, w-3*u, 9*u, 4*u); ctx.fillStyle = '#6b3a1c'; ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  const dots = [[-40,33.5],[-18,36],[0,33],[22,36],[40,33.5]];
  for(const d of dots){ ctx.beginPath(); ctx.arc(cx+d[0]*u, y0+d[1]*u, 0.9*u, 0, Math.PI*2); ctx.fill(); }
  // cheese
  ctx.fillStyle = '#ffc93c';
  rr(ctx, x0+3*u, y0+25*u, w-6*u, 5*u, 1.5*u); ctx.fill();
  const drips = [x0+16*u, cx-26*u, cx+26*u, x1-16*u];
  for(const dx of drips){
    ctx.beginPath();
    ctx.moveTo(dx, y0+29.5*u);
    ctx.lineTo(dx+6*u, y0+29.5*u);
    ctx.lineTo(dx+3*u, y0+34.5*u);
    ctx.closePath(); ctx.fill();
  }
  // tomato
  rr(ctx, x0+5*u, y0+20*u, w-10*u, 5*u, 2.5*u); ctx.fillStyle = '#e8483f'; ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,0.25)';
  rr(ctx, x0+8*u, y0+21*u, w-16*u, 1.4*u, 0.7*u); ctx.fill();
  // lettuce
  ctx.beginPath();
  ctx.moveTo(x0-4*u, y0+20*u);
  const step = 12*u;
  for(let x = x0-4*u; x < x1+4*u; x += step){
    ctx.quadraticCurveTo(x+step/2, y0+12.5*u, Math.min(x+step, x1+4*u), y0+20*u);
  }
  ctx.lineTo(x1+4*u, y0+22*u);
  ctx.lineTo(x0-4*u, y0+22*u);
  ctx.closePath();
  ctx.fillStyle = '#7ed957'; ctx.fill();
  // top bun
  const g = ctx.createLinearGradient(0, y0, 0, y0+16*u);
  g.addColorStop(0, '#f8cd8a'); g.addColorStop(1, '#e0973f');
  ctx.beginPath();
  ctx.moveTo(x0, y0+15*u);
  ctx.lineTo(x0, y0+9*u);
  ctx.quadraticCurveTo(x0, y0, cx, y0);
  ctx.quadraticCurveTo(x1, y0, x1, y0+9*u);
  ctx.lineTo(x1, y0+15*u);
  ctx.closePath();
  ctx.fillStyle = g; ctx.fill();
  // shine
  ctx.fillStyle = 'rgba(255,255,255,0.30)';
  ctx.beginPath(); ctx.ellipse(cx - w*0.18, y0+5*u, 13*u, 3.2*u, -0.25, 0, Math.PI*2); ctx.fill();
  // sesame
  ctx.fillStyle = '#fff3d6';
  const seeds = [[-33,8,-0.5],[-19,5,0.3],[-4,4,-0.2],[12,5,0.25],[28,8,-0.3],[-42? no]];
```
seeds: [[-33,8,-0.5],[-19,5,0.3],[-4,4,-0.2],[12,5,0.25],[28,8,-0.3]] — 5 seeds; add [ -46? outside dome at dy? at dy 8 half-width 62u, -46 ok but close to edge; skip]. Add one more: [20? ] fine 5-6. Add [ -10? ] no. Use 6: [[-33,8,-0.5],[-19,5,0.3],[-4,4,-0.2],[12,5,0.25],[28,8,-0.3],[0? no center dip area is y -6..? dome top center... seed at (0,6)? center top fine: [0? maybe [ -? ]]. I'll add [ -45? no]. Keep 5 plus [22? dup]. Final: [[-34,8,-0.5],[-20,5,0.3],[-5,4,-0.2],[11,5,0.25],[27,8,-0.3]].

```js
  for(const s of seeds){
    ctx.save();
    ctx.translate(cx + s[0]*u, y0 + s[1]*u);
    ctx.rotate(s[2]);
    ctx.beginPath(); ctx.ellipse(0, 0, 2.4*u, 1.3*u, 0, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  }
  // face on the patty
  const ex = 10*u, ey = y0+34*u;
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(cx-ex, ey, 2.6*u, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(cx+ex, ey, 2.6*u, 0, Math.PI*2); ctx.fill();
  ctx.fillStyle = '#2a1508';
  ctx.beginPath(); ctx.arc(cx-ex+0.5*u, ey+0.4*u, 1.25*u, 0, Math.PI*2); ctx.fill();
  ctx.beginPath(); ctx.arc(cx+ex+0.5*u, ey+0.4*u, 1.25*u, 0, Math.PI*2); ctx.fill();
  ctx.strokeStyle = '#2a1508'; ctx.lineWidth = 1.6*u; ctx.lineCap = 'round';
  ctx.beginPath(); ctx.arc(cx, y0+32.5*u, 4.6*u, Math.PI*0.25, Math.PI*0.75); ctx.stroke();
  ctx.restore();
}
```

drawBrick:
```js
function drawBrick(b){
  rr(ctx, b.x, b.y, b.w, b.h, 6);
  ctx.fillStyle = b.color; ctx.fill();
  ctx.fillStyle = 'rgba(255,255,255,0.30)';
  rr(ctx, b.x+2, b.y+2, b.w-4, b.h*0.4, 4); ctx.fill();
  ctx.fillStyle = 'rgba(0,0,0,0.20)';
  rr(ctx, b.x+2, b.y+b.h*0.62, b.w-4, b.h*0.34, 4); ctx.fill();
  if(b.gold) drawHeart(b.x+b.w/2, b.y+b.h/2+1, 0.34, 0, 'rgba(255,255,255,0.95)', 1);
  if(b.maxHp > 1 && b.hp < b.maxHp){
    ctx.strokeStyle = 'rgba(40,5,20,0.5)'; ctx.lineWidth = 1.5;
    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.55); ctx.lineTo(b.x+b.w*0.34, b.y+b.h-3);
    ctx.moveTo(b.x+b.w*0.66, b.y+4); ctx.lineTo(b.x+b.w*0.58, b.y+b.h*0.5); ctx.lineTo(b.x+b.w*0.72, b.y+b.h-4);
    ctx.stroke();
  }
}
```

drawBall:
```js
function drawBall(){
  ctx.save();
  ctx.translate(ball.x, ball.y);
  ctx.rotate(Math.sin(time*3)*0.15);
  ctx.shadowColor = '#ff4d8d'; ctx.shadowBlur = 18;
  ctx.fillStyle = '#ff2e63';
  heartPath(ctx, 0.62); ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255,255,255,0.85)';
  ctx.beginPath(); ctx.ellipse(-2.6, -3.4, 1.8, 1.1, -0.6, 0, Math.PI*2); ctx.fill();
  ctx.restore();
}
```

HUD:
```js
function drawHUD(){
  const fs = Math.max(13, Math.min(17, W*0.018));
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'left';
  ctx.fillStyle = 'rgba(255,190,215,0.6)';
  ctx.font = '700 ' + fs + 'px system-ui, sans-serif';
  ctx.fillText('SCORE', 18, 26);
  ctx.fillStyle = '#ffffff';
  ctx.font = '800 ' + (fs+5) + 'px system-ui, sans-serif';
  ctx.fillText(String(state.score).padStart(6,'0'), 18, 46);
  ctx.textAlign = 'center';
  ctx.fillStyle = 'rgba(255,214,227,0.9)';
  ctx.font = '800 ' + (fs+2) + 'px system-ui, sans-serif';
  ctx.fillText('🍔 HEARTBREAKER', W/2, 22);
  ctx.fillStyle = 'rgba(255,255,255,0.75)';
  ctx.font = '700 ' + fs + 'px system-ui, sans-serif';
  ctx.fillText('LEVEL ' + state.level, W/2, 42);
  if(state.combo >= 2){
    ctx.fillStyle = '#ff8fab';
    ctx.font = '800 ' + (fs+4) + 'px system-ui, sans-serif';
    ctx.fillText('COMBO ×' + state.combo, W/2, 63);
  }
  const n = Math.min(Math.max(state.lives,0), 5);
  for(let i=0;i<5;i++){
    drawHeart(W-28-i*28, 32, 0.55, 0, i < n ? '#ff4d8d' : 'rgba(255,255,255,0.14)', 1);
  }
}
```
Lives: draw 5 slots always; filled i<n. If lives could exceed 5 (no), fine.

render:
```js
function render(){
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, W, H);
  ctx.fillStyle = glowGrad; ctx.fillRect(0, 0, W, H);
  for(const h of bgHearts) drawHeart(h.fx*W, h.y, h.s, h.rot, h.color, h.alpha);
  ctx.save();
  if(shake > 0.3) ctx.translate(rand(-shake, shake), rand(-shake, shake));
  for(const b of bricks) if(b.alive) drawBrick(b);
  for(let i=trail.length-1;i>=0;i--){
    const k = 1 - i/trail.length;
    drawHeart(trail[i].x, trail[i].y, 0.12 + 0.22*k, 0, '#ff8fab', 0.35*k);
  }
  for(const p of particles) drawHeart(p.x, p.y, p.s, p.rot, p.color, p.alpha);
  ctx.textAlign = 'center';
  for(const p of popups){
    ctx.globalAlpha = Math.max(0, p.life/p.max);
    ctx.fillStyle = p.color;
    ctx.font = '800 ' + p.size + 'px system-ui, sans-serif';
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
  drawBurger();
  if(!state.over) drawBall();
  ctx.restore();
  drawHUD();
  if(state.paused && !state.over){
    ctx.fillStyle = 'rgba(10,4,22,0.55)'; ctx.fillRect(0,0,W,H);
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillStyle = '#fff'; ctx.font = '900 36px system-ui, sans-serif';
    ctx.fillText('PAUSED', W/2, H/2 - 12);
    ctx.font = '600 16px system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.7)';
    ctx.fillText('press P to resume', W/2, H/2 + 22);
  }
}
```

addPopup:
```js
function addPopup(x, y, text, color, size, life){
  popups.push({x, y, text, color, size, life, max:life});
}
```

burst:
```js
function burst(x, y, n, brickColor, power){
  for(let i=0;i<n;i++){
    if(particles.length > 750) particles.shift();
    const a = Math.random()*Math.PI*2;
    const sp = (60 + Math.random()*380) * power;
    const life = 0.9 + Math.random()*1.0;
    particles.push({
      x: x + rand(-10,10), y: y + rand(-8,8),
      vx: Math.cos(a)*sp, vy: Math.sin(a)*sp - 170,
      s: 0.15 + Math.random()*0.3,
      rot: Math.random()*Math.PI*2, vr: rand(-6,6),
      life, max: life,
      color: Math.random() < 0.55 ? HEART_COLORS[(Math.random()*HEART_COLORS.length)|0] : brickColor,
      alpha: 1
    });
  }
}
function heartRain(){
  for(let i=0;i<110;i++){
    const life = 2.5 + Math.random()*2;
    particles.push({
      x: Math.random()*W, y: -30 - Math.random()*H*0.6,
      vx: rand(-70,70), vy: 90 + Math.random()*230,
      s: 0.2 + Math.random()*0.35,
      rot: Math.random()*6.28, vr: rand(-4,4),
      life, max: life,
      color: HEART_COLORS[(Math.random()*HEART_COLORS.length)|0],
      alpha: 1
    });
  }
}
```

hitBrick:
```js
function hitBrick(b){
  b.hp--;
  if(b.hp <= 0){
    b.alive = false;
    state.combo++;
    const pts = (b.gold ? 50 : 10) + 5*state.combo;
    state.score += pts;
    addPopup(b.x+b.w/2, b.y, '+'+pts, '#ffffff', 15, 0.9);
    if(state.combo >= 3) addPopup(b.x+b.w/2, b.y-24, 'COMBO ×'+state.combo, '#ff8fab', 14, 0.9);
    burst(b.x+b.w/2, b.y+b.h/2, b.gold ? 56 : Math.min(46, 22 + state.level*4), b.color, b.gold ? 1.4 : 1);
    shake = Math.min(7, 2.5 + state.combo*0.4);
    tone(300 + Math.min(state.combo,10)*35, 0.1, 'triangle', 0.12, 240);
    if(b.gold) sfxGold();
  } else {
    burst(b.x+b.w/2, b.y+b.h/2, 8, b.color, 0.7);
    tone(220, 0.07, 'square', 0.08, -60);
  }
}
```

serve/launch/loseLife/gameOver/reset:
```js
function ballSpeed(){ return Math.min(380 + (state.level-1)*40, 620); }
function serve(){ ball.stuck = true; stuckT = 0; }
function launch(){
  if(!ball.stuck) return;
  ball.stuck = false; stuckT = 0;
  const sp = ballSpeed();
  const a = rand(-0.3, 0.3);
  ball.vx = sp*Math.sin(a);
  ball.vy = -sp*Math.cos(a);
  tone(300, 0.08, 'triangle', 0.1, 150);
}
function loseLife(){
  state.lives--; state.combo = 0;
  burst(ball.x, Math.min(ball.y, H-40), 46, '#ff5d8f', 1.2);
  shake = 9;
  tone(320, 0.45, 'sawtooth', 0.12, -270);
  if(state.lives <= 0) gameOver();
  else { serve(); addPopup(W/2, H*0.4, '💔', '#ff8fab', 34, 1.1); }
}
let overToken = 0;
function gameOver(){
  state.over = true;
  finalScore.textContent = 'SCORE ' + state.score + '  ·  LEVEL ' + state.level;
  overEl.classList.remove('hidden');
  if(state.mode === 'demo'){
    const tok = ++overToken;
    setTimeout(() => { if(tok === overToken && state.over){ resetGame(); overEl.classList.add('hidden'); } }, 3000);
  }
}
function resetGame(){
  state.score = 0; state.lives = 3; state.level = 1; state.combo = 0;
  state.over = false; state.clearing = false; state.paused = false;
  popups.length = 0;
  buildLevel();
  serve();
}
```

controlPaddle:
```js
function aiTarget(){
  if(ball.stuck || ball.vy < 0) return W/2 + Math.sin(time*1.3)*W*0.06;
  const t = (paddle.y - ball.y)/ball.vy;
  let x = ball.x + ball.vx*t;
  const per = 2*W;
  x = ((x % per) + per) % per;
  if(x > W) x = per - x;
  return Math.max(paddle.w/2+6, Math.min(W-paddle.w/2-6, x + aiErr));
}
function controlPaddle(dt){
  const now = performance.now()/1000;
  const prev = paddle.cx;
  if(keys.left) paddle.cx -= 780*dt;
  else if(keys.right) paddle.cx += 780*dt;
  else if(state.mode === 'demo' || now - lastInput > 3.5){
    aiTimer -= dt;
    if(aiTimer <= 0){ aiTimer = 0.22; aiErr = rand(-1,1)*Math.max(6, 36 - state.level*4); }
    const tgt = aiTarget();
    const sp = Math.min(560 + state.level*40, 860);
    const d = tgt - paddle.cx;
    paddle.cx += Math.max(-sp*dt, Math.min(sp*dt, d));
  } else if(now - lastMove < 3.5 && mouseX !== null){
    paddle.cx += (mouseX - paddle.cx)*Math.min(1, dt*20);
  }
  paddle.cx = Math.max(paddle.w/2+6, Math.min(W-paddle.w/2-6, paddle.cx));
  paddle.vx = (paddle.cx - prev)/Math.max(dt, 1e-4);
  const targetTilt = Math.max(-0.14, Math.min(0.14, paddle.vx*0.00045));
  paddle.tilt += (targetTilt - paddle.tilt)*Math.min(1, dt*10);
}
```

audio:
```js
function initAudio(){
  if(!AC){
    try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){ AC = null; }
  }
  if(AC && AC.state === 'suspended') AC.resume();
}
function tone(f, d, type, v, slide){
  if(!AC) return;
  try {
    const t = AC.currentTime;
    const o = AC.createOscillator(), g = AC.createGain();
    o.type = type || 'triangle';
    o.frequency.setValueAtTime(f, t);
    if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, f+slide), t+d);
    g.gain.setValueAtTime(v || 0.12, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t+d);
    o.connect(g); g.connect(AC.destination);
    o.start(t); o.stop(t+d+0.05);
  } catch(e){}
}
function sfxGold(){
  tone(660, 0.09, 'square', 0.09, 0);
  setTimeout(() => tone(880, 0.12, 'square', 0.09, 0), 70);
  setTimeout(() => tone(1320, 0.18, 'square', 0.08, 0), 140);
}
function sfxLevel(){
  [523, 659, 784, 1046].forEach((f,i) => setTimeout(() => tone(f, 0.15, 'triangle', 0.11, 0), i*90));
}
```

input:
```js
function startFromGesture(){
  initAudio();
  lastInput = performance.now()/1000;
  if(state.over){ resetGame(); overEl.classList.add('hidden'); }
  if(state.mode === 'demo'){ state.mode = 'play'; overlay.classList.add('hidden'); }
  if(ball.stuck && !state.clearing) launch();
}
window.addEventListener('pointerdown', e => {
  mouseX = e.clientX; lastMove = performance.now()/1000;
  startFromGesture();
});
window.addEventListener('pointermove', e => {
  mouseX = e.clientX; lastMove = performance.now()/1000; lastInput = performance.now()/1000;
});
window.addEventListener('keydown', e => {
  if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].indexOf(e.key) !== -1) e.preventDefault();
  if(e.key === 'ArrowLeft') keys.left = true;
  if(e.key === 'ArrowRight') keys.right = true;
  if(e.key === 'p' || e.key === 'P'){ if(!state.over) state.paused = !state.paused; }
  if(e.key === ' ' && ball.stuck && !state.clearing) launch();
  startFromGesture();
});
window.addEventListener('keyup', e => {
  if(e.key === 'ArrowLeft') keys.left = false;
  if(e.key === 'ArrowRight') keys.right = false;
});
```
Note: keydown ' ' triggers launch twice (explicit + startFromGesture) — launch guards !stuck, second is no-op. Fine. Also startFromGesture called after setting paused — if over, reset. OK. But careful: pressing 'p' while over → we toggle paused only if !over; then startFromGesture resets game (over→reset) and sets lastInput. OK.

Hmm: keydown handler calls startFromGesture() which if state.over resets — but the 'p' toggle happened before; resetGame sets paused=false. fine.

frame loop:
```js
let last = performance.now();
function frame(t){
  requestAnimationFrame(frame);
  let dt = (t - last)/1000;
  last = t;
  if(dt > 0.05) dt = 0.05;
  if(dt < 0) dt = 0;
  if(!state.paused && !state.over) update(dt);
  else updateAmbient(dt);
  render();
}
```

init:
```js
for(let i=0;i<34;i++){
  bgHearts.push({ fx: Math.random(), y: Math.random()*1200, vy: rand(8,26), s: rand(0.25,0.8), rot: Math.random()*6.28, vr: rand(-0.5,0.5), alpha: rand(0.04,0.11), color: HEART_COLORS[(Math.random()*HEART_COLORS.length)|0] });
}
resize();
paddle.cx = W/2;
buildLevel();
ball.x = paddle.cx; ball.y = paddle.y - ball.r - 2;
serve();
requestAnimationFrame(frame);
```

Wait: resize() sets paddle.cx using `paddle.cx || W/2` — paddle.cx initialized 0 → W/2. Good; then set again anyway.

bgHearts y up to 1200 but H may be 800; they wrap when y<-40 to H+40 — initial y beyond H just invisible until wrap... if y=1100 > H=800, heart drawn below screen, moves up eventually (vy positive? y decreases). vy positive, y -= vy*dt → moves up into view. fine.

Also on resize, H changes; bgHearts with y > H will come up. ok.

One more: in update, when state.clearing, ball stuck? Ball might still be moving; when clearing triggered, set ball.stuck = true to freeze? During clearing (1.7s), ball keeps bouncing — harmless. But bricks all dead; ball bounces off paddle/walls. Fine, keeps action. But then serve() sets stuck. OK. Actually during clearing maybe hide ball? It's fine.

Also during clearing, moveBall still runs — could lose life during the 1.7s if ball falls — loseLife → serve; then clearing completes → buildLevel + serve again. Minor edge, acceptable. Or set ball.stuck=true when clearing starts to avoid losing life: in the else-if branch add ball.stuck = true? Then ball sits at paddle via update. But update's stuck branch positions ball at paddle — yes do: state.clearing branch sets ball.stuck = true. Add `ball.stuck = true;` there. But then launch in demo? stuck branch: demo auto-launch would launch during clearing... guard: in stuck branch, only auto-launch if !state.clearing. Add condition.

Also startFromGesture launch guard already has !state.clearing.

render draws ball when !state.over — during clearing ball stuck on paddle, fine.

Double-check drawHeart alpha for particles uses p.alpha which we update. Good.

Popup font uses p.size px — set in addPopup.

Edge: `String(state.score).padStart(6,'0')` fine.

Emoji '🍔' in canvas fillText — fine in Chrome.

Check requirement: "Click or press a key to start" message visible — yes, overlay with that text. Demo self-plays behind — yes mode demo AI.

Potential console errors: none apparent. `backdrop-filter` fine. `window.webkitAudioContext` guarded.

One thing: pointerdown fires also when clicking after game over overlay visible → startFromGesture resets. Good.

Also on touch, pointermove during drag updates paddle — good.

Prevent double-tap zoom: viewport user-scalable=no + touch-action none.

Now, is there any use of blocked APIs? No localStorage etc. setTimeout allowed. No external requests. 

Let me also make sure the ball doesn't get stuck bouncing horizontally between walls forever — the vy<70 safeguard handles it.

Also initial serve: ball.stuck=true, demo auto-launch at 0.8s. Screenshot at ~3s: ball launched at 0.8, travels up ~464px at speed ~380 → hits bricks ~2.0-2.2s, burst hearts live 0.9-1.9s → visible until ~4s. At 3s hearts visible. 

Also add a tiny bit: at load, spawn an initial celebratory burst? Could add burst at first brick area... not necessary.

Let me also ensure bricks don't overlap HUD: top = topWall+36 = ~114 at H=800. HUD text at y≤63. Good.

Level rows up to 7: top 114 + 7*(25.6+7.7)=114+233=347 < paddle 726. fine.

Now assemble final file. Also include `lang="en"`. Make sure script wrapped in IIFE and 'use strict'.

One more check on `layoutBricks` being called in resize before state.cols set — guard `if(!bricks.length || !state.cols) return;` bricks=[] initially and state.cols=10 initial — I set state.cols initial 10, bricks empty → returns. Good.

In buildLevel I assign `bricks = []` (let). ok.

trail declared. ok.

Write final answer with single html code block. Also double-check no stray text outside block.

CSS: overlay z-index above canvas. canvas position fixed.

Let me finalize the seeds array: `const seeds = [[-34,8,-0.5],[-20,5,0.3],[-5,4,-0.2],[11,5,0.25],[27,8,-0.3]];`

Face position: patty 30u–39u; eyes at ey=y0+34u, radius 2.6u → 31.4–36.6 within patty. Smile center y0+32.5u r4.6u → lowest 37.1u, within patty (39u). Good. But cheese drips at cx±26u and x0+16u, x1-16u end at 34.5u — don't overlap smile (cx±4.6u). Good.

Eyes at cx±10u; smile spans cx±~4u. Fine.

Patty dots at [-40,33.5] etc — near edges, ok.

Check drawBurger rotate: rotate around center of burger; shadow drawn before save/rotate — shadow ellipse at y0+h+5 unrotated. Good.

Ball r=10 but heart drawn s=0.62 → heart ~12.4 wide, 12.4 tall — collision radius 10 slightly larger than visual width/2=6.2... visual smaller than collision circle; acceptable (forgiving). Maybe s=0.75 → 15 wide. Use 0.7. r=10 ok.

Trail hearts small.

I think ready. Also add `aria` nothing needed.

Final check of update order: controlPaddle before ball move so paddle position current for collisions. Yes.

In demo mode before start, lastInput=-10 so `now-lastInput>3.5` true, but mode==='demo' branch anyway.

After user starts and stops moving mouse for 3.5s, AI takes over — paddle follows ball; good.

One subtle bug: in keydown, `startFromGesture()` launches ball if stuck — but also when user presses arrow keys just to move, ball launches — that's fine/desired.

Also pressing space when paused? Space launches if stuck; paused blocks update. fine.

Write it out now. Also ensure `<meta name="viewport">` includes viewport-fit? not needed.

Counting code size ~ fine.

One more: `ctx.textBaseline` set in HUD each time; popups drawn with default baseline 'alphabetic' — set textBaseline='alphabetic' before popups or it inherits from HUD (middle) — set explicitly in render: after ctx.save() for world, set ctx.textBaseline='alphabetic' before popups? Order: popups drawn before HUD; HUD sets 'middle' at its start. Popups drawn earlier with whatever baseline — initial default is 'alphabetic', but after first HUD call it stays 'middle' for subsequent frames' popup drawing. So set `ctx.textBaseline='alphabetic'` in render before popups. Add it.

Also globalAlpha reset after popups — done.

Done. Write final.