Models 44

Gemini 4 Hydrogen

by Googlereasoning defaultClean
Open in new window

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

How it went

Total time
2 min 20 s
Cost
$0.172
Output tokens
22,809
Spent thinking
3,162 tokens
Speed
165 tok/s
Console errors
0
First token after
2.1 s
First visible text after
19.7 s
Prompt tokens
563
Code size
50.1 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-4-hydrogen
Reasoning sent
model default
Max output
40,340 tokens
Generated
Nov 13, 2026

What it was asked

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

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

Code run-1.html · 1780 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>Breakout — Neon Arcade</title>
<style>
  :root {
    --bg: #070b14;
    --panel: #0f172a;
    --border: #1e293b;
    --text: #f1f5f9;
    --muted: #94a3b8;
    --accent: #38bdf8;
    --accent2: #f472b6;
    --gold: #fbbf24;
    --emerald: #34d399;
  }

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: radial-gradient(circle at 50% 20%, #111b33 0%, var(--bg) 75%);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }

  .app {
    width: 100%;
    height: 100dvh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 10px 12px;
    gap: 10px;
  }

  .hud {
    width: 100%;
    max-width: 820px;
    background: rgba(15, 23, 42, 0.85);
    border: 1px solid rgba(56, 189, 248, 0.22);
    border-radius: 12px;
    padding: 8px 14px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(8px);
    flex-shrink: 0;
  }

  .hud-group {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
  }

  .stat {
    display: flex;
    flex-direction: column;
    line-height: 1.15;
  }

  .stat-label {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--muted);
    font-weight: 600;
  }

  .stat-value {
    font-size: 18px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--text);
  }

  .lives-row {
    display: flex;
    align-items: center;
    gap: 6px;
    height: 22px;
  }

  .life-orb {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 35%, #ffffff, #38bdf8 60%, #0284c7 100%);
    box-shadow: 0 0 8px rgba(56, 189, 248, 0.8);
    transition: transform 0.2s, opacity 0.2s;
  }

  .life-orb.lost {
    background: #1e293b;
    box-shadow: none;
    opacity: 0.4;
    transform: scale(0.8);
  }

  .power-badge {
    display: none;
    align-items: center;
    gap: 8px;
    background: rgba(56, 189, 248, 0.14);
    border: 1px solid rgba(56, 189, 248, 0.45);
    border-radius: 999px;
    padding: 4px 10px;
    font-size: 12px;
    font-weight: 600;
    color: #7dd3fc;
  }

  .power-badge.active {
    display: inline-flex;
  }

  .power-bar-wrap {
    width: 48px;
    height: 6px;
    background: rgba(15, 23, 42, 0.9);
    border-radius: 999px;
    overflow: hidden;
  }

  .power-bar-fill {
    height: 100%;
    width: 100%;
    background: linear-gradient(90deg, #38bdf8, #34d399);
    border-radius: 999px;
  }

  .hud-actions {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .icon-btn {
    background: rgba(30, 41, 59, 0.8);
    border: 1px solid rgba(148, 163, 184, 0.25);
    color: var(--text);
    border-radius: 8px;
    padding: 6px 10px;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, transform 0.1s;
  }

  .icon-btn:hover {
    background: rgba(51, 65, 85, 0.95);
    border-color: rgba(56, 189, 248, 0.5);
  }

  .icon-btn:active {
    transform: scale(0.96);
  }

  .stage-wrap {
    position: relative;
    width: 100%;
    max-width: 820px;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .canvas-frame {
    position: relative;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    aspect-ratio: 800 / 580;
    margin: auto;
    border-radius: 14px;
    overflow: hidden;
    border: 1.5px solid rgba(56, 189, 248, 0.28);
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55), inset 0 0 30px rgba(56, 189, 248, 0.05);
    background: #080d1a;
  }

  /* Ensure aspect-ratio fits inside both width & height constraints */
  @supports (aspect-ratio: 800 / 580) {
    .canvas-frame {
      width: min(100%, calc((100dvh - 105px) * 800 / 580));
      height: auto;
    }
  }

  canvas {
    display: block;
    width: 100%;
    height: 100%;
    cursor: none;
  }

  .canvas-frame.show-cursor canvas {
    cursor: default;
  }

  .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    padding: 16px;
    text-align: center;
  }

  .overlay.interactive {
    pointer-events: auto;
    background: rgba(6, 10, 20, 0.72);
    backdrop-filter: blur(6px);
  }

  .overlay.hidden {
    display: none;
  }

  .demo-pill {
    margin-top: auto;
    margin-bottom: 18%;
    background: rgba(10, 16, 32, 0.82);
    border: 1px solid rgba(56, 189, 248, 0.45);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55), 0 0 20px rgba(56, 189, 248, 0.18);
    border-radius: 14px;
    padding: 14px 24px;
    backdrop-filter: blur(6px);
    animation: pulseGlow 2.4s ease-in-out infinite;
  }

  .demo-pill .demo-tag {
    display: inline-block;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: #38bdf8;
    background: rgba(56, 189, 248, 0.14);
    padding: 3px 8px;
    border-radius: 999px;
    margin-bottom: 6px;
  }

  .demo-pill h2 {
    font-size: clamp(16px, 2.8vw, 22px);
    font-weight: 700;
    color: #f8fafc;
    margin-bottom: 4px;
  }

  .demo-pill p {
    font-size: clamp(11px, 1.8vw, 13px);
    color: #cbd5e1;
  }

  @keyframes pulseGlow {
    0%, 100% { transform: translateY(0); box-shadow: 0 10px 30px rgba(0,0,0,0.55), 0 0 16px rgba(56, 189, 248, 0.16); }
    50% { transform: translateY(-2px); box-shadow: 0 14px 34px rgba(0,0,0,0.6), 0 0 26px rgba(56, 189, 248, 0.34); }
  }

  .modal-card {
    background: linear-gradient(160deg, rgba(15, 23, 42, 0.96), rgba(9, 14, 28, 0.98));
    border: 1px solid rgba(148, 163, 184, 0.25);
    border-radius: 18px;
    padding: 26px 30px;
    max-width: 380px;
    width: 100%;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.65);
  }

  .modal-card h1 {
    font-size: clamp(24px, 4vw, 32px);
    font-weight: 800;
    margin-bottom: 8px;
    letter-spacing: -0.02em;
  }

  .modal-card.win h1 {
    background: linear-gradient(90deg, #34d399, #38bdf8, #fbbf24);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  .modal-card.lose h1 {
    background: linear-gradient(90deg, #fb7185, #f97316);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  .modal-sub {
    font-size: 14px;
    color: var(--muted);
    margin-bottom: 18px;
  }

  .modal-stats {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-bottom: 20px;
  }

  .modal-stat-box {
    background: rgba(30, 41, 59, 0.55);
    border: 1px solid rgba(148, 163, 184, 0.14);
    border-radius: 10px;
    padding: 10px;
  }

  .modal-stat-box .lbl {
    font-size: 11px;
    text-transform: uppercase;
    color: var(--muted);
    letter-spacing: 0.06em;
  }

  .modal-stat-box .val {
    font-size: 20px;
    font-weight: 700;
    color: #fff;
    margin-top: 2px;
  }

  .primary-btn {
    width: 100%;
    padding: 12px 18px;
    border: none;
    border-radius: 10px;
    font-size: 15px;
    font-weight: 700;
    color: #06101e;
    background: linear-gradient(90deg, #38bdf8, #34d399);
    cursor: pointer;
    box-shadow: 0 8px 22px rgba(56, 189, 248, 0.32);
    transition: transform 0.12s, filter 0.15s;
  }

  .primary-btn:hover {
    filter: brightness(1.08);
  }

  .primary-btn:active {
    transform: scale(0.98);
  }

  .legend {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
    font-size: 11px;
    color: var(--muted);
    flex-wrap: wrap;
    flex-shrink: 0;
  }

  .legend-item {
    display: inline-flex;
    align-items: center;
    gap: 5px;
  }

  .dot {
    width: 9px;
    height: 9px;
    border-radius: 3px;
    display: inline-block;
  }

  @media (max-width: 540px) {
    .app { padding: 6px; gap: 6px; }
    .hud { padding: 6px 10px; gap: 6px; }
    .stat-value { font-size: 15px; }
    .hud-group { gap: 10px; }
    .icon-btn { padding: 5px 8px; font-size: 11px; }
    .legend { font-size: 10px; gap: 10px; }
    .demo-pill { padding: 10px 14px; margin-bottom: 12%; }
  }
</style>
</head>
<body>
<div class="app">
  <header class="hud">
    <div class="hud-group">
      <div class="stat">
        <span class="stat-label">Score</span>
        <span class="stat-value" id="scoreVal">0</span>
      </div>
      <div class="stat">
        <span class="stat-label">Lives</span>
        <div class="lives-row" id="livesRow"></div>
      </div>
      <div class="stat">
        <span class="stat-label">Bricks</span>
        <span class="stat-value" id="bricksVal">0</span>
      </div>
      <div class="power-badge" id="wideBadge">
        <span>↔ WIDE</span>
        <div class="power-bar-wrap"><div class="power-bar-fill" id="wideBar"></div></div>
        <span id="wideTime">10.0s</span>
      </div>
    </div>
    <div class="hud-actions">
      <button class="icon-btn" id="soundBtn" type="button" title="Toggle Sound">Sound: ON</button>
      <button class="icon-btn" id="restartHudBtn" type="button" title="Restart Game">Restart</button>
    </div>
  </header>

  <main class="stage-wrap">
    <div class="canvas-frame show-cursor" id="canvasFrame">
      <canvas id="gameCanvas"></canvas>

      <!-- Demo Mode Prompt -->
      <div class="overlay" id="demoOverlay">
        <div class="demo-pill">
          <div class="demo-tag">Auto-Play Demo</div>
          <h2>Click or press a key to start</h2>
          <p>Mouse or ← → Arrow keys to move &bull; Catch power-ups</p>
        </div>
      </div>

      <!-- Launch Prompt -->
      <div class="overlay hidden" id="launchOverlay">
        <div class="demo-pill" style="margin-bottom: 14%; animation: none;">
          <h2>Ready to Launch</h2>
          <p>Click or press Space to release the ball</p>
        </div>
      </div>

      <!-- Game Over / Victory Modal -->
      <div class="overlay interactive hidden" id="endOverlay">
        <div class="modal-card" id="endCard">
          <h1 id="endTitle">Game Over</h1>
          <p class="modal-sub" id="endSub">All balls lost!</p>
          <div class="modal-stats">
            <div class="modal-stat-box">
              <div class="lbl">Final Score</div>
              <div class="val" id="endScore">0</div>
            </div>
            <div class="modal-stat-box">
              <div class="lbl">Bricks Broken</div>
              <div class="val" id="endBricks">0</div>
            </div>
          </div>
          <button class="primary-btn" id="playAgainBtn" type="button">Play Again</button>
        </div>
      </div>
    </div>
  </main>

  <footer class="legend">
    <span class="legend-item"><span class="dot" style="background:#94a3b8; border:1px solid #e2e8f0;"></span> Tough Brick (2 hits)</span>
    <span class="legend-item"><span class="dot" style="background:#38bdf8;"></span> ↔ Wide Paddle (10s)</span>
    <span class="legend-item"><span class="dot" style="background:#fbbf24;"></span> ••• Multi-Ball (3 balls)</span>
    <span class="legend-item">Controls: Mouse / ← → Keys</span>
  </footer>
</div>

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

  const LOGIC_W = 800;
  const LOGIC_H = 580;

  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const canvasFrame = document.getElementById('canvasFrame');

  // HUD & Overlays
  const scoreEl = document.getElementById('scoreVal');
  const livesRow = document.getElementById('livesRow');
  const bricksEl = document.getElementById('bricksVal');
  const wideBadge = document.getElementById('wideBadge');
  const wideBar = document.getElementById('wideBar');
  const wideTimeEl = document.getElementById('wideTime');
  const soundBtn = document.getElementById('soundBtn');
  const restartHudBtn = document.getElementById('restartHudBtn');

  const demoOverlay = document.getElementById('demoOverlay');
  const launchOverlay = document.getElementById('launchOverlay');
  const endOverlay = document.getElementById('endOverlay');
  const endCard = document.getElementById('endCard');
  const endTitle = document.getElementById('endTitle');
  const endSub = document.getElementById('endSub');
  const endScore = document.getElementById('endScore');
  const endBricks = document.getElementById('endBricks');
  const playAgainBtn = document.getElementById('playAgainBtn');

  // High-DPI sizing
  let dpr = Math.min(window.devicePixelRatio || 1, 2);
  function resizeCanvas() {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = LOGIC_W * dpr;
    canvas.height = LOGIC_H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  resizeCanvas();
  window.addEventListener('resize', resizeCanvas);

  // Lightweight Web Audio Synthesizer (initialized safely on first user gesture)
  let audioCtx = null;
  let soundEnabled = true;

  function ensureAudio() {
    if (!audioCtx && soundEnabled) {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (AC) audioCtx = new AC();
    }
    if (audioCtx && audioCtx.state === 'suspended') {
      audioCtx.resume().catch(() => {});
    }
  }

  function playTone(freq, duration = 0.07, type = 'triangle', gainVal = 0.08, slideTo = null) {
    if (!soundEnabled || !audioCtx || state === 'DEMO') return;
    try {
      const now = audioCtx.currentTime;
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = type;
      osc.frequency.setValueAtTime(freq, now);
      if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, now + duration);
      gain.gain.setValueAtTime(gainVal, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.start(now);
      osc.stop(now + duration);
    } catch (_) {}
  }

  soundBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    soundEnabled = !soundEnabled;
    soundBtn.textContent = soundEnabled ? 'Sound: ON' : 'Sound: OFF';
    if (soundEnabled) ensureAudio();
  });

  // Constants
  const WALL_PAD = 12;
  const TOP_PAD = 12;
  const BASE_PADDLE_W = 108;
  const WIDE_PADDLE_W = 176;
  const PADDLE_H = 15;
  const PADDLE_Y = LOGIC_H - 38;
  const PADDLE_SPEED = 640;
  const BALL_R = 7.5;
  const BASE_BALL_SPEED = 430;
  const MAX_BALL_SPEED = 530;
  const MAX_BOUNCE_ANGLE = (66 * Math.PI) / 180; // sharp angle at edges, 0 at center
  const WIDE_DURATION = 10.0;

  // Row color palettes
  const ROW_PALETTE = [
    { fill: '#f43f5e', light: '#fda4af', dark: '#be123c' }, // Rose
    { fill: '#f97316', light: '#fdba74', dark: '#c2410c' }, // Orange
    { fill: '#eab308', light: '#fde047', dark: '#a16207' }, // Amber
    { fill: '#10b981', light: '#6ee7b7', dark: '#047857' }, // Emerald
    { fill: '#06b6d4', light: '#67e8f9', dark: '#0e7490' }, // Cyan
    { fill: '#6366f1', light: '#a5b4fc', dark: '#4338ca' }, // Indigo
    { fill: '#ec4899', light: '#f9a8d4', dark: '#be185d' }  // Pink
  ];

  const TOUGH_PALETTE = {
    fill: '#64748b',
    light: '#cbd5e1',
    dark: '#334155',
    crackedFill: '#475569',
    crackedLight: '#94a3b8',
    crackedDark: '#1e293b'
  };

  // Game State
  let state = 'DEMO'; // 'DEMO' | 'READY' | 'PLAYING' | 'GAMEOVER' | 'VICTORY'
  let score = 0;
  let lives = 3;
  let totalBricksCount = 0;
  let brokenBricksCount = 0;
  let wideTimer = 0;
  let shakeTime = 0;
  let shakeMag = 0;

  let paddle = {
    x: LOGIC_W / 2 - BASE_PADDLE_W / 2,
    y: PADDLE_Y,
    w: BASE_PADDLE_W,
    targetW: BASE_PADDLE_W,
    h: PADDLE_H,
    hitFlash: 0
  };

  let balls = [];
  let bricks = [];
  let powerUps = [];
  let particles = [];
  let floatingTexts = [];

  // Input State
  const keys = { left: false, right: false };
  let aiTargetOffset = 0;
  let aiOffsetTimer = 0;

  // Brick Layout: 7 rows x 10 cols = 70 bricks
  function buildLevel() {
    bricks = [];
    const cols = 10;
    const rows = 7;
    const gap = 6;
    const sideMargin = 28;
    const topStart = 44;
    const brickW = (LOGIC_W - sideMargin * 2 - gap * (cols - 1)) / cols;
    const brickH = 24;

    // Hand-crafted tough brick (2-hit) & power-up layout for balanced action right away
    // 'T' = tough 2-hit, 'W' = wide power-up, 'M' = multi-ball power-up, 'TW'/'TM' = tough + power-up
    const layout = [
      ['T',  'T',  'TW', 'T',  'T',  'T',  'T',  'TM', 'T',  'T'],
      ['N',  'T',  'N',  'M',  'T',  'T',  'W',  'N',  'T',  'N'],
      ['N',  'N',  'T',  'N',  'N',  'N',  'N',  'T',  'N',  'N'],
      ['W',  'N',  'N',  'T',  'TM', 'TW', 'T',  'N',  'N',  'M'],
      ['N',  'T',  'N',  'N',  'N',  'N',  'N',  'N',  'T',  'N'],
      ['N',  'N',  'M',  'N',  'T',  'T',  'N',  'W',  'N',  'N'],
      ['N',  'W',  'N',  'T',  'N',  'N',  'T',  'N',  'M',  'N']
    ];

    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        const code = layout[r][c];
        const isTough = code.includes('T');
        let powerUp = null;
        if (code.includes('W')) powerUp = 'wide';
        else if (code.includes('M')) powerUp = 'multi';

        const x = sideMargin + c * (brickW + gap);
        const y = topStart + r * (brickH + gap);

        // Pre-generate deterministic jagged crack path for 2-hit bricks
        const crackSeed = (r * 13 + c * 7) % 10;
        const crackPoints = [
          { x: 0.22 + crackSeed * 0.02, y: 0.05 },
          { x: 0.36 + crackSeed * 0.01, y: 0.42 },
          { x: 0.28 + crackSeed * 0.015, y: 0.68 },
          { x: 0.44, y: 0.95 },
          // Branch
          { x: 0.36 + crackSeed * 0.01, y: 0.42 },
          { x: 0.65, y: 0.56 },
          { x: 0.78, y: 0.22 }
        ];

        bricks.push({
          x, y, w: brickW, h: brickH,
          row: r,
          col: c,
          hp: isTough ? 2 : 1,
          maxHp: isTough ? 2 : 1,
          powerUp,
          palette: isTough ? TOUGH_PALETTE : ROW_PALETTE[r % ROW_PALETTE.length],
          hitFlash: 0,
          crackPoints
        });
      }
    }

    totalBricksCount = bricks.length;
    brokenBricksCount = 0;
  }

  function createBall(x, y, angleFromUp, speed = BASE_BALL_SPEED) {
    return {
      x,
      y,
      r: BALL_R,
      vx: speed * Math.sin(angleFromUp),
      vy: -speed * Math.cos(angleFromUp),
      speed,
      trail: []
    };
  }

  function resetPaddleAndBall(launchImmediately = false) {
    paddle.w = BASE_PADDLE_W;
    paddle.targetW = BASE_PADDLE_W;
    paddle.x = LOGIC_W / 2 - paddle.w / 2;
    paddle.hitFlash = 0;
    wideTimer = 0;

    const startAngle = launchImmediately ? 0.42 : 0.32;
    balls = [
      createBall(
        paddle.x + paddle.w / 2,
        paddle.y - BALL_R - 1,
        startAngle,
        BASE_BALL_SPEED
      )
    ];
    powerUps = [];
  }

  function startDemo() {
    state = 'DEMO';
    score = 0;
    lives = 3;
    particles = [];
    floatingTexts = [];
    buildLevel();
    resetPaddleAndBall(true);
    // Position paddle & ball for immediate exciting opening shot in demo
    balls[0].x = LOGIC_W * 0.48;
    balls[0].y = paddle.y - BALL_R - 4;
    balls[0].vx = BASE_BALL_SPEED * Math.sin(0.26);
    balls[0].vy = -BASE_BALL_SPEED * Math.cos(0.26);

    demoOverlay.classList.remove('hidden');
    launchOverlay.classList.add('hidden');
    endOverlay.classList.add('hidden');
    canvasFrame.classList.add('show-cursor');
    updateHUD();
  }

  function startNewGame(autoLaunch = false) {
    ensureAudio();
    score = 0;
    lives = 3;
    particles = [];
    floatingTexts = [];
    buildLevel();
    resetPaddleAndBall(false);
    state = autoLaunch ? 'PLAYING' : 'READY';

    demoOverlay.classList.add('hidden');
    endOverlay.classList.add('hidden');
    launchOverlay.classList.toggle('hidden', autoLaunch);
    canvasFrame.classList.remove('show-cursor');
    updateHUD();
  }

  function launchBall() {
    if (state !== 'READY') return;
    state = 'PLAYING';
    launchOverlay.classList.add('hidden');
    const b = balls[0];
    // Slight angle based on paddle position from center, or crisp default
    const offset = ((paddle.x + paddle.w / 2) - LOGIC_W / 2) / (LOGIC_W / 2);
    const angle = Math.max(-0.45, Math.min(0.45, offset * 0.35 + (offset === 0 ? 0.2 : 0)));
    b.vx = b.speed * Math.sin(angle);
    b.vy = -b.speed * Math.cos(angle);
    playTone(520, 0.08, 'sine', 0.09, 760);
  }

  function updateHUD() {
    scoreEl.textContent = score.toLocaleString();
    bricksEl.textContent = bricks.length;

    // Render lives orbs
    if (livesRow.children.length !== 3) {
      livesRow.innerHTML = '';
      for (let i = 0; i < 3; i++) {
        const orb = document.createElement('span');
        orb.className = 'life-orb';
        livesRow.appendChild(orb);
      }
    }
    for (let i = 0; i < 3; i++) {
      livesRow.children[i].classList.toggle('lost', i >= lives);
    }

    // Wide power-up timer
    if (wideTimer > 0) {
      wideBadge.classList.add('active');
      wideBar.style.width = `${Math.max(0, Math.min(100, (wideTimer / WIDE_DURATION) * 100))}%`;
      wideTimeEl.textContent = `${wideTimer.toFixed(1)}s`;
    } else {
      wideBadge.classList.remove('active');
    }
  }

  // Particles & Floating Text
  function spawnBrickParticles(brick, count, isCrackOnly = false) {
    const pal = brick.palette;
    const cx = brick.x + brick.w / 2;
    const cy = brick.y + brick.h / 2;

    for (let i = 0; i < count; i++) {
      const px = isCrackOnly
        ? cx + (Math.random() - 0.5) * brick.w * 0.5
        : brick.x + Math.random() * brick.w;
      const py = isCrackOnly
        ? cy + (Math.random() - 0.5) * brick.h * 0.5
        : brick.y + Math.random() * brick.h;

      const angle = Math.random() * Math.PI * 2;
      const speed = (isCrackOnly ? 60 : 80) + Math.random() * (isCrackOnly ? 140 : 230);
      const colors = [pal.fill, pal.light, '#ffffff'];
      particles.push({
        x: px,
        y: py,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - (isCrackOnly ? 20 : 40),
        size: isCrackOnly ? 2 + Math.random() * 2.5 : 2.5 + Math.random() * 4.5,
        color: colors[Math.floor(Math.random() * colors.length)],
        alpha: 1,
        life: 0,
        maxLife: (isCrackOnly ? 0.28 : 0.45) + Math.random() * 0.3,
        gravity: 340,
        rot: Math.random() * Math.PI,
        vRot: (Math.random() - 0.5) * 10
      });
    }
  }

  function spawnCelebrationConfetti() {
    const colors = ['#38bdf8', '#34d399', '#fbbf24', '#f472b6', '#a78bfa'];
    for (let i = 0; i < 85; i++) {
      particles.push({
        x: Math.random() * LOGIC_W,
        y: -10 - Math.random() * 120,
        vx: (Math.random() - 0.5) * 180,
        vy: 120 + Math.random() * 220,
        size: 4 + Math.random() * 5,
        color: colors[i % colors.length],
        alpha: 1,
        life: 0,
        maxLife: 2.2 + Math.random() * 1.4,
        gravity: 90,
        rot: Math.random() * Math.PI,
        vRot: (Math.random() - 0.5) * 8
      });
    }
  }

  function spawnPowerUpParticles(x, y, color) {
    for (let i = 0; i < 14; i++) {
      const a = (i / 14) * Math.PI * 2;
      const spd = 90 + Math.random() * 110;
      particles.push({
        x, y,
        vx: Math.cos(a) * spd,
        vy: Math.sin(a) * spd,
        size: 3 + Math.random() * 2.5,
        color,
        alpha: 1,
        life: 0,
        maxLife: 0.4 + Math.random() * 0.2,
        gravity: 120,
        rot: 0,
        vRot: 0
      });
    }
  }

  function addFloatingText(x, y, text, color) {
    floatingTexts.push({
      x, y, text, color,
      life: 0,
      maxLife: 0.95
    });
  }

  // Power-Up Activation
  function activatePowerUp(pu) {
    if (pu.type === 'wide') {
      wideTimer = WIDE_DURATION;
      paddle.targetW = WIDE_PADDLE_W;
      addFloatingText(paddle.x + paddle.w / 2, paddle.y - 14, 'WIDE PADDLE 10s!', '#38bdf8');
      spawnPowerUpParticles(pu.x, pu.y, '#38bdf8');
      playTone(440, 0.14, 'triangle', 0.1, 880);
    } else if (pu.type === 'multi') {
      // Ensure at least 3 balls in play (if 1 -> 3, if 2 -> 3, if >=3 -> add 2 capped at 6)
      const needed = balls.length < 3 ? (3 - balls.length) : Math.min(2, 6 - balls.length);
      const source = balls[0] || createBall(paddle.x + paddle.w / 2, paddle.y - BALL_R - 2, 0);
      const baseAngle = Math.atan2(source.vx, -source.vy);
      const spreads = [-0.42, 0.42, -0.7, 0.7];

      for (let i = 0; i < needed; i++) {
        const ang = baseAngle + spreads[i % spreads.length];
        const nb = createBall(source.x, source.y, ang, source.speed);
        // Ensure upward or well-angled component so newly spawned balls don't immediately dive straight out
        if (nb.vy > 0 && source.y > LOGIC_H * 0.65) {
          nb.vy = -Math.abs(nb.vy);
        }
        balls.push(nb);
      }
      addFloatingText(paddle.x + paddle.w / 2, paddle.y - 14, 'MULTI-BALL x3!', '#fbbf24');
      spawnPowerUpParticles(pu.x, pu.y, '#fbbf24');
      playTone(587, 0.14, 'sine', 0.1, 1046);
    }
    score += 50;
  }

  // Clamp paddle cleanly inside playfield
  function clampPaddle() {
    const minX = WALL_PAD;
    const maxX = LOGIC_W - WALL_PAD - paddle.w;
    if (paddle.x < minX) paddle.x = minX;
    if (paddle.x > maxX) paddle.x = maxX;
  }

  // Collision Helper: Circle vs AABB (never tunnels when paired with sub-stepping)
  function collideBallWithBrick(ball, brick) {
    const closestX = Math.max(brick.x, Math.min(ball.x, brick.x + brick.w));
    const closestY = Math.max(brick.y, Math.min(ball.y, brick.y + brick.h));
    const dx = ball.x - closestX;
    const dy = ball.y - closestY;
    const distSq = dx * dx + dy * dy;

    if (distSq > ball.r * ball.r) return false;

    // Resolve penetration cleanly along minimum overlap axis or normal
    if (distSq > 0.0001) {
      const dist = Math.sqrt(distSq);
      const overlap = ball.r - dist;
      const nx = dx / dist;
      const ny = dy / dist;

      ball.x += nx * overlap;
      ball.y += ny * overlap;

      // Reflect velocity along collision normal if moving into brick
      const dot = ball.vx * nx + ball.vy * ny;
      if (dot < 0) {
        ball.vx -= 2 * dot * nx;
        ball.vy -= 2 * dot * ny;
      }
    } else {
      // Ball center is inside AABB (rare fallback): push out along shallowest axis
      const leftPen = ball.x - brick.x;
      const rightPen = (brick.x + brick.w) - ball.x;
      const topPen = ball.y - brick.y;
      const bottomPen = (brick.y + brick.h) - ball.y;
      const minPen = Math.min(leftPen, rightPen, topPen, bottomPen);

      if (minPen === leftPen) {
        ball.x = brick.x - ball.r;
        ball.vx = -Math.abs(ball.vx);
      } else if (minPen === rightPen) {
        ball.x = brick.x + brick.w + ball.r;
        ball.vx = Math.abs(ball.vx);
      } else if (minPen === topPen) {
        ball.y = brick.y - ball.r;
        ball.vy = -Math.abs(ball.vy);
      } else {
        ball.y = brick.y + brick.h + ball.r;
        ball.vy = Math.abs(ball.vy);
      }
    }

    // Prevent shallow infinite horizontal loops
    const speed = Math.hypot(ball.vx, ball.vy) || ball.speed;
    const minVy = speed * 0.2;
    if (Math.abs(ball.vy) < minVy) {
      ball.vy = (ball.vy >= 0 ? 1 : -1) * minVy;
      const scale = speed / Math.hypot(ball.vx, ball.vy);
      ball.vx *= scale;
      ball.vy *= scale;
    }

    return true;
  }

  function handleBrickHit(brick, ball) {
    brick.hp -= 1;
    brick.hitFlash = 1;

    if (brick.hp <= 0) {
      // Break brick
      const idx = bricks.indexOf(brick);
      if (idx !== -1) bricks.splice(idx, 1);
      brokenBricksCount += 1;
      score += brick.maxHp === 2 ? 150 : 100;

      spawnBrickParticles(brick, 18, false);
      shakeTime = 0.08;
      shakeMag = 2.6;

      // Slight speed boost up to cap
      ball.speed = Math.min(MAX_BALL_SPEED, ball.speed + 1.5);
      const curSpd = Math.hypot(ball.vx, ball.vy);
      if (curSpd > 0) {
        ball.vx = (ball.vx / curSpd) * ball.speed;
        ball.vy = (ball.vy / curSpd) * ball.speed;
      }

      if (brick.powerUp) {
        powerUps.push({
          x: brick.x + brick.w / 2,
          y: brick.y + brick.h / 2,
          w: 36,
          h: 18,
          vy: 145,
          type: brick.powerUp,
          pulse: 0
        });
      }

      playTone(320 + (7 - brick.row) * 55, 0.075, 'triangle', 0.08, 220);
    } else {
      // Tough brick cracked
      score += 40;
      spawnBrickParticles(brick, 9, true);
      shakeTime = 0.05;
      shakeMag = 1.6;
      playTone(210, 0.07, 'square', 0.05, 160);
    }
  }

  // AI Controller for Demo Mode
  function updateDemoAI(dt) {
    aiOffsetTimer -= dt;
    if (aiOffsetTimer <= 0) {
      aiOffsetTimer = 0.9 + Math.random() * 1.2;
      aiTargetOffset = (Math.random() - 0.5) * (paddle.w * 0.42);
    }

    // Prioritize lowest downward-moving ball, or nearby falling power-up if safe
    let targetX = LOGIC_W / 2;
    let bestScore = -Infinity;

    for (const b of balls) {
      let priority = b.y;
      if (b.vy > 0) priority += 260; // strongly prioritize incoming balls
      if (priority > bestScore) {
        bestScore = priority;
        // Simple predictive aim when ball is descending
        if (b.vy > 0 && b.y < paddle.y) {
          const timeToPaddle = (paddle.y - b.y) / b.vy;
          let predX = b.x + b.vx * timeToPaddle;
          // Mirror off side walls once for accuracy
          const minX = WALL_PAD + b.r;
          const maxX = LOGIC_W - WALL_PAD - b.r;
          if (predX < minX) predX = minX + (minX - predX);
          if (predX > maxX) predX = maxX - (predX - maxX);
          targetX = predX + aiTargetOffset;
        } else {
          targetX = b.x + aiTargetOffset * 0.5;
        }
      }
    }

    // Opportunistically catch power-up if no ball is dangerously low
    const lowestBallY = balls.reduce((m, b) => Math.max(m, b.y), 0);
    if (lowestBallY < paddle.y - 160 && powerUps.length > 0) {
      let lowestPU = powerUps[0];
      for (const pu of powerUps) {
        if (pu.y > lowestPU.y) lowestPU = pu;
      }
      targetX = lowestPU.x;
    }

    const desiredPaddleX = targetX - paddle.w / 2;
    const diff = desiredPaddleX - paddle.x;
    const maxMove = PADDLE_SPEED * 0.92 * dt;
    if (Math.abs(diff) <= maxMove) {
      paddle.x = desiredPaddleX;
    } else {
      paddle.x += Math.sign(diff) * maxMove;
    }
    clampPaddle();
  }

  // Main Update Loop
  function update(dt) {
    // Update screen shake & flash
    if (shakeTime > 0) shakeTime = Math.max(0, shakeTime - dt);
    if (paddle.hitFlash > 0) paddle.hitFlash = Math.max(0, paddle.hitFlash - dt * 5);

    // Smoothly animate paddle width toward targetW (keeping center anchored)
    if (Math.abs(paddle.w - paddle.targetW) > 0.5) {
      const oldCenter = paddle.x + paddle.w / 2;
      paddle.w += (paddle.targetW - paddle.w) * Math.min(1, dt * 12);
      paddle.x = oldCenter - paddle.w / 2;
      clampPaddle();
    } else {
      paddle.w = paddle.targetW;
    }

    // Wide paddle countdown
    if ((state === 'PLAYING' || state === 'DEMO') && wideTimer > 0) {
      wideTimer -= dt;
      if (wideTimer <= 0) {
        wideTimer = 0;
        paddle.targetW = BASE_PADDLE_W;
      }
    }

    // Brick flash decay
    for (const br of bricks) {
      if (br.hitFlash > 0) br.hitFlash = Math.max(0, br.hitFlash - dt * 6);
    }

    // Update particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life += dt;
      if (p.life >= p.maxLife) {
        particles.splice(i, 1);
        continue;
      }
      p.vy += p.gravity * dt;
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.rot += p.vRot * dt;
      p.alpha = 1 - p.life / p.maxLife;
    }

    // Update floating texts
    for (let i = floatingTexts.length - 1; i >= 0; i--) {
      const ft = floatingTexts[i];
      ft.life += dt;
      ft.y -= 34 * dt;
      if (ft.life >= ft.maxLife) floatingTexts.splice(i, 1);
    }

    if (state === 'GAMEOVER' || state === 'VICTORY') return;

    // Paddle control
    if (state === 'DEMO') {
      updateDemoAI(dt);
    } else if (state === 'READY' || state === 'PLAYING') {
      let move = 0;
      if (keys.left) move -= 1;
      if (keys.right) move += 1;
      if (move !== 0) {
        paddle.x += move * PADDLE_SPEED * dt;
        clampPaddle();
      }
    }

    // Ready state: ball rides smoothly on paddle
    if (state === 'READY') {
      if (balls[0]) {
        balls[0].x = paddle.x + paddle.w / 2;
        balls[0].y = paddle.y - BALL_R - 1;
        balls[0].trail = [];
      }
      updateHUD();
      return;
    }

    // Update falling power-ups
    for (let i = powerUps.length - 1; i >= 0; i--) {
      const pu = powerUps[i];
      pu.y += pu.vy * dt;
      pu.pulse += dt * 6;

      // Check paddle catch
      const hitX = pu.x + pu.w / 2 >= paddle.x && pu.x - pu.w / 2 <= paddle.x + paddle.w;
      const hitY = pu.y + pu.h / 2 >= paddle.y && pu.y - pu.h / 2 <= paddle.y + paddle.h;
      if (hitX && hitY) {
        activatePowerUp(pu);
        powerUps.splice(i, 1);
        continue;
      }

      if (pu.y - pu.h / 2 > LOGIC_H + 10) {
        powerUps.splice(i, 1);
      }
    }

    // Sub-stepped Ball Physics (prevents any tunneling through bricks or paddle)
    for (let bIdx = balls.length - 1; bIdx >= 0; bIdx--) {
      const ball = balls[bIdx];

      // Record trail
      ball.trail.push({ x: ball.x, y: ball.y });
      if (ball.trail.length > 7) ball.trail.shift();

      const totalDist = Math.hypot(ball.vx * dt, ball.vy * dt);
      const maxStep = BALL_R * 0.32; // ~2.4px max per step
      const steps = Math.max(1, Math.ceil(totalDist / maxStep));
      const subDt = dt / steps;

      for (let s = 0; s < steps; s++) {
        ball.x += ball.vx * subDt;
        ball.y += ball.vy * subDt;

        // Left & Right Walls
        if (ball.x - ball.r < WALL_PAD) {
          ball.x = WALL_PAD + ball.r;
          ball.vx = Math.abs(ball.vx);
          playTone(260, 0.04, 'sine', 0.04);
        } else if (ball.x + ball.r > LOGIC_W - WALL_PAD) {
          ball.x = LOGIC_W - WALL_PAD - ball.r;
          ball.vx = -Math.abs(ball.vx);
          playTone(260, 0.04, 'sine', 0.04);
        }

        // Top Wall
        if (ball.y - ball.r < TOP_PAD) {
          ball.y = TOP_PAD + ball.r;
          ball.vy = Math.abs(ball.vy);
          playTone(280, 0.04, 'sine', 0.04);
        }

        // Paddle Collision (only when moving downward)
        if (ball.vy > 0) {
          const closestX = Math.max(paddle.x, Math.min(ball.x, paddle.x + paddle.w));
          const closestY = Math.max(paddle.y, Math.min(ball.y, paddle.y + paddle.h));
          const dx = ball.x - closestX;
          const dy = ball.y - closestY;

          if (dx * dx + dy * dy <= ball.r * ball.r && ball.y - ball.r <= paddle.y + paddle.h * 0.65) {
            // Position ball cleanly on top of paddle
            ball.y = paddle.y - ball.r - 0.5;

            // Bounce angle based on hit location: center = straight up, edges = sharp angle
            const paddleCenter = paddle.x + paddle.w / 2;
            const normOffset = Math.max(-1, Math.min(1, (ball.x - paddleCenter) / (paddle.w / 2)));
            const bounceAngle = normOffset * MAX_BOUNCE_ANGLE;

            ball.vx = ball.speed * Math.sin(bounceAngle);
            ball.vy = -ball.speed * Math.cos(bounceAngle);
            paddle.hitFlash = 1;

            playTone(380 + (1 - Math.abs(normOffset)) * 120, 0.06, 'sine', 0.08, 460);
            continue;
          }
        }

        // Brick Collisions (resolve at most one brick per micro-step for stability)
        for (let i = 0; i < bricks.length; i++) {
          const br = bricks[i];
          if (collideBallWithBrick(ball, br)) {
            handleBrickHit(br, ball);
            break;
          }
        }
      }

      // Check bottom out
      if (ball.y - ball.r > LOGIC_H + 12) {
        balls.splice(bIdx, 1);
      }
    }

    // Check Victory
    if (bricks.length === 0) {
      if (state === 'DEMO') {
        startDemo();
      } else {
        triggerEnd('VICTORY');
      }
      return;
    }

    // Check Ball Loss / Life Loss
    if (balls.length === 0) {
      if (state === 'DEMO') {
        lives -= 1;
        if (lives <= 0) {
          startDemo();
        } else {
          resetPaddleAndBall(true);
        }
      } else {
        lives -= 1;
        playTone(180, 0.25, 'sawtooth', 0.09, 85);
        if (lives <= 0) {
          triggerEnd('GAMEOVER');
          return;
        } else {
          state = 'READY';
          resetPaddleAndBall(false);
          launchOverlay.classList.remove('hidden');
        }
      }
    }

    updateHUD();
  }

  function triggerEnd(result) {
    state = result;
    updateHUD();
    canvasFrame.classList.add('show-cursor');
    launchOverlay.classList.add('hidden');
    endOverlay.classList.remove('hidden');

    if (result === 'VICTORY') {
      const lifeBonus = lives * 250;
      score += lifeBonus;
      scoreEl.textContent = score.toLocaleString();
      endCard.className = 'modal-card win';
      endTitle.textContent = 'Victory!';
      endSub.textContent = `Level cleared! (+${lifeBonus} life bonus)`;
      spawnCelebrationConfetti();
      playTone(523, 0.14, 'triangle', 0.1, 1046);
    } else {
      endCard.className = 'modal-card lose';
      endTitle.textContent = 'Game Over';
      endSub.textContent = 'Out of lives — give it another shot!';
    }

    endScore.textContent = score.toLocaleString();
    endBricks.textContent = `${brokenBricksCount}/${totalBricksCount}`;
  }

  // Drawing Helpers
  function roundRectPath(x, y, w, h, r) {
    const rr = Math.min(r, w / 2, h / 2);
    ctx.beginPath();
    ctx.moveTo(x + rr, y);
    ctx.arcTo(x + w, y, x + w, y + h, rr);
    ctx.arcTo(x + w, y + h, x, y + h, rr);
    ctx.arcTo(x, y + h, x, y, rr);
    ctx.arcTo(x, y, x + w, y, rr);
    ctx.closePath();
  }

  function drawBackground() {
    ctx.fillStyle = '#080d1a';
    ctx.fillRect(0, 0, LOGIC_W, LOGIC_H);

    // Subtle arcade grid
    ctx.strokeStyle = 'rgba(56, 189, 248, 0.045)';
    ctx.lineWidth = 1;
    const step = 40;
    ctx.beginPath();
    for (let x = WALL_PAD; x <= LOGIC_W - WALL_PAD; x += step) {
      ctx.moveTo(x, TOP_PAD);
      ctx.lineTo(x, LOGIC_H);
    }
    for (let y = TOP_PAD; y <= LOGIC_H; y += step) {
      ctx.moveTo(WALL_PAD, y);
      ctx.lineTo(LOGIC_W - WALL_PAD, y);
    }
    ctx.stroke();

    // Arena walls border (left, top, right)
    ctx.strokeStyle = 'rgba(56, 189, 248, 0.35)';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(WALL_PAD, LOGIC_H);
    ctx.lineTo(WALL_PAD, TOP_PAD);
    ctx.lineTo(LOGIC_W - WALL_PAD, TOP_PAD);
    ctx.lineTo(LOGIC_W - WALL_PAD, LOGIC_H);
    ctx.stroke();
  }

  function drawPowerUpIcon(cx, cy, type, scale = 1) {
    ctx.save();
    ctx.translate(cx, cy);
    ctx.scale(scale, scale);

    if (type === 'wide') {
      // Glowing cyan expand-arrows icon
      ctx.strokeStyle = '#e0f2fe';
      ctx.fillStyle = '#38bdf8';
      ctx.lineWidth = 2;
      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';

      // Center bar
      ctx.beginPath();
      ctx.moveTo(-8, 0);
      ctx.lineTo(8, 0);
      // Left arrowhead
      ctx.moveTo(-4, -3.5);
      ctx.lineTo(-8.5, 0);
      ctx.lineTo(-4, 3.5);
      // Right arrowhead
      ctx.moveTo(4, -3.5);
      ctx.lineTo(8.5, 0);
      ctx.lineTo(4, 3.5);
      ctx.stroke();
    } else if (type === 'multi') {
      // Triple glowing ball icon
      const pts = [
        { x: 0, y: -3.5 },
        { x: -4.5, y: 3 },
        { x: 4.5, y: 3 }
      ];
      for (const p of pts) {
        ctx.beginPath();
        ctx.arc(p.x, p.y, 2.7, 0, Math.PI * 2);
        ctx.fillStyle = '#fef08a';
        ctx.fill();
        ctx.strokeStyle = '#f59e0b';
        ctx.lineWidth = 1;
        ctx.stroke();
      }
    }
    ctx.restore();
  }

  function drawBricks() {
    for (const br of bricks) {
      const isTough = br.maxHp === 2;
      const isCracked = isTough && br.hp === 1;
      const pal = br.palette;

      const fill = isCracked ? pal.crackedFill : pal.fill;
      const light = isCracked ? pal.crackedLight : pal.light;
      const dark = isCracked ? pal.crackedDark : pal.dark;

      // Shadow
      ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
      roundRectPath(br.x + 1, br.y + 2, br.w, br.h, 5);
      ctx.fill();

      // Main brick body
      const grad = ctx.createLinearGradient(br.x, br.y, br.x, br.y + br.h);
      grad.addColorStop(0, light);
      grad.addColorStop(0.22, fill);
      grad.addColorStop(1, dark);
      ctx.fillStyle = grad;
      roundRectPath(br.x, br.y, br.w, br.h, 5);
      ctx.fill();

      // Tough brick metallic frame & rivets
      if (isTough) {
        ctx.strokeStyle = isCracked ? 'rgba(203, 213, 225, 0.35)' : 'rgba(241, 245, 249, 0.6)';
        ctx.lineWidth = 1.4;
        roundRectPath(br.x + 1, br.y + 1, br.w - 2, br.h - 2, 4);
        ctx.stroke();

        // Corner rivets when intact
        if (!isCracked) {
          ctx.fillStyle = 'rgba(241, 245, 249, 0.7)';
          const rx1 = br.x + 5, rx2 = br.x + br.w - 5;
          const ry1 = br.y + 5, ry2 = br.y + br.h - 5;
          for (const [rx, ry] of [[rx1, ry1], [rx2, ry1], [rx1, ry2], [rx2, ry2]]) {
            ctx.beginPath();
            ctx.arc(rx, ry, 1.2, 0, Math.PI * 2);
            ctx.fill();
          }
        }
      } else {
        // Subtle top gloss
        ctx.fillStyle = 'rgba(255, 255, 255, 0.22)';
        roundRectPath(br.x + 3, br.y + 2.5, br.w - 6, 4, 2);
        ctx.fill();
      }

      // Visible cracks after 1st hit on tough bricks
      if (isCracked) {
        const pts = br.crackPoints;
        // Dark fracture line
        ctx.strokeStyle = '#0f172a';
        ctx.lineWidth = 2.4;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.beginPath();
        ctx.moveTo(br.x + pts[0].x * br.w, br.y + pts[0].y * br.h);
        for (let i = 1; i <= 3; i++) {
          ctx.lineTo(br.x + pts[i].x * br.w, br.y + pts[i].y * br.h);
        }
        ctx.moveTo(br.x + pts[4].x * br.w, br.y + pts[4].y * br.h);
        ctx.lineTo(br.x + pts[5].x * br.w, br.y + pts[5].y * br.h);
        ctx.lineTo(br.x + pts[6].x * br.w, br.y + pts[6].y * br.h);
        ctx.stroke();

        // Bright fractured edge highlight
        ctx.strokeStyle = 'rgba(226, 232, 240, 0.55)';
        ctx.lineWidth = 0.9;
        ctx.beginPath();
        ctx.moveTo(br.x + pts[0].x * br.w + 1, br.y + pts[0].y * br.h);
        for (let i = 1; i <= 3; i++) {
          ctx.lineTo(br.x + pts[i].x * br.w + 1, br.y + pts[i].y * br.h);
        }
        ctx.stroke();
      }

      // Visible Power-Up Icon Badge on Brick
      if (br.powerUp) {
        const cx = br.x + br.w / 2;
        const cy = br.y + br.h / 2;
        const badgeColor = br.powerUp === 'wide' ? 'rgba(8, 47, 73, 0.82)' : 'rgba(69, 26, 3, 0.82)';
        const ringColor = br.powerUp === 'wide' ? '#38bdf8' : '#fbbf24';

        ctx.fillStyle = badgeColor;
        roundRectPath(cx - 13, cy - 8, 26, 16, 8);
        ctx.fill();
        ctx.strokeStyle = ringColor;
        ctx.lineWidth = 1.2;
        roundRectPath(cx - 13, cy - 8, 26, 16, 8);
        ctx.stroke();

        drawPowerUpIcon(cx, cy, br.powerUp, 0.9);
      }

      // Hit flash overlay
      if (br.hitFlash > 0) {
        ctx.fillStyle = `rgba(255, 255, 255, ${br.hitFlash * 0.55})`;
        roundRectPath(br.x, br.y, br.w, br.h, 5);
        ctx.fill();
      }
    }
  }

  function drawPowerUps() {
    for (const pu of powerUps) {
      const x = pu.x - pu.w / 2;
      const y = pu.y - pu.h / 2;
      const isWide = pu.type === 'wide';
      const glow = isWide ? 'rgba(56, 189, 248, 0.45)' : 'rgba(251, 191, 36, 0.45)';
      const bg1 = isWide ? '#0369a1' : '#b45309';
      const bg2 = isWide ? '#0284c7' : '#d97706';
      const border = isWide ? '#7dd3fc' : '#fde047';

      ctx.save();
      ctx.shadowColor = glow;
      ctx.shadowBlur = 12 + Math.sin(pu.pulse) * 4;

      const grad = ctx.createLinearGradient(x, y, x, y + pu.h);
      grad.addColorStop(0, bg2);
      grad.addColorStop(1, bg1);
      ctx.fillStyle = grad;
      roundRectPath(x, y, pu.w, pu.h, pu.h / 2);
      ctx.fill();

      ctx.lineWidth = 1.6;
      ctx.strokeStyle = border;
      ctx.stroke();
      ctx.restore();

      drawPowerUpIcon(pu.x, pu.y, pu.type, 1.0);
    }
  }

  function drawPaddle() {
    const isWide = wideTimer > 0;
    const x = paddle.x;
    const y = paddle.y;
    const w = paddle.w;
    const h = paddle.h;

    ctx.save();
    ctx.shadowColor = isWide ? 'rgba(52, 211, 153, 0.55)' : 'rgba(56, 189, 248, 0.45)';
    ctx.shadowBlur = 14 + paddle.hitFlash * 10;

    const grad = ctx.createLinearGradient(x, y, x, y + h);
    if (isWide) {
      grad.addColorStop(0, '#a7f3d0');
      grad.addColorStop(0.35, '#10b981');
      grad.addColorStop(1, '#047857');
    } else {
      grad.addColorStop(0, '#bae6fd');
      grad.addColorStop(0.35, '#38bdf8');
      grad.addColorStop(1, '#0369a1');
    }

    ctx.fillStyle = grad;
    roundRectPath(x, y, w, h, h / 2);
    ctx.fill();
    ctx.restore();

    // Glowing side tips to visually hint edge-angle steering
    ctx.fillStyle = isWide ? '#fde047' : '#f472b6';
    roundRectPath(x + 2, y + 2, 10, h - 4, 4);
    ctx.fill();
    roundRectPath(x + w - 12, y + 2, 10, h - 4, 4);
    ctx.fill();

    // Center notch indicator
    ctx.fillStyle = 'rgba(255, 255, 255, 0.65)';
    roundRectPath(x + w / 2 - 8, y + 3, 16, 3, 1.5);
    ctx.fill();

    // Hit flash
    if (paddle.hitFlash > 0) {
      ctx.fillStyle = `rgba(255, 255, 255, ${paddle.hitFlash * 0.45})`;
      roundRectPath(x, y, w, h, h / 2);
      ctx.fill();
    }
  }

  function drawBalls() {
    for (const ball of balls) {
      // Motion trail
      for (let i = 0; i < ball.trail.length; i++) {
        const t = ball.trail[i];
        const ratio = (i + 1) / (ball.trail.length + 1);
        ctx.beginPath();
        ctx.arc(t.x, t.y, ball.r * ratio * 0.85, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(56, 189, 248, ${ratio * 0.28})`;
        ctx.fill();
      }

      // Ball glow + crisp sphere gradient
      ctx.save();
      ctx.shadowColor = 'rgba(125, 211, 252, 0.8)';
      ctx.shadowBlur = 12;

      const grad = ctx.createRadialGradient(
        ball.x - ball.r * 0.3,
        ball.y - ball.r * 0.3,
        ball.r * 0.15,
        ball.x,
        ball.y,
        ball.r
      );
      grad.addColorStop(0, '#ffffff');
      grad.addColorStop(0.55, '#bae6fd');
      grad.addColorStop(1, '#0284c7');

      ctx.beginPath();
      ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
      ctx.fillStyle = grad;
      ctx.fill();
      ctx.restore();
    }
  }

  function drawParticles() {
    for (const p of particles) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, p.alpha);
      ctx.translate(p.x, p.y);
      ctx.rotate(p.rot);
      ctx.fillStyle = p.color;
      ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
      ctx.restore();
    }
  }

  function drawFloatingTexts() {
    ctx.save();
    ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
    ctx.textAlign = 'center';
    for (const ft of floatingTexts) {
      const alpha = Math.max(0, 1 - ft.life / ft.maxLife);
      ctx.globalAlpha = alpha;
      ctx.fillStyle = ft.color;
      ctx.shadowColor = 'rgba(0,0,0,0.8)';
      ctx.shadowBlur = 6;
      ctx.fillText(ft.text, ft.x, ft.y);
    }
    ctx.restore();
  }

  function render() {
    ctx.save();
    if (shakeTime > 0) {
      const sx = (Math.random() - 0.5) * shakeMag * 2;
      const sy = (Math.random() - 0.5) * shakeMag * 2;
      ctx.translate(sx, sy);
    }

    drawBackground();
    drawBricks();
    drawPowerUps();
    drawPaddle();
    drawBalls();
    drawParticles();
    drawFloatingTexts();

    ctx.restore();
  }

  // Input Handlers
  function movePaddleToClientX(clientX) {
    const rect = canvas.getBoundingClientRect();
    if (rect.width <= 0) return;
    const logicX = ((clientX - rect.left) / rect.width) * LOGIC_W;
    paddle.x = logicX - paddle.w / 2;
    clampPaddle();
  }

  canvasFrame.addEventListener('mousemove', (e) => {
    if (state === 'READY' || state === 'PLAYING') {
      movePaddleToClientX(e.clientX);
    }
  });

  canvasFrame.addEventListener('touchstart', (e) => {
    if (state === 'DEMO') {
      startNewGame(false);
      return;
    }
    if (state === 'READY') {
      if (e.touches[0]) movePaddleToClientX(e.touches[0].clientX);
      launchBall();
      return;
    }
    if (state === 'PLAYING' && e.touches[0]) {
      movePaddleToClientX(e.touches[0].clientX);
    }
  }, { passive: true });

  canvasFrame.addEventListener('touchmove', (e) => {
    if ((state === 'READY' || state === 'PLAYING') && e.touches[0]) {
      movePaddleToClientX(e.touches[0].clientX);
    }
  }, { passive: true });

  canvasFrame.addEventListener('click', (e) => {
    // Ignore clicks on modal buttons (handled separately)
    if (e.target.closest('button')) return;

    if (state === 'DEMO') {
      startNewGame(false);
      movePaddleToClientX(e.clientX);
    } else if (state === 'READY') {
      movePaddleToClientX(e.clientX);
      launchBall();
    }
  });

  window.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
      keys.left = true;
    } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
      keys.right = true;
    }

    if (state === 'DEMO') {
      if (['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) return;
      e.preventDefault();
      startNewGame(false);
      return;
    }

    if (state === 'READY' && (e.key === ' ' || e.key === 'Enter' || e.key === 'ArrowUp')) {
      e.preventDefault();
      launchBall();
      return;
    }

    if ((state === 'GAMEOVER' || state === 'VICTORY') && (e.key === ' ' || e.key === 'Enter')) {
      e.preventDefault();
      startNewGame(false);
    }
  });

  window.addEventListener('keyup', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
      keys.left = false;
    } else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
      keys.right = false;
    }
  });

  playAgainBtn.addEventListener('click', () => startNewGame(false));
  restartHudBtn.addEventListener('click', () => startNewGame(false));

  // Animation Loop
  let lastTime = performance.now();
  function loop(now) {
    const rawDt = (now - lastTime) / 1000;
    lastTime = now;
    const dt = Math.min(rawDt, 0.04); // Cap dt so tab-switching never breaks physics

    update(dt);
    render();
    requestAnimationFrame(loop);
  }

  // Boot directly into active Demo Mode
  startDemo();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Reasoning trace (3,162 tokens)
**Designing Responsive Breakout**

I'm focusing on crafting a sleek, responsive Breakout experience entirely within a single HTML file, utilizing `<canvas>` and vanilla JavaScript. The core goal is a self-contained game that looks stellar across various viewports, ensuring no external dependencies mar the presentation.

**Implementing Interactive Elements**

I'm refining the game logic, specifically focusing on the paddle-ball interaction to ensure accurate and engaging bounce angles based on hit location. Simultaneously, I'm integrating the power-up drop and collection mechanics, ensuring visual feedback and timed effects for features like the wider paddle and multi-ball.

**Refining Ball Physics**

I'm meticulously detailing the ball's physics, specifically addressing how it interacts with bricks and the paddle to prevent tunneling. My current focus is on implementing a robust sub-stepping approach, breaking down movement into smaller increments to ensure accurate collision detection and resolution.

**Improving Collision Detection**

I'm refining the ball-brick collision to prevent tunneling by processing single brick hits per sub-step and calculating precise penetration. For paddle collisions, I'm focusing on determining the bounce angle dynamically based on the hit offset across the paddle's surface.

**Polishing Visuals and Controls**

I'm now focusing on the visual polish, aiming for a deep, dark arcade aesthetic with neon accents and sharp, high-DPI rendering on the canvas. I'm also implementing a demo mode that will auto-play on load, controlled by an AI that tracks the ball.

**Adding Audio and Enhancing UX**

I'm integrating subtle sound effects using the Web Audio API, ensuring they're initialized only after user interaction to avoid autoplay warnings. A mute toggle will also be included for player preference.

**Implementing Demo Mode AI**

My current focus is on developing the AI for the demo mode. It will automatically track the lowest downward-moving ball or nearest falling power-up to showcase gameplay without requiring user input initially.

**Activating Game Features**

I'm focusing on the immediate visual impact of the demo, ensuring bricks break with particles and power-ups drop within seconds. I'm also implementing a clear, unobtrusive prompt for users to take control.

**Enhancing Power-Up Mechanics**

I'm refining the multi-ball power-up logic to ensure it consistently brings the ball count up to three, or adds two more if at least three are already in play, capped at five. I'm also implementing clear visual indicators on bricks that carry power-ups, using distinct icons and glows that directly transfer to the falling power-up capsules.

**Cracking Bricks Visually**

I'm focusing on detailing the visual representation of brick damage. After their first hit, bricks will now display distinct jagged crack lines with a dimmed appearance, accompanied by small chip sparks to enhance the impact. I'm also refining the initial brick layout for better player engagement.

**Configuring Launch Mechanics**

I'm deciding on the most intuitive launch sequence. The ball will rest on the paddle initially, with a clear prompt. A single click will initiate gameplay, and a subsequent click or spacebar press will launch the ball, allowing for pre-launch paddle orientation. Lives decrement on losing a ball, with power-ups resetting but the ball returning to the paddle.

**Defining End States**

I'm now mapping out the game's conclusion. This involves setting up clear transition logic for both game over and victory screens, complete with score summaries and replay options. I'm also ensuring the demo mode gracefully handles these states by looping seamlessly.